为什么使用-Os进行编译会使此函数更大?

时间:2018-11-17 17:26:27

标签: c gcc compiler-optimization

考虑此功能:

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

1 个答案:

答案 0 :(得分:2)

与使用-Os-O1-O2选项生成的代码相比,-O3(“大小优化”)的代码更紧凑。为了速度”),确实没有这样的保证,正如@Robert Harvey所说。

优化编译是一个非常复杂而微妙的过程。 它由数十个不同的优化阶段组成,这些阶段通常是按顺序执行的:每个优化阶段都以程序树表示形式进行工作,并为下一个阶段做好准备。在优化过程中,在一个阶段中做出的每个决定都可能对未来的优化产生影响,并且传球可能会以非平凡的方式相互作用,这可能很难预测。编译器采用不同的启发式方法来生成最佳代码,但在某些情况下,如这种情况,这些启发式方法就不够用了。

在此示例中,似乎一切都按预期开始--Os产生了更紧凑的中间代码,但此后发生了变化。由GCC执行的第一阶段之一是 Expand 阶段,该阶段将称为GIMPLE的GCC高级树表示形式转换为较低级别的RTL表示形式。它产生类似于以下内容的RTL代码:

O3:

  1. tmp1 <-x
  2. tmp2 <-tmp1 << 2
  3. tmp3 <-tmp2 + x
  4. retval <-tmp3 + 6

操作系统:

  1. tmp <-x * 5
  2. tmp2 <-tmp + 6
  3. retval <-tmp2

到目前为止,一切都很好--Os获胜。但是之后,在大约15个阶段之后,执行了 Combine 阶段,该阶段尝试将一系列指令组合为一条指令。对于-O3代码, Combine 可以非常巧妙地将其折叠到最终输出中的leaq指令,但是对于-Os Combine < / em>的作用不大,也无法进一步折叠代码。从那时起,通过进一步的优化,代码不会发生太大变化。

要回答确切的问题-为什么GCC会这样做(生成在-O3的Expand中执行的代码,以及为什么 Combine 在{{1 }}),必须检查一下GCC代码,找出哪些GCC参数是有影响力的参数,以及前面的优化阶段所做出的决定。

但是,事实是,尽管在本示例中执行的GCC不足,但对于大多数其他示例而言,它可能是最佳选择。这是一个微妙的权衡问题-对于编译器编写者而言并非易事!

这可能无法完全回答问题,但希望它能提供一些有用的背景。如果您有兴趣在每个优化阶段检查GCC的输出,则可以添加-Os编译标志(将为每个阶段生成带注释的树转储)和-da标志,以添加树对生成的程序集输出的注释以及-dP