Compilando esto:
#include <iostream>
int main()
{
for (int i = 0; i < 4; ++i)
std::cout << i*1000000000 << std::endl;
}
y gcc
produce 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 i
valor 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 laO3
bandera, pero noO0
oO1
Respuestas:
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++ -O3
emite 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, %edi
lí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
-Wall
es la opción gcc que habilita todas las advertencias útiles sin falsos positivos. Este es un mínimo que siempre debes usar.-Wall
ya que pueden advertir sobre falsos positivosusar banderas de depuración para depurar
-ftrapv
atrapa el programa en desbordamiento,-fcatch-undefined-behavior
coge una gran cantidad de casos de comportamiento indefinido (nota:"a lot of" != "all of them"
)Use gcc's
-fwrapv
1 - 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 + 1
está matemáticamente definido - por las reglas del módulo aritmético 2 nfuente
i
ve afectado? En general, el comportamiento indefinido es no tener este tipo de efectos secundarios extraños, después de todo,i*100000000
debería ser un valori
cualquier valor mayor que 2 tiene un comportamiento indefinido -> (2) podemos suponer quei <= 2
para fines de optimización -> (3) la condición del bucle siempre es verdadera -> (4 ) está optimizado en un bucle infinito.i
en 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,
gcc
especí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-optimizations
el 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 ++
5
Expresiones, 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
gcc
optimizador para convertir esto en un bucle infinito? Suena completamente raro. Pero afortunadamentegcc
nos 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,
i
siempre debe ser menor4
y, 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:
gcc
dedujo que dado ques
se hizos->f;
referencia en y dado que la desreferenciación de un puntero nulo es un comportamiento indefinido, entoncess
no 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::endl
ser 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-O1
es:Tenga en cuenta que
-1431655768
está4 * 715827882
en el complemento de 2.Golpear
-O2
optimiza eso a lo siguiente:Entonces, la optimización que se ha hecho es simplemente que
addl
se movió más arriba.Si, en su
715827883
lugar, 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, %esi
en-O1
, esa línea se ha eliminado-O2
. El optimizador debe haber decidido que agregar715827883
a%esi
nunca puede ser igual-1431655764
.Esto es bastante desconcertante. Añadiendo que a
INT_MIN+1
no generar el resultado esperado, por lo que el optimizador debe haber decidido que%esi
nunca puede haberINT_MIN+1
y no estoy seguro de por qué decidiría eso.¡En el ejemplo de trabajo parece que sería igualmente válido concluir que sumar
715827882
a 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::endl
esté en línea.La alineación de
std::endl
parece 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
%esi
se usa en el original y%ebx
en la segunda versión; ¿Hay alguna diferencia en la semántica definida entre%esi
y%ebx
en 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
).signed
el desbordamiento de enteros invoca un comportamiento indefinido . En este caso no se puede predecir nada. ¡El ciclo puede iterar solo4
veces 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