Por que o excesso de número inteiro no x86 com o GCC causa um loop infinito?

129

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
Mysticial
fonte
10
Isso seria muito mais responsável se você incluísse o código de montagem gerado a partir de gcc -S.
Greg Hewgill
A montagem é surpreendentemente longa. Ainda devo editá-lo?
Mysticial
Apenas as partes relevantes para o seu loop, por favor.
Greg Hewgill
12
-1. você diz que esse é um comportamento indefinido estritamente falando e pergunta se esse é um comportamento indefinido. então essa não é uma pergunta real para mim.
Johannes Schaub - litb
8
@ JohannesSchaub-litb Obrigado por comentar. Provavelmente palavras ruins da minha parte. Vou tentar o meu melhor para esclarecer de forma a ganhar seu voto não realizado (e editarei a pergunta de acordo). Basicamente, eu sei que é UB. Mas também sei que o GCC no x86 usa instruções inteiras x86 - que envolvem o estouro. Portanto, eu esperava que ele fosse encerrado, apesar de ser UB. No entanto, não aconteceu e isso me confundiu. Daí a questão.
Mysticial

Respostas:

178

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 -fwrapvpara g++ou gccao compilar; isso fornece uma semântica de estouro bem definida (com dois complementos), mas pode prejudicar o desempenho.

bdonlan
fonte
24
Oh uau. Eu não estava ciente -fwrapv. Obrigado por apontar isso.
Mysticial
1
Existe uma opção de aviso que tenta perceber loops infinitos acidentais?
Jeff Burdges
5
Achei -Wunsafe-loop-otimizações mencionados aqui: stackoverflow.com/questions/2982507/...
Jeff Burdges
1
-1 "Sim, em CPUs x86, os números inteiros geralmente são do jeito que você espera." isto é errado. mas é sutil. Pelo que me lembro, é possível fazê-los ficar presos, mas não é disso que estamos falando aqui , e nunca vi isso acontecer. além disso, e desconsiderando as operações x86 bcd (representação não permitida em C ++), as operações inteiras x86 sempre envolvem, porque são o complemento de duas. você está confundindo a otimização defeituosa do g ++ (ou extremamente impraticável e sem sentido) com uma propriedade de operações inteiras x86.
Saúde e hth. #
5
@ Cheersandhth.-Alf, por 'on CPUs x86' quero dizer 'quando você estiver desenvolvendo para CPUs x86 usando um compilador C'. Eu realmente preciso soletrar? Obviamente, toda a minha conversa sobre compiladores e GCC é irrelevante se você estiver desenvolvendo em assembler; nesse caso, a semântica para excesso de número inteiro é realmente muito bem definida.
bdonlan
18

É 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 -O2opçã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.

Dennis
fonte
É meio ruim que um compilador opte por um loop infinito de todas as coisas por comportamento indefinido.
Inverse
27
@ Inverso: eu discordo. Se você codificou algo com comportamento indefinido, ore por um loop infinito. Torna mais fácil para detectar ...
Dennis
Quero dizer, se o compilador está procurando ativamente por UB, por que não inserir uma exceção em vez de tentar otimizar o código quebrado?
Inverse
15
@ Inverso: O compilador não está procurando ativamente um comportamento indefinido , ele pressupõe que isso não ocorra. Isso permite que o compilador otimize o código. Por exemplo, em vez de computar for (j = i; j < i + 10; ++j) ++k;, ele apenas será definido k = 10, pois isso sempre será verdadeiro se não ocorrer nenhum estouro assinado.
Dennis
@ Inverse O compilador não "optou" por nada. Você escreveu o loop no seu código. O compilador não o inventou.
Lightness Races em órbita
13

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 ++.

Mankarse
fonte
5
i += i;

// o estouro é indefinido.

Com -fwrapv, está correto. -fwrapv

lostyzd
fonte
3

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.

vonbrand
fonte
1
Sim, eu sei muito bem o que é um comportamento indefinido. Mas quando você sabe como certos aspectos da linguagem são implementados para um ambiente específico, pode esperar ver certos tipos de UB e não outros. Eu sei que o GCC implementa aritmética inteira como aritmética inteira x86 - que envolve o estouro. Então eu assumi o comportamento como tal. O que eu não esperava era que o GCC fizesse outra coisa, como bdonlan respondeu.
Mysticial
7
Errado. O que acontece é que o GCC tem permissão para assumir que você não invocará um comportamento indefinido; portanto, ele apenas emite código como se não pudesse acontecer. Se não acontecer, as instruções para fazer o que você pedir com nenhum comportamento indefinido se executado, eo resultado é o que o CPU faz. Ou seja, em x86 é que faz coisas x86. Se for outro processador, poderia fazer algo totalmente diferente. Ou o compilador pode ser inteligente o suficiente para descobrir que você está chamando um comportamento indefinido e iniciar o nethack (sim, algumas versões antigas do gcc fizeram exatamente isso).
vonbrand
4
Eu acredito que você interpretou mal o meu comentário. Eu disse: "O que eu não esperava" - e foi por isso que fiz a pergunta em primeiro lugar. Eu não esperava que o GCC fizesse truques.
Mysticial
1

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.

supercat
fonte