为什么整数在x86上与GCC溢出导致无限循环?

以下代码在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; } 

所以这里的交易:签名整数溢出技术上是未定义的行为。 但是x86上的GCC使用x86整数指令实现整数运算 – 在溢出时换行。

因此,我会期望它会溢出 – 尽pipe这是不确定的行为。 但事实并非如此。 那么我错过了什么?

我编译这个使用:

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

GCC输出:

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

在禁用优化的情况下,没有无限循环,输出是正确的。 Visual Studio也正确编译这个,并给出以下结果:

正确的输出:

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

这里有一些其他的变化:

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

以下是所有相关的版本信息:

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

所以问题是:这是GCC中的错误吗? 还是我误解了GCC如何处理整数算术?

*我也标记了这个C,因为我认为这个bug会在C中重现(我还没有validation过)。

编辑:

这里是循环的程序集:(如果我认识到了这一点)

 .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 

当标准说这是不确定的行为, 这意味着它 。 任何事情都可能发生。 “任何事物”都包括“通常是整数,但偶尔会发生奇怪的事情”。

是的,在x86 CPU上,整数通常以您期望的方式包装。 这是这些例外之一。 编译器假定你不会导致未定义的行为,并优化掉循环testing。 如果你真的想环绕,在编译时把-fwrapv传给g++或者gcc ; 这给你定义(二补)溢出语义,但可能会伤害性能。

很简单:未定义的行为 – 特别是优化( -O2 )打开 – 意味着任何事情都可能发生。

你的代码performance为(你)没有-O2开关。

顺便说一句,icl和tcc是很好用的,但是你不能依赖这样的东西…

据此,gcc优化实际上是利用有符号的整数溢出。 这意味着这个“错误”是devise的。

这里要注意的重要一点是C ++程序是为C ++抽象机器编写的(通常通过硬件指令来模拟)。 你正在为x86编译的事实与这个没有定义的行为是完全不相关的。

编译器可以自由使用未定义行为的存在来改进其优化(通过从循环中删除条件,如本例中所示)。 除了要求机器代码在执行时产生C ++抽象机器所要求的结果之外,在C ++级别构造和x86级别机器代码构造之间没有保证甚至是有用的映射。

 i += i; 

//溢出是未定义的。

用-fwrapv这是正确的。 -fwrapv

请人, 未定义的行为就是这样, 没有定义 。 这意味着什么都可能发生。 在实践中(如在这种情况下),编译器可以自由地假定它不会被调用,如果可以使代码更快/更小,可以随心所欲。 不应该运行的代码发生什么是任何人的猜测。 它将取决于周围的代码(取决于这一点,编译器可以生成不同的代码),使用的variables/常量,编译器标志,…哦,编译器可以得到更新和编写相同的代码,或者你可以在代码生成中获得另一个具有不同视图的编译器。 或者只是得到一个不同的机器,甚至在同一架构线的另一个模型可能会有自己的未定义的行为(查找未定义的操作码,一些有进取的程序员发现,在一些早期的机器有时确实有用的东西…) 。 没有 “编译器对未定义的行为给出明确的行为”。 有一些领域是实现定义的,在那里你应该能够指望编译器的行为一致。

即使编译器指定整数溢出必须被认为是“未定义的行为”的非关键forms(如附录L中所定义的),整数溢出的结果应该是,在没有特定平台承诺的更具体的行为的情况下,至less被视为“部分不确定的价值”。 在这样的规则下,1073741824 + 1073741824可以任意地视为产生2147483648或-2147483648或与2147483648 mod 4294967296一致的任何其他值,并且通过相加所获得的值可以被任意地视为与mod 4294967296相等的任何值。

允许溢出产生“部分不确定的值”的规则将被充分定义为遵守附录L的文字和精神,但是并不妨碍编译器做出相同的普遍有用的推论,如果溢出不受限制未定义的行为。 它会阻止编译器做一些虚假的“优化”,其主要作用在很多情况下是要求程序员给代码添加额外的混乱,其唯一目的是防止这种“优化”; 这是否是一件好事取决于一个人的观点。