GCC - 函数内联、LTO 和优化

GCC - Function inlining, LTO and optimizations

有这样的代码:

#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程序不感兴趣的优化。此外,我不需要任何其他类型的循环,也不需要 inter/intra-procedural 优化:我只想将 完全 的编译结果内联到 kernel 的调用中kernelmain 中。我尝试了很多不同的东西(用 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    [=14=]x2710,%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    [=14=]x1,%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    [=14=]x1,%edi
    10bd:   b8 01 00 00 00          mov    [=14=]x1,%eax
    10c2:   f3 0f 5a c0             cvtss2sd %xmm0,%xmm0
...

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


编辑 0:

另外两个细节:

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

在汇编级别内联,没有任何优化,甚至更多,保持函数体(汇编)完全由于寄存器分配和堆栈的原因会很尴尬管理问题。它可能仍然有点好处(由于删除了 call; 并且可能由于寄存器分配具有有关所用寄存器的附加信息,不太可能分配非易失性寄存器),但任何编译器都不太可能可以选择这样做。这将需要一个特殊的内联通道,该通道实际上会在后端发生(由于要求保持汇编原样)。

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

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

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

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

不过,这可能不会阻止编译器在内联后将循环不变的工作提升到循环之外。为了打败它,您可能需要类似的 DoNotOptimize 转义符或函数本身内部的 asm volatile 内容,以让它在不打败基准的情况下内联。 (call/ret 确实很便宜,所以尝试不让它内联并不是没有道理的,尽管这会在调用点产生更多的开销,并且它可能需要 save/restore 一些寄存器。)

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