问题描述
我发现该代码在优化时会导致gnu C ++编译器出现惊人的错误。
#include <stdio.h>
int main()
{
int a = 333666999,b = 0;
for (short i = 0; i<7; ++i)
{
b += a;
printf("%d ",b);
}
return 9;
}
要使用g++ -Os fail.cpp
进行编译,可执行文件不会打印七个数字,它会一直持续打印和打印。我正在使用-
-rwxr-xr-x 4 root root 700388 Jun 3 2013 /usr/bin/g++
是否有更高版本的更正版本?
解决方法
编译器非常非常很少出错。在这种情况下,b
正在溢出,which is undefined behaviour for signed integers:
$ g++ --version
g++ (GCC) 10.2.0
...
$ g++ -Os -otest test.cpp
test.cpp: In function ‘int main()’:
test.cpp:8:11: warning: iteration 6 invokes undefined behavior [-Waggressive-loop-optimizations]
8 | b += a;
| ~~^~~~
test.cpp:6:24: note: within this loop
6 | for (short i = 0; i<7; ++i)
| ~^~
如果您调用未定义的行为,则编译器可以自由执行其喜欢的任何事情,包括使程序永不终止。
编辑:有些人似乎认为UB应该只影响b
的值,而不影响循环迭代。这不符合标准(UB可能导致任何事情发生),但是这是一个合理的想法,所以让我们看一下生成的程序集,看看循环为什么不会终止。
第一个without -Os
:
.LC0:
.string "%d "
main:
push rbp
mov rbp,rsp
sub rsp,16
mov DWORD PTR [rbp-12],333666999
mov DWORD PTR [rbp-4],0
mov WORD PTR [rbp-6],0
.L3:
cmp WORD PTR [rbp-6],6 # Compare i to 6
jg .L2 # If greater,jump to end
mov eax,DWORD PTR [rbp-12]
add DWORD PTR [rbp-4],eax
mov eax,DWORD PTR [rbp-4]
mov esi,eax
mov edi,OFFSET FLAT:.LC0
mov eax,0
call printf
movzx eax,WORD PTR [rbp-6]
add eax,1
mov WORD PTR [rbp-6],ax
jmp .L3
.L2:
mov eax,9
leave
ret
然后with -Os
:
.LC0:
.string "%d "
main:
push rbx
xor ebx,ebx
.L2:
add ebx,333666999
mov edi,OFFSET FLAT:.LC0
xor eax,eax
mov esi,ebx
call printf
jmp .L2
比较和跳转指令完全消失了。具有讽刺意味的是,编译器完全按照您的要求进行了操作:优化大小,因此在遵循C ++标准的同时尽可能多地删除指令。 -O3
和-O2
在这里生成与-Os
完全相同的代码。
-O1
generates的输出非常有趣:
.LC0:
.string "%d "
main:
push rbx
mov ebx,0
.L2:
add ebx,333666999
mov esi,ebx
mov edi,0
call printf
cmp ebx,-1959298303
jne .L2
mov eax,9
pop rbx
ret
在这里,编译器优化了循环计数器i
并仅将b
的值与7次迭代后的最终值进行比较,利用有符号溢出根据两位补码发生的事实在这个平台上!厚脸皮,不是吗? :)
我正在使用g ++版本4.8.1。 Thomas的版本为10.2.0,当添加两个有符号整数时,显然会发出有关“未定义行为”的警告。但是,仅警告仍会继续并编译程序。但是,在所有情况下,“未定义行为”都只应与要添加的整数有关。实际上,这些整数实际上遵守2的补码期望结果。 “未定义的行为”不应覆盖程序中的其他变量。否则,该可执行文件将根本无法被信任。如果无法信任它,则不应对其进行编译。也许还有gnu编译器的更高版本可以在优化时正常工作?