考虑此功能:
long foo(long x) {
return 5*x + 6;
}
当我使用带有-O3
(或-O2
或-O1
)的x86-64 gcc 8.2进行编译时,它会编译为:
foo:
leaq 6(%rdi,%rdi,4), %rax # 5 bytes: 48 8d 44 bf 06
ret # 1 byte: c3
当我改用-Os
时,它将编译为:
foo:
leaq (%rdi,%rdi,4), %rax # 4 bytes: 48 8d 04 bf
addq $6, %rax # 4 bytes: 48 83 c0 06
ret # 1 byte: c3
后者长3个字节。 -Os
是否应该产生尽可能小的代码,即使更大的代码会更有效?为什么在这里似乎发生了相反的事情?
Godbolt:https://godbolt.org/z/jzNquk
答案 0 :(得分:2)
与使用-Os
,-O1
和-O2
选项生成的代码相比,-O3
(“大小优化”)的代码更紧凑。为了速度”),确实没有这样的保证,正如@Robert Harvey所说。
优化编译是一个非常复杂而微妙的过程。 它由数十个不同的优化阶段组成,这些阶段通常是按顺序执行的:每个优化阶段都以程序树表示形式进行工作,并为下一个阶段做好准备。在优化过程中,在一个阶段中做出的每个决定都可能对未来的优化产生影响,并且传球可能会以非平凡的方式相互作用,这可能很难预测。编译器采用不同的启发式方法来生成最佳代码,但在某些情况下,如这种情况,这些启发式方法就不够用了。
在此示例中,似乎一切都按预期开始--Os
产生了更紧凑的中间代码,但此后发生了变化。由GCC执行的第一阶段之一是 Expand 阶段,该阶段将称为GIMPLE的GCC高级树表示形式转换为较低级别的RTL表示形式。它产生类似于以下内容的RTL代码:
O3:
tmp1
<-x
tmp2
<-tmp1 << 2
tmp3
<-tmp2 + x
retval
<-tmp3 + 6
操作系统:
tmp
<-x * 5
tmp2
<-tmp + 6
retval
<-tmp2
到目前为止,一切都很好--Os
获胜。但是之后,在大约15个阶段之后,执行了 Combine 阶段,该阶段尝试将一系列指令组合为一条指令。对于-O3
代码, Combine 可以非常巧妙地将其折叠到最终输出中的leaq
指令,但是对于-Os
, Combine < / em>的作用不大,也无法进一步折叠代码。从那时起,通过进一步的优化,代码不会发生太大变化。
要回答确切的问题-为什么GCC会这样做(生成在-O3
的Expand中执行的代码,以及为什么 Combine 在{{1 }}),必须检查一下GCC代码,找出哪些GCC参数是有影响力的参数,以及前面的优化阶段所做出的决定。
但是,事实是,尽管在本示例中执行的GCC不足,但对于大多数其他示例而言,它可能是最佳选择。这是一个微妙的权衡问题-对于编译器编写者而言并非易事!
这可能无法完全回答问题,但希望它能提供一些有用的背景。如果您有兴趣在每个优化阶段检查GCC的输出,则可以添加-Os
编译标志(将为每个阶段生成带注释的树转储)和-da
标志,以添加树对生成的程序集输出的注释以及-dP
。