GCC - 函数内联,LTO 和优化

3

拥有像这样的代码:

#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)的编译结果完全地内联到主程序(main)中对内核(kernel)的调用中。我尝试了很多不同的方法(使用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: 两个细节:
  • 使用类似的方法(IPO、内联标志等)使用ICC时,我获得类似的结果,即内联+优化。我还没有尝试过Clang。
  • 上述代码,将kernel内联到主函数中,只是基本上省略了对tmp0tmp1的加载,并将其乘积的结果添加到a[0]中;我知道这很聪明,但我不想要它,我想保留原始代码形式。

2
你在这里试图解决什么实际问题?基准测试?通常,没有人想要未针对调用现场/参数进行优化的更糟糕的 ASM,并且没有办法让 GCC 做你所要求的事情。因此,这似乎是一个 XY 问题,那么你真正想要什么呢? - Peter Cordes
另外,cvtss2sd 168(%rdx), %xmm1似乎不符合您的源代码。您的函数参数是double*,但GCC正在发出float-> double的转换指令。这看起来像您将所有参数设为float *,但在double临时变量上进行数学计算。(然后您在源代码中进行了修复,但没有更新汇编代码。) - Peter Cordes
@PeterCordes 是的,确实,我正在尝试对一些代码进行基准测试。请别误会,我知道没有人希望性能变差,我只是在问是否有办法控制如何应用优化。而且,我忘记更新 C 代码了,谢谢提醒。 - horro
2个回答

4
内联通常在IR(中间表示形式)或字节码级别上发生。这意味着它是在源代码的抽象机器无关(到一定程度)表示上执行的。然后进行其他优化传递,以利用内联代码。这是内联的主要好处之一。
在汇编语言级别进行内联,没有任何优化,甚至保留函数体(汇编语言)完全相同会很尴尬,因为需要考虑寄存器分配和栈管理问题。这可能仍然略有益处(由于删除call;)并且可能由于寄存器分配具有有关所使用的寄存器的附加信息而不太可能分配非易失性寄存器),但是高度不太可能有任何编译器以这种方式执行此操作。它需要一个特殊的内联传递,它会从字面上在后端发生(由于需要保持汇编语言原样)
你可以做的是:如果你真的希望kernel在汇编中精确地达到某种程度-请使用汇编编写kernel函数(作为选项:内联汇编)。如果您的问题确实是其他问题(例如编译器优化计算或负载,您不希望这样做)-可能有其他解决方案。

这是一个很好的解释。尽管如此,您有任何可用的来源可以深入研究这个话题吗? - horro
3
@horro https://gcc.gnu.org/wiki/ListOfCompilerBooks@horro https://gcc.gnu.org/wiki/编译器书籍列表 - rustyx

3

没有选项可以让GCC按照你的意愿去做;那对实际程序的性能并没有什么用处。(只有在基准测试中可能有效)

如果你想要内联版本的优化效果与独立版本大致相同,你需要阻止任何常量传递到args等参数中。也可以通过将它们存储到volatile本地变量中并将其传递给函数来隐藏这些信息以避免被编译器优化。

虽然这不能保证生成完全相同的汇编代码,但应该足够类似于进行基准测试。当然,如果您想在另一个循环中执行此操作,则使用volatile可能会增加从内存中加载的额外负担。因此,您可能只需要像使用asm ("" : "+g" (var))这样的内联汇编,让编译器忘记有关变量值的任何信息,并将该值实现为编译器选择的寄存器或内存。(使用clang时,可能选择"+r",因为它喜欢无缘由地使用内存)

尽管如此,这可能无法阻止编译器将循环不变工作提前到内联后的循环外部。为了打败这一点,您可能需要类似的DoNotOptimize逃逸或asm volatile等东西来让它内联而不影响基准测试。(调用/返回实际上非常便宜,所以尝试不让它内联是不太合理的,虽然这可能会在调用点产生更多开销,并且可能需要保存/恢复一些寄存器。)

或者构造一个测试用例来真实地反映您的实际用例,包括什么周围的代码可以与乱序执行重叠。


asm(“”:“+g”(var))在我的情况下是一个非常好的“hack”。我永远不会想到那个。太棒了。 - horro
1
@horro:请注意,如果你想强制编译器将变量(存储在寄存器中)实体化,即使它后面不使用,你需要使用asm volatile。(如果要强制它以值的形式实例化而告诉编译器你的汇编重写了它,则使用asm volatile("" :: "r"(var)),就像某些DoNotOptimize函数的定义一样。这里或者I don't understand the definition of DoNotOptimizeAway)。便携式等效方法是将其分配给volatile int foo并从那里重新读取,但这会导致存储前传延迟。 - Peter Cordes
此外,"Escape"和"Clobber"在MSVC中的等效版本也有可用的GNU C版本,并链接了一篇由Chandler Carruth(clang开发人员)关于使用perf进行微基准测试的好文章,其中演示了如何使用它们。 - Peter Cordes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接