O código a seguir entra em um loop infinito no 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;
}
Então, eis o negócio: o excesso de número inteiro assinado é um comportamento tecnicamente indefinido. Mas o GCC no x86 implementa aritmética de número inteiro usando instruções de número inteiro x86 - que envolvem o estouro.
Portanto, eu esperaria que ele fosse envolto em excesso - apesar do fato de ser um comportamento indefinido. Mas esse claramente não é o caso. Então ... o que eu perdi?
Eu compilei isso usando:
~/Desktop$ g++ main.cpp -O2
Saída 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. O Visual Studio também compila isso corretamente e fornece 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.
Aqui estão todas as informações relevantes da 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$
Portanto, a pergunta é: isso é um bug no GCC? Ou entendi algo errado sobre como o GCC lida com a aritmética inteira?
* Estou marcando esse C também, porque presumo que esse bug seja reproduzido em C. (ainda não o verifiquei).
EDITAR:
Aqui está a montagem do loop: (se eu o reconheci 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
gcc -S
.Respostas:
Quando o padrão diz que é um comportamento indefinido, significa isso . Nada pode acontecer. "Qualquer coisa" inclui "geralmente números inteiros se espalham, mas às vezes acontecem coisas estranhas".
Sim, em CPUs x86, os números inteiros geralmente envolvem o que você espera. Essa é uma dessas exceções. O compilador assume que você não causará comportamento indefinido e otimiza o teste de loop. Se você realmente deseja envolvê-lo, passe
-fwrapv
parag++
ougcc
ao compilar; isso fornece uma semântica de estouro bem definida (com dois complementos), mas pode prejudicar o desempenho.fonte
-fwrapv
. Obrigado por apontar isso.É 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
-O2
opção.A propósito, funciona muito bem com icl e tcc, mas você não pode confiar em coisas assim ...
De acordo com isso , a otimização do gcc realmente explora o excesso de número inteiro assinado. Isso significa que o "bug" é por design.
fonte
for (j = i; j < i + 10; ++j) ++k;
, ele apenas será definidok = 10
, pois isso sempre será verdadeiro se não ocorrer nenhum estouro assinado.O importante a ser observado 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 o x86 é totalmente irrelevante para o fato de isso ter um comportamento indefinido.
O compilador é livre para usar a existência de comportamento indefinido para melhorar suas otimizações (removendo uma condicional de um loop, como neste exemplo). Não há mapeamento garantido, ou mesmo útil, entre construções no nível C ++ e construções no código da máquina no nível x86, exceto pelo requisito de que o código da máquina, quando executado, produza o resultado exigido pela máquina abstrata do C ++.
fonte
// o estouro é indefinido.
Com -fwrapv, está correto. -fwrapv
fonte
Por favor, pessoal, comportamento indefinido é exatamente isso, indefinido . Isso significa que tudo pode acontecer. Na prática (como neste caso), o compilador é livre para assumir que nãoser chamado e fazer o que bem entender se isso puder tornar o código mais rápido / menor. O que acontece com o código que não deve ser executado é uma incógnita. Depende do código ao redor (dependendo disso, o compilador pode gerar código diferente), variáveis / constantes usadas, sinalizadores do compilador, ... Ah, e o compilador pode ser atualizado e escrever o mesmo código de forma diferente, ou você pode obtenha outro compilador com uma visão diferente sobre a geração de código. Ou apenas obtenha uma máquina diferente, mesmo outro modelo na mesma linha de arquitetura poderia muito bem ter seu próprio comportamento indefinido (procure códigos de operação indefinidos, alguns programadores empreendedores descobriram que em algumas dessas máquinas antigas algumas vezes faziam coisas úteis ...) . Há sim existe"o compilador fornece um comportamento definido em comportamento indefinido". Existem áreas definidas pela implementação e você deve poder contar com o compilador se comportando de maneira consistente.
fonte
Mesmo que um compilador especifique que o excesso de número inteiro deve ser considerado uma forma "não crítica" de comportamento indefinido (conforme definido no Anexo L), o resultado de um excesso de número inteiro deve, na ausência de uma promessa específica da plataforma de comportamento mais específico, na ausência de uma promessa específica de plataforma. no mínimo considerado como "valor parcialmente indeterminado". De acordo com essas regras, a adição de 1073741824 + 1073741824 poderia arbitrariamente ser considerada como produzindo 2147483648 ou -2147483648 ou qualquer outro valor que fosse congruente a 2147483648 mod 4294967296, e os valores obtidos por adições poderiam ser arbitrariamente 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 respeitar 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 estouros fossem irrestritos. Comportamento indefinido. Impediria que um compilador fizesse algumas "otimizações" falsas, cujo efeito principal, em muitos casos, é exigir que os programadores adicionem mais confusão ao código cujo único objetivo é impedir essas "otimizações"; se isso seria bom ou não, depende do ponto de vista de alguém.
fonte