Compilando isso:
#include <iostream>
int main()
{
for (int i = 0; i < 4; ++i)
std::cout << i*1000000000 << std::endl;
}
e gcc
produz o seguinte aviso:
warning: iteration 3u invokes undefined behavior [-Waggressive-loop-optimizations]
std::cout << i*1000000000 << std::endl;
^
Eu entendo que há um estouro inteiro assinado.
O que não consigo entender é por que o i
valor é quebrado por essa operação de estouro?
Eu li as respostas para Por que o número inteiro excedente no x86 com o GCC causa um loop infinito? , mas ainda não sei ao certo por que isso acontece. Entendi que "indefinido" significa "tudo pode acontecer", mas qual é a causa subjacente desse comportamento específico ?
Online: http://ideone.com/dMrRKR
Compilador: gcc (4.8)
c++
gcc
undefined-behavior
zerkms
fonte
fonte
O2
, eO3
, mas nãoO0
ou #O1
Respostas:
Estouro de número inteiro assinado (como estritamente falando, não existe "estouro de número inteiro não assinado") significa comportamento indefinido . E isso significa que tudo pode acontecer, e discutir por que isso acontece sob as regras do C ++ não faz sentido.
Rascunho C ++ 11 N3337: §5.4: 1
Seu código compilado com
g++ -O3
aviso de emissão (mesmo sem-Wall
)A única maneira de analisar o que o programa está fazendo é lendo o código de montagem gerado.
Aqui está a lista completa da montagem:
Eu mal consigo ler montagem, mas até consigo ver a
addl $1000000000, %edi
linha. O código resultante se parece maisEste comentário do @TC:
deu-me a idéia de comparar o código de montagem do código do OP com o código de montagem do código a seguir, sem comportamento indefinido.
E, de fato, o código correto tem condição de término.
Lide com isso, você escreveu o código do buggy e deve se sentir mal. Suportar as consequências.
... ou, alternativamente, faça o uso adequado de melhores diagnósticos e melhores ferramentas de depuração - é para isso que servem:
ativar todos os avisos
-Wall
é a opção gcc que permite todos os avisos úteis sem falsos positivos. Esse é o mínimo que você deve sempre usar.-Wall
, pois podem alertar sobre falsos positivosuse sinalizadores de depuração para depuração
-ftrapv
intercepta o programa em excesso,-fcatch-undefined-behavior
captura muitas instâncias de comportamento indefinido (nota"a lot of" != "all of them"
:)Use gcc's
-fwrapv
1 - esta regra não se aplica ao "estouro de número inteiro não assinado", como §3.9.1.4 diz que
e, por exemplo, resultado de
UINT_MAX + 1
é matematicamente definido - pelas regras do módulo aritmético 2 nfonte
i
ele é afetado? Em um comportamento indefinido em geral não está a ter este tipo de efeitos colaterais estranhos, afinal,i*100000000
deve ser um rvaluei
qualquer valor maior que 2 tem um comportamento indefinido -> (2) podemos assumir que,i <= 2
para fins de otimização -> (3) a condição do loop é sempre verdadeira -> (4 ) é otimizado em um loop infinito.i
em 1e9 cada iteração (e alterando a condição do loop de acordo). Essa é uma otimização perfeitamente válida sob a regra "como se", pois esse programa não pôde observar a diferença se estivesse se comportando bem. Infelizmente, não é, e a otimização "vaza".Resposta curta,
gcc
especificamente documentou esse problema, podemos ver nas notas de versão do gcc 4.8 que diz ( ênfase minha daqui para frente ):e, de fato, se usarmos
-fno-aggressive-loop-optimizations
o comportamento do loop infinito cessará e ocorre em todos os casos que testei.A resposta longa começa com o conhecimento de que o excesso de número inteiro assinado é um comportamento indefinido, observando o rascunho da seção padrão C ++,
5
Expressões, parágrafo 4, que diz:Sabemos que o padrão diz que o comportamento indefinido é imprevisível a partir da observação que acompanha a definição que diz:
Mas o que o
gcc
otimizador pode fazer no mundo para transformar isso em um loop infinito? Parece completamente maluco. Mas, felizmente,gcc
nos dá uma pista para descobrir isso no aviso:A pista é: o
Waggressive-loop-optimizations
que isso significa? Felizmente para nós, não é a primeira vez que essa otimização quebra o código dessa maneira e temos sorte porque John Regehr documentou um caso no artigo GCC pré-4.8 Breaks Broken SPEC 2006 Benchmarks, que mostra o seguinte código:o artigo diz:
e depois diz:
Então, o que o compilador deve estar fazendo em alguns casos está assumindo, já que o excesso de número inteiro assinado é um comportamento indefinido,
i
sempre deve ser menor que4
e, portanto, temos um loop infinito.Ele explica que isso é muito semelhante à infame remoção de verificação de ponteiro nulo do kernel do Linux, onde ao ver este código:
gcc
inferiu que, uma vez ques
foi diferidos->f;
e desreferenciar um ponteiro nulo é um comportamento indefinido, eles
não deve ser nulo e, portanto, otimiza aif (!s)
verificação na próxima linha.A lição aqui é que os otimizadores modernos são muito agressivos sobre a exploração de comportamentos indefinidos e provavelmente só serão mais agressivos. Claramente, com apenas alguns exemplos, podemos ver que o otimizador faz coisas que parecem completamente irracionais para um programador, mas em retrospecto da perspectiva dos otimizadores, faz sentido.
fonte
tl; dr O código gera um teste que inteiro + número inteiro positivo == número inteiro negativo . Normalmente, o otimizador não otimiza isso, mas no caso específico de
std::endl
ser usado a seguir, o compilador otimiza esse teste. Ainda não descobri o que há de especialendl
.No código do assembly nos níveis -O1 e superiores, é claro que o gcc refatora o loop para:
O maior valor que funciona corretamente é
715827882
, ou seja, floor (INT_MAX/3
). O snippet de montagem em-O1
é:Observe que o complemento
-1431655768
está4 * 715827882
no 2's.Bater
-O2
otimiza isso para o seguinte:Portanto, a otimização que foi feita é apenas que a
addl
subida foi mais alta.Se recompilarmos em
715827883
vez disso, a versão -O1 será idêntica à parte do número alterado e do valor do teste. No entanto, -O2 faz uma alteração:Onde havia
cmpl $-1431655764, %esi
a-O1
, essa linha foi removido porque-O2
. O otimizador deve ter decidido que adicionar715827883
a%esi
nunca pode ser igual-1431655764
.Isso é bastante intrigante. Acrescentando que, para
INT_MIN+1
não gerar o resultado esperado, para que o otimizador deve ter decidido que%esi
nunca pode serINT_MIN+1
e eu não sei por que ele iria decidir isso.No exemplo de trabalho, parece igualmente válido concluir que adicionar
715827882
a um número não pode ser igualINT_MIN + 715827882 - 2
! (isso só é possível se a envolvência realmente ocorrer), mas não otimiza a linha nesse exemplo.O código que eu estava usando é:
Se o
std::endl(std::cout)
for removido, a otimização não ocorrerá mais. De fato, substituí-lo porstd::cout.put('\n'); std::flush(std::cout);
também faz com que a otimização não aconteça, mesmo questd::endl
esteja embutida.O inlining de
std::endl
parece afetar a parte anterior da estrutura do loop (que eu não entendo direito o que está fazendo, mas vou publicá-la aqui, caso alguém o faça):Com código original e
-O2
:Com inlining mymanual de
std::endl
,-O2
:Uma diferença entre esses dois é que
%esi
é usado no original e%ebx
na segunda versão; existe alguma diferença na semântica definida entre%esi
e%ebx
em geral? (Eu não sei muito sobre montagem x86).fonte
Outro exemplo desse erro relatado no gcc é quando você tem um loop que é executado para um número constante de iterações, mas você está usando a variável counter como um índice em uma matriz que possui menos que esse número de itens, como:
O compilador pode determinar que esse loop tentará acessar a memória fora da matriz 'a'. O compilador reclama disso com esta mensagem bastante enigmática:
fonte
Parece que o excesso de número inteiro ocorre na 4ª iteração (para
i = 3
).signed
estouro inteiro invoca um comportamento indefinido . Nesse caso, nada pode ser previsto. O loop pode iterar apenas algumas4
vezes ou pode ir para o infinito ou qualquer outra coisa!O resultado pode variar de compilador para compilador ou mesmo para versões diferentes do mesmo compilador.
C11: 1.3.24 comportamento indefinido:
fonte