【问题标题】:GCC - Function inlining, LTO and optimizationsGCC - 函数内联、LTO 和优化
【发布时间】:2021-06-10 09:38:05
【问题描述】:

有这样的代码:

#include "kernel.h"
int main() {
    ...
    for (int t = 0; t < TSTEPS; ++t) {
       kernel(A,B,C);
    }
    ...
}

地点:

// kernel.h
void kernel(float *__restrict A, float *__restrict B, float *__restrict C);

// kernel.c
#include "kernel.h"

void kernel(float *__restrict A, float *__restrict B, float *__restrict C) {
    // some invariant code
    float tmp0 = B[42];
    float tmp1 = C[42];
    // some operations with tmpX, e.g.
    A[0] += tmp0 * tmp1;
} 

这个想法是独立编译kernel,因为我需要应用一组我对main 程序不感兴趣的优化。此外,我不想要任何其他类型的循环或程序间/程序内优化:我只想将 kernel 的编译结果完全内联到 @ 中对 kernel 的调用中987654327@。我尝试了很多不同的东西(用inline__attribute__((always_inline)) 等给出提示,但内联的唯一方法是:

gcc -c -O3 -flto kernel.c
gcc -O1 -flto kernel.o main.c

kernel 生成以下汇编代码:

kernel:
.LFB0:
    .cfi_startproc
    endbr64
    vxorps  %xmm1, %xmm1, %xmm1
    vcvtss2sd   168(%rsi), %xmm1, %xmm0
    vcvtss2sd   168(%rdx), %xmm1, %xmm2
    vcvtss2sd   (%rdi), %xmm1, %xmm1
    vfmadd132sd %xmm2, %xmm1, %xmm0
    vcvtsd2ss   %xmm0, %xmm0, %xmm0
    vmovss  %xmm0, (%rdi)
    ret
    .cfi_endproc

kernel 调用应该在main 中,生成的代码是:

...
    1092:   f3 0f 10 0d 76 0f 00    movss  0xf76(%rip),%xmm1        # 2010 <_IO_stdin_used+0x10>
    1099:   00 
    109a:   f3 0f 10 00             movss  (%rax),%xmm0
    109e:   b8 10 27 00 00          mov    $0x2710,%eax
    10a3:   0f 1f 44 00 00          nopl   0x0(%rax,%rax,1)
    10a8:   f3 0f 58 c1             addss  %xmm1,%xmm0
    10ac:   83 e8 01                sub    $0x1,%eax
    10af:   75 f7                   jne    10a8 <main+0x28>
    10b1:   48 8d 35 4c 0f 00 00    lea    0xf4c(%rip),%rsi        # 2004 <_IO_stdin_used+0x4>
    10b8:   bf 01 00 00 00          mov    $0x1,%edi
    10bd:   b8 01 00 00 00          mov    $0x1,%eax
    10c2:   f3 0f 5a c0             cvtss2sd %xmm0,%xmm0
...

当然,这很聪明,也可能是 LTO 的重点。尽管如此,我想摆脱任何类型的优化,但只内联那些独立编译的函数。除了手写之外,还有什么“正式”的方式吗?用-O0 编译main 根本不内联,甚至用-finline-functions 也不行。我也尝试过“拒绝”-O1 引入的所有优化标志,但我无法关闭链接时优化。这些结果是针对gcc 9.3.1gcc 10.2.0 获得的(在本次测试中它们之间存在细微差别)。


编辑 0:

另外两个细节:

  • 使用 ICC 使用类似的方法(IPO、内联标志等),我获得了类似的结果,即内联 + 优化。我还没有尝试过 Clang。
  • 上面的代码,将kernel内联到main上,只是基本消除了tmp0tmp1的负载,只是将其相乘的结果与a[0]相加;我知道这很聪明,但我不想要它,我想保留原始代码形式。

【问题讨论】:

  • 你想在这里解决什么真正的问题?基准测试?通常没有人想要没有针对调用站点/参数优化的更糟糕的 asm,并且没有办法让 GCC 做你所要求的。所以这似乎是一个 XY 问题,那么你真正想要什么?
  • 另外,cvtss2sd 168(%rdx), %xmm1 似乎与您的来源不匹配;您的函数 args 是 double* 但 GCC 正在发出 float->double 转换指令。看起来您的所有参数都是 float*,但在 double 临时变量上进行数学运算。 (然后您在源代码中修复了该问题,但没有更新 asm。)
  • @PeterCordes 是的,实际上,我基本上是在尝试对一些代码进行基准测试。不要误会我的意思,我知道没有人想要更差的性能,我只是想问是否有办法控制如何应用优化。是的,我忘了更新 C 代码,谢谢。

标签: assembly gcc compiler-optimization microbenchmark inlining


【解决方案1】:

内联通常发生在 IR(中间表示)或字节码级别。这意味着它是在源代码的抽象机器独立(在一定程度上)表示上执行的。然后是其他优化过程,这将利用内联代码。这是内联的主要好处之一。

在程序集级别内联,没有任何优化,更何况,由于寄存器分配和堆栈管理问题,保持函数体(程序集)完全保持原样会相当尴尬。它可能仍然稍微有益(由于删除了call;并且可能由于寄存器分配具有有关所用寄存器的附加信息,不太可能分配非易失性寄存器),但任何编译器都不太可能具有选择这样做。这将需要一个特殊的内联传递,该传递实际上会在后端发生(由于要求保持组装原样)。

您可以做什么:如果您真的希望 kernel 在汇编中完全以某种方式出现 - 使用汇编编写您的 kernel 函数(作为选项:内联汇编)。如果您的问题确实是其他问题(例如编译器优化计算或您不想要的负载) - 可能还有其他解决方案。

【讨论】:

【解决方案2】:

没有办法让 GCC 做你想做的事;这对实际程序的性能没有用处。 (仅可能用于基准测试。)

如果您希望内联版本的优化与独立版本大致相同,您需要阻止任何常量传播到 args,以及类似的东西。也许通过将它们存储到 volatile 本地变量并将它们传递给函数来隐藏编译器。

这并不能保证 相同 asm,但它应该足够相似以用于基准测试目的。当然,如果您想在另一个循环中执行此操作,那么 volatile 将意味着来自内存的额外负载。因此,您可能只需要像asm("" : "+g"(var)) 这样的内联汇编,以使编译器忘记它所知道的有关变量值的任何信息,并将该值具体化到编译器选择的寄存器或内存中。 (用clang,可能会选择"+r",因为它喜欢无缘无故的使用内存)

但这可能不会阻止编译器在内联后将循环不变的工作提升到循环之外。为了解决这个问题,您可能需要在函数本身内部进行类似的 DoNotOptimize 转义或 asm volatile 内容,以使其内联而不破坏基准。 (call/ret 真的很便宜,所以尝试不让它内联并不是不合理的,尽管这会在调用站点产生更多开销,并且可能需要保存/恢复一些寄存器。)

或者只是构建一个真实反映您的真实用例的测试用例,包括周围的代码乱序执行可能与此重叠。

【讨论】:

  • asm("" : "+g"(var)) 在我的情况下是一个非常好的“黑客”。我永远不会做到这一点。太棒了。
  • @horro:请注意,如果你想强制编译器实现 var(在寄存器中),即使它以后不使用,你会想要asm volatile。 (并且为了强制它实现值而不告诉编译器你的 asm 重写了它,asm volatile("" :: "r"(var)) 就像 DoNotOptimize 函数使用的一些定义一样。thisI don't understand the definition of DoNotOptimizeAway)。可移植的等效项是分配给 volatile int foo 并从中重新读取,但这会导致存储转发延迟。
  • 另外,"Escape" and "Clobber" equivalent in MSVC 有工作的 GNU C 版本,并将 Chandler Carruth(clang 开发人员)关于微基准测试的 CppCon 演讲与 perf 链接,其中演示了您如何使用它们。