我发现这段代码在优化时会导致 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
溢出,这是有符号整数的未定义行为:
$ 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)
| ~^~
如果你调用未定义的行为,编译器可以自由地做任何它喜欢的事情,包括让你的程序永不终止。
>Edit:有些人似乎认为UB应该只影响b
的值,而不是循环迭代。这不是根据标准(UB 实际上会导致任何事情发生),但这是一个合理的想法,所以让我们看看生成的程序集,看看为什么循环不会终止。
第一个没有-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
然后用-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
生成一个非常有趣的输出:
.LC0:
.string "%d "
main:
push rbx
mov ebx, 0
.L2:
add ebx, 333666999
mov esi, ebx
mov edi, OFFSET FLAT:.LC0
mov eax, 0
call printf
cmp ebx, -1959298303
jne .L2
mov eax, 9
pop rbx
ret
在这里,编译器优化了循环计数器i
,只是将b
的值与 7 次迭代后的最终值进行比较,利用了在此平台上根据 2 的补码发生的事实!厚脸皮,不是吗?:)
我正在使用g ++版本4.8.1。 Thomas的版本为10.2.0,在添加两个有符号整数时,它显然发出了有关"未定义行为"的警告。 但是,只是一个警告仍然会继续编译程序。 但是,在所有情况下,"未定义的行为"应该只涉及要添加的整数。 实际上,这些整数确实遵守了 2 的补码预期结果。 "未定义的行为"不应覆盖程序中的其他变量。 否则,根本无法信任可执行文件。 如果它不可信,就不应该被编译。 也许有一个更新版本的 gnu 编译器在优化时可以正常工作?