Compilando esto:
#include <iostream>
int main()
{
for (int i = 0; i < 4; ++i)
std::cout << i*1000000000 << std::endl;
}
y gccproduce la siguiente advertencia:
warning: iteration 3u invokes undefined behavior [-Waggressive-loop-optimizations]
std::cout << i*1000000000 << std::endl;
^
Entiendo que hay un desbordamiento de entero con signo.
Lo que no puedo entender es ¿por qué el ivalor se rompe por esa operación de desbordamiento?
He leído las respuestas a ¿Por qué el desbordamiento de enteros en x86 con GCC causa un bucle infinito? , pero todavía no tengo claro por qué sucede esto: entiendo que "indefinido" significa "cualquier cosa puede suceder", pero ¿cuál es la causa subyacente de este comportamiento específico ?
En línea: http://ideone.com/dMrRKR
Compilador: gcc (4.8)
c++
gcc
undefined-behavior
zerkms
fuente
fuente

O2, y laO3bandera, pero noO0oO1Respuestas:
El desbordamiento de enteros con signo (como estrictamente hablando, no existe el "desbordamiento de enteros sin signo") significa un comportamiento indefinido . Y esto significa que puede pasar cualquier cosa, y discutir por qué sucede bajo las reglas de C ++ no tiene sentido.
C ++ 11 borrador N3337: §5.4: 1
Su código compilado con
g++ -O3emite advertencia (incluso sin-Wall)La única forma en que podemos analizar lo que está haciendo el programa es leyendo el código de ensamblaje generado.
Aquí está el listado completo de ensamblaje:
Apenas puedo leer el ensamblaje, pero incluso puedo ver la
addl $1000000000, %edilínea. El código resultante se parece más aEste comentario de @TC:
me dio la idea de comparar el código de ensamblaje del código de OP con el código de ensamblaje del siguiente código, sin comportamiento indefinido.
Y, de hecho, el código correcto tiene la condición de terminación.
Enfréntalo, escribiste el código con errores y deberías sentirte mal. Llevar las consecuencias.
... o, como alternativa, hacer un uso adecuado de mejores diagnósticos y mejores herramientas de depuración, para eso están:
habilitar todas las advertencias
-Walles la opción gcc que habilita todas las advertencias útiles sin falsos positivos. Este es un mínimo que siempre debes usar.-Wallya que pueden advertir sobre falsos positivosusar banderas de depuración para depurar
-ftrapvatrapa el programa en desbordamiento,-fcatch-undefined-behaviorcoge una gran cantidad de casos de comportamiento indefinido (nota:"a lot of" != "all of them")Use gcc's
-fwrapv1 - esta regla no se aplica al "desbordamiento de enteros sin signo", como §3.9.1.4 dice que
y, por ejemplo, el resultado de
UINT_MAX + 1está matemáticamente definido - por las reglas del módulo aritmético 2 nfuente
ive afectado? En general, el comportamiento indefinido es no tener este tipo de efectos secundarios extraños, después de todo,i*100000000debería ser un valoricualquier valor mayor que 2 tiene un comportamiento indefinido -> (2) podemos suponer quei <= 2para fines de optimización -> (3) la condición del bucle siempre es verdadera -> (4 ) está optimizado en un bucle infinito.ien 1e9 cada iteración (y cambiando la condición del bucle en consecuencia). Esta es una optimización perfectamente válida bajo la regla "como si" ya que este programa no podía observar la diferencia si se comportaba bien. Por desgracia, no lo es, y la optimización "se escapa".Respuesta corta,
gccespecíficamente ha documentado este problema, podemos ver que en las notas de lanzamiento de gcc 4.8 que dice ( énfasis mío en adelante ):y, de hecho, si usamos
-fno-aggressive-loop-optimizationsel comportamiento de bucle infinito debería cesar y lo hace en todos los casos que he probado.La respuesta larga comienza sabiendo que el desbordamiento de enteros con signo es un comportamiento indefinido al mirar el borrador de la sección estándar de C ++
5Expresiones, párrafo 4, que dice:Sabemos que el estándar dice que el comportamiento indefinido es impredecible de la nota que viene con la definición que dice:
Pero, ¿qué puede hacer el
gccoptimizador para convertir esto en un bucle infinito? Suena completamente raro. Pero afortunadamentegccnos da una pista para descubrirlo en la advertencia:La pista es
Waggressive-loop-optimizations, ¿qué significa eso? Afortunadamente para nosotros, esta no es la primera vez que esta optimización ha roto el código de esta manera y tenemos suerte porque John Regehr ha documentado un caso en el artículo GCC pre-4.8 Breaks Broken SPEC 2006 Benchmarks que muestra el siguiente código:el artículo dice:
y luego dice:
Entonces, lo que el compilador debe estar haciendo en algunos casos es suponer que dado que el desbordamiento de enteros con signo es un comportamiento indefinido,
isiempre debe ser menor4y, por lo tanto, tenemos un bucle infinito.Explica que esto es muy similar a la infame eliminación de verificación de puntero nulo del kernel de Linux, donde al ver este código:
gccdedujo que dado quesse hizos->f;referencia en y dado que la desreferenciación de un puntero nulo es un comportamiento indefinido, entoncessno debe ser nulo y, por lo tanto, optimiza laif (!s)comprobación en la siguiente línea.La lección aquí es que los optimizadores modernos son muy agresivos sobre la explotación de comportamientos indefinidos y lo más probable es que solo se vuelvan más agresivos. Claramente, con solo unos pocos ejemplos, podemos ver que el optimizador hace cosas que parecen completamente irracionales para un programador, pero en retrospectiva desde la perspectiva de los optimizadores tiene sentido.
fuente
tl; dr El código genera una prueba que entero + entero positivo == entero negativo . Por lo general, el optimizador no optimiza esto, pero en el caso específico de
std::endlser utilizado a continuación, el compilador optimiza esta prueba. Todavía no he descubierto qué tiene de especialendl.Del código de ensamblaje en -O1 y niveles superiores, está claro que gcc refactoriza el ciclo para:
El mayor valor que funciona correctamente es
715827882, es decir, floor (INT_MAX/3). El fragmento de ensamblado en-O1es:Tenga en cuenta que
-1431655768está4 * 715827882en el complemento de 2.Golpear
-O2optimiza eso a lo siguiente:Entonces, la optimización que se ha hecho es simplemente que
addlse movió más arriba.Si, en su
715827883lugar, volvemos a compilar , la versión -O1 es idéntica aparte del número modificado y el valor de prueba. Sin embargo, -O2 hace un cambio:Donde había
cmpl $-1431655764, %esien-O1, esa línea se ha eliminado-O2. El optimizador debe haber decidido que agregar715827883a%esinunca puede ser igual-1431655764.Esto es bastante desconcertante. Añadiendo que a
INT_MIN+1no generar el resultado esperado, por lo que el optimizador debe haber decidido que%esinunca puede haberINT_MIN+1y no estoy seguro de por qué decidiría eso.¡En el ejemplo de trabajo parece que sería igualmente válido concluir que sumar
715827882a un número no puede ser igualINT_MIN + 715827882 - 2! (esto solo es posible si realmente ocurre el ajuste), pero no optimiza la salida de línea en ese ejemplo.El código que estaba usando es:
Si
std::endl(std::cout)se elimina, la optimización ya no se produce. De hecho, reemplazarlostd::cout.put('\n'); std::flush(std::cout);también hace que la optimización no suceda, aunquestd::endlesté en línea.La alineación de
std::endlparece afectar la parte anterior de la estructura del bucle (que no entiendo muy bien lo que está haciendo, pero lo publicaré aquí en caso de que alguien más lo haga):Con código original y
-O2:Con la expansión en línea de mymanual
std::endl,-O2:Una diferencia entre estos dos es que
%esise usa en el original y%ebxen la segunda versión; ¿Hay alguna diferencia en la semántica definida entre%esiy%ebxen general? (No sé mucho sobre el montaje x86).fuente
Otro ejemplo de este error que se informa en gcc es cuando tiene un bucle que se ejecuta durante un número constante de iteraciones, pero está utilizando la variable de contador como un índice en una matriz que tiene menos de ese número de elementos, como:
El compilador puede determinar que este bucle intentará acceder a la memoria fuera de la matriz 'a'. El compilador se queja de esto con este mensaje bastante críptico:
fuente
Parece que el desbordamiento de enteros ocurre en la cuarta iteración (para
i = 3).signedel desbordamiento de enteros invoca un comportamiento indefinido . En este caso no se puede predecir nada. ¡El ciclo puede iterar solo4veces o puede ir al infinito o cualquier otra cosa!El resultado puede variar de compilador a compilador o incluso para diferentes versiones del mismo compilador.
C11: 1.3.24 comportamiento indefinido:
fuente