以下代码进入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整数指令来实现整数运算,这些指令在溢出时进行包装。
因此,尽管它是未定义的行为,我还是希望它在溢出时进行包装。但事实显然并非如此。那么我错过了什么?
我使用:编译了这个
~/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,因为我认为这个错误会在C中重现。(我还没有验证它。(
编辑:
以下是循环的组装:(如果我正确识别的话(
.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上,整数通常按照您期望的方式包装。这是其中一个例外。编译器假设您不会导致未定义的行为,并优化掉循环测试。如果您真的想要环绕,那么在编译时将-fwrapv
传递给g++
或gcc
;这为您提供了定义良好的(twos补码(溢出语义,但可能会影响性能。
很简单:未定义的行为——尤其是在启用优化(-O2
(的情况下——意味着任何事情都可能发生。
在没有-O2
开关的情况下,您的代码的行为与(您(预期的一样。
顺便说一句,它在icl和tcc上运行得很好,但你不能依赖这样的东西。。。
据此,gcc优化实际上利用了有符号整数溢出。这意味着"bug"是设计出来的。
这里需要注意的是,C++程序是为C++抽象机编写的(通常通过硬件指令模拟(。您为x86编译的事实是完全与它具有未定义行为的事实无关。
编译器可以自由地使用未定义行为的存在来改进其优化(如本例所示,通过从循环中删除条件(。除了要求机器代码在执行时产生C++抽象机器所需的结果之外,C++级构造和x86级机器代码构造之间没有任何保证,甚至没有任何有用的映射。
i += i;
//溢出未定义。
使用-fwrav是正确的-fwrapv
请大家注意,未定义的行为正是如此,undefined。这意味着任何事情都有可能发生。在实践中(在这种情况下(,编译器可以自由地假设它不会被调用,如果这可以使代码更快/更小,则可以随心所欲。不应该运行的代码会发生什么,这是任何人的猜测。它将取决于周围的代码(取决于此,编译器很可能会生成不同的代码(、使用的变量/常量、编译器标志。。。哦,编译器可以得到更新并以不同的方式编写相同的代码,或者你可以得到另一个对代码生成有不同看法的编译器。或者只是换一台机器,即使是同一体系结构中的另一个模型也很可能有自己的未定义行为(查找未定义的操作码,一些有进取心的程序员发现,在一些早期的机器上,有时确实做了有用的事…(。有些领域是由实现定义的,在那里你应该能够指望编译器的行为一致。
即使编译器指定整数溢出必须被视为未定义行为的"非关键"形式(如附录L中所定义(,在没有更具体行为的特定平台承诺的情况下,整数溢出的结果至少应被视为"部分不确定值"。在这样的规则下,加1073741824+10073741824可以任意地被视为产生2147483648或-2147483648或与2147483648mod 4294967296全等的任何其他值,并且通过加法获得的值可以任意地视为与0 mod 429496729全等的任意值。
允许溢出产生"部分不确定值"的规则将得到充分的定义,以遵守附件L的文字和精神,但不会阻止编译器做出与溢出是不受约束的未定义行为时相同的通常有用的推断。它可以防止编译器进行一些虚假的"优化",在许多情况下,这些"优化"的主要作用是要求程序员在代码中添加额外的混乱,而程序员的唯一目的是防止这种"优化";这是否是一件好事取决于一个人的观点。