c++ - tiempo - ¿Por qué el desbordamiento de enteros en x86 con GCC causa un bucle infinito?
ciclos en c++ (6)
El siguiente código entra en un ciclo infinito en GCC:
#include <iostream>
using namespace std;
int main(){
int i = 0x10000000;
int c = 0;
do{
c++;
i += i;
cout << i << endl;
}while (i > 0);
cout << c << endl;
return 0;
}
Así que aquí está la oferta: El desbordamiento de enteros con signo es un comportamiento técnicamente indefinido. Pero GCC en x86 implementa la aritmética de enteros usando instrucciones enteras x86, que envuelven el desbordamiento.
Por lo tanto, esperaba que se desborde, a pesar de que es un comportamiento indefinido. Pero claramente ese no es el caso. ¿Así que ... qué me perdí?
Compilé esto usando:
~/Desktop$ g++ main.cpp -O2
Salida GCC:
~/Desktop$ ./a.out
536870912
1073741824
-2147483648
0
0
0
... (infinite loop)
Con las optimizaciones desactivadas, no hay un bucle infinito y la salida es correcta. Visual Studio también compila correctamente esto y da el siguiente resultado:
Salida correcta:
~/Desktop$ g++ main.cpp
~/Desktop$ ./a.out
536870912
1073741824
-2147483648
3
Aquí hay algunas otras variaciones:
i *= 2; // Also fails and goes into infinite loop.
i <<= 1; // This seems okay. It does not enter infinite loop.
Aquí está toda la información relevante de la versión:
~/Desktop$ g++ -v
Using built-in specs.
COLLECT_GCC=g++
COLLECT_LTO_WRAPPER=/usr/lib/x86_64-linux-gnu/gcc/x86_64-linux-gnu/4.5.2/lto-wrapper
Target: x86_64-linux-gnu
Configured with: ..
...
Thread model: posix
gcc version 4.5.2 (Ubuntu/Linaro 4.5.2-8ubuntu4)
~/Desktop$
Entonces la pregunta es: ¿Esto es un error en GCC? ¿O no entendí bien cómo GCC maneja la aritmética de enteros?
* También estoy etiquetando esta C, porque supongo que este error se reproducirá en C. (Todavía no lo he verificado).
EDITAR:
Aquí está el montaje del ciclo: (si lo reconocí correctamente)
.L5:
addl %ebp, %ebp
movl $_ZSt4cout, %edi
movl %ebp, %esi
.cfi_offset 3, -40
call _ZNSolsEi
movq %rax, %rbx
movq (%rax), %rax
movq -24(%rax), %rax
movq 240(%rbx,%rax), %r13
testq %r13, %r13
je .L10
cmpb $0, 56(%r13)
je .L3
movzbl 67(%r13), %eax
.L4:
movsbl %al, %esi
movq %rbx, %rdi
addl $1, %r12d
call _ZNSo3putEc
movq %rax, %rdi
call _ZNSo5flushEv
cmpl $3, %r12d
jne .L5
Cuando el estándar dice que es un comportamiento indefinido, lo significa . Cualquier cosa puede suceder. "Cualquier cosa" incluye "por lo general enteros, pero en ocasiones sucede algo extraño".
Sí, en las CPU x86, los enteros generalmente se envuelven de la manera esperada. Esta es una de esas excepciones. El compilador supone que no provocará un comportamiento indefinido y optimiza la prueba de bucle. Si realmente quieres un envolvente, pasa -fwrapv
a g++
o gcc
al compilar; esto le proporciona una semántica de desbordamiento bien definida (dos complementos), pero puede dañar el rendimiento.
Es simple: el comportamiento indefinido, especialmente con la optimización ( -O2
) activada, significa que cualquier cosa puede suceder.
Su código se comporta como (usted) esperado sin el interruptor -O2
.
Funciona bastante bien con icl y tcc por cierto, pero no puedes confiar en cosas así ...
De acuerdo con this , la optimización de gcc realmente explota el desbordamiento de entero con signo. Esto significaría que el "error" es por diseño.
Incluso si un compilador especificara que el desbordamiento de enteros debe considerarse una forma "no crítica" de Comportamiento Indefinido (como se define en el Anexo L), el resultado de un desbordamiento de enteros debe ser, en ausencia de una promesa de plataforma específica de un comportamiento más específico, como mínimo considerado como un "valor parcialmente indeterminado". De acuerdo con dichas reglas, agregar 1073741824 + 1073741824 podría considerarse arbitrariamente como rendimiento 2147483648 o -2147483648 o cualquier otro valor que fuera congruente con 2147483648 mod 4294967296, y los valores obtenidos por adiciones podrían considerarse arbitrariamente como cualquier valor que fuera congruente con 0 mod 4294967296.
Las reglas que permiten el desbordamiento para producir "valores parcialmente indeterminados" estarían lo suficientemente bien definidas para cumplir con la letra y el espíritu del Anexo L, pero no evitarían que un compilador haga las mismas inferencias generalmente útiles que estarían justificadas si los desbordamientos no fueran restringidos. Comportamiento indefinido Evitaría que un compilador haga algunas "optimizaciones" falsas cuyo efecto principal en muchos casos es exigir que los programadores agreguen desorden adicional al código cuyo único propósito es evitar tales "optimizaciones"; si eso sería bueno o no depende del punto de vista de uno.
Lo importante a tener en cuenta aquí es que los programas C ++ se escriben para la máquina abstracta C ++ (que generalmente se emula a través de instrucciones de hardware). El hecho de que esté compilando para x86 es totalmente irrelevante para el hecho de que esto tiene un comportamiento indefinido.
El compilador es libre de usar la existencia de un comportamiento indefinido para mejorar sus optimizaciones (eliminando un condicional de un bucle, como en este ejemplo). No existe una asignación garantizada, o incluso útil, entre construcciones de nivel de C ++ y construcciones de código de máquina de nivel x86, aparte del requisito de que el código de máquina producirá, cuando se ejecute, el resultado exigido por la máquina abstracta de C ++.
Por favor, gente, el comportamiento indefinido es exactamente eso, indefinido . Significa que cualquier cosa podría pasar. En la práctica (como en este caso), el compilador es libre de asumir que no se le llamará, y hará lo que le plazca si eso pudiera hacer que el código sea más rápido / más pequeño. Lo que sucede con el código que no debe ejecutarse es algo que nadie sabe. Dependerá del código circundante (dependiendo de eso, el compilador podría generar código diferente), las variables / constantes utilizadas, los indicadores del compilador, ... Ah, y el compilador podría actualizarse y escribir el mismo código de manera diferente, o podría obtener otro compilador con una vista diferente sobre la generación de código. O simplemente obtener una máquina diferente, incluso otro modelo en la misma línea de arquitectura podría muy bien tener su propio comportamiento indefinido (buscar códigos de operación indefinidos, algunos programadores emprendedores descubrieron que en algunas de esas primeras máquinas algunas veces funcionaban cosas útiles ...) . No hay "el compilador da un comportamiento definido en el comportamiento indefinido". Hay áreas que están definidas por la implementación, y allí usted debe poder contar con que el compilador se comporte de manera consistente.