Por que o estouro de inteiro no x86 com o GCC causa um loop infinito?

O código a seguir entra em um loop infinito no GCC:

#include  using namespace std; int main(){ int i = 0x10000000; int c = 0; do{ c++; i += i; cout << i < 0); cout << c << endl; return 0; } 

Então, eis o seguinte: Estouro de inteiro assinado é um comportamento tecnicamente indefinido. Mas o GCC em x86 implementa a aritmética de inteiros usando instruções inteiras x86 – as quais envolvem overflow.

Portanto, eu teria esperado que ele envolvesse o estouro – apesar do fato de que é um comportamento indefinido. Mas isso claramente não é o caso. Então … o que eu perdi?

Eu compilei isso usando:

 ~/Desktop$ g++ main.cpp -O2 

Saída do GCC:

 ~/Desktop$ ./a.out 536870912 1073741824 -2147483648 0 0 0 ... (infinite loop) 

Com as otimizações desativadas, não há loop infinito e a saída está correta. Visual Studio também corretamente compila isso e dá o seguinte resultado:

Saída Correta:

 ~/Desktop$ g++ main.cpp ~/Desktop$ ./a.out 536870912 1073741824 -2147483648 3 

Aqui estão algumas outras variações:

 i *= 2; // Also fails and goes into infinite loop. i <<= 1; // This seems okay. It does not enter infinite loop. 

Veja todas as informações relevantes sobre a versão:

 ~/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$ 

Então a questão é: isso é um bug no GCC? Ou eu entendi mal algo sobre como o GCC lida com a aritmética de inteiros?

* Estou etiquetando este C também, porque eu suponho que esse bug irá se reproduzir em C. (Eu ainda não o verifiquei).

EDITAR:

Aqui está a assembly do loop: (se eu reconhecê-lo corretamente)

 .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 

Quando a norma diz que é um comportamento indefinido, significa isso . Nada pode acontecer. “Qualquer coisa” inclui “geralmente números inteiros, mas de vez em quando acontecem coisas estranhas”.

Sim, em CPUs x86, os números inteiros geralmente são agrupados da maneira esperada. Essa é uma dessas exceções. O compilador assume que você não causará um comportamento indefinido e otimiza o teste de loop. Se você realmente quer -fwrapv -lo, passe -fwrapv para g++ ou gcc ao compilar; isso oferece uma semântica de estouro bem definida (complemento de dois), mas pode prejudicar o desempenho.

É simples: comportamento indefinido – especialmente com a otimização ( -O2 ) ativada – significa que tudo pode acontecer.

Seu código se comporta como (você) esperava sem a opção -O2 .

Funciona muito bem com o icl e o tcc, mas você não pode confiar em coisas assim …

De acordo com isso , a otimização do gcc realmente explora o estouro de inteiro assinado. Isso significaria que o “bug” é por design.

O importante a notar aqui é que os programas C ++ são escritos para a máquina abstrata C ++ (que geralmente é emulada através de instruções de hardware). O fato de você estar compilando para x86 é totalmente irrelevante para o fato de que isso tem um comportamento indefinido.

O compilador está livre para usar a existência de comportamento indefinido para melhorar suas otimizações (removendo um condicional de um loop, como neste exemplo). Não há mapeamento garantido, ou mesmo útil, entre construções de nível C ++ e construções de código de máquina no nível x86, além da exigência de que o código da máquina, quando executado, produza o resultado exigido pela máquina abstrata C ++.

 i += i; 

// o estouro é indefinido.

Com -fwrapv está correto. -fwrapv

Por favor pessoas, comportamento indefinido é exatamente isso, indefinido . Isso significa que tudo pode acontecer. Na prática (como neste caso), o compilador está livre para assumir que ele não será chamado, e fazer o que quiser se isso tornar o código mais rápido / menor. O que acontece com o código que não deve ser executado é um palpite de ninguém. Dependerá do código circundante (dependendo disso, o compilador poderia gerar código diferente), variables ​​/ constantes usadas, sinalizadores de compilador, … Ah, e o compilador poderia ser atualizado e escrever o mesmo código de forma diferente, ou você poderia obter outro compilador com uma visão diferente sobre a geração de código. Ou simplesmente pegar uma máquina diferente, até mesmo outro modelo na mesma linha de arquitetura poderia ter seu próprio comportamento indefinido (procure por opcodes indefinidos, alguns programadores empreendedores descobriram que em algumas daquelas primeiras máquinas às vezes faziam coisas úteis …) . Não “o compilador dá um comportamento definido no comportamento indefinido”. Existem áreas que são definidas pela implementação e você deve poder contar com o comportamento do compilador de forma consistente.

Mesmo que um compilador especifique que o estouro de inteiro deve ser considerado uma forma “não crítica” de Comportamento Indefinido (conforme definido no Anexo L), o resultado de um estouro de inteiro deve, na falta de uma plataforma específica de comportamento mais específico, ser no mínimo considerado como um “valor parcialmente indeterminado”. Sob tais regras, a adição de 1073741824 + 1073741824 poderia arbitrariamente ser considerada como rendendo 2147483648 ou -2147483648 ou qualquer outro valor que fosse congruente com 2147483648 mod 4294967296, e valores obtidos por adições poderiam arbitrariamente ser considerados como qualquer valor que fosse congruente com 0 mod 4294967296.

As regras que permitem que o estouro produza “valores parcialmente indeterminados” seriam suficientemente bem definidas para cumprir a letra e o espírito do Anexo L, mas não impediriam que um compilador fizesse as mesmas inferências geralmente úteis que seriam justificadas se os extravasamentos fossem irrestritos Comportamento indefinido. Isso impediria que um compilador fizesse algumas “otimizações” falsas, cujo efeito primário, em muitos casos, é exigir que os programadores adicionem mais confusão ao código cujo único propósito é evitar tais “otimizações”; se isso seria uma coisa boa ou não depende do ponto de vista de alguém.