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
的调用中kernel
在 main
中。我尝试了很多不同的东西(用 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.1
和 gcc 10.2.0
获得的(对于此测试,它们之间存在细微差别)。
编辑 0:
另外两个细节:
- 通过使用类似方法(IPO、内联标志等)的 ICC,我获得了类似的结果,即内联 + 优化。我还没有尝试过 Clang。
- 上面的代码,将
kernel
内联到 main 上,只是基本上避免了 tmp0
和 tmp1
的负载,只是将其乘法结果添加到 a[0]
;我知道这很聪明,但我不想要它,我想保留原始代码形式。
内联通常发生在 IR(中间表示)或字节码级别。这意味着它是在源代码的抽象机器独立(在一定程度上)表示上执行的。然后是其他优化过程,这将利用内联代码。这是内联的主要好处之一。
在汇编级别内联,没有任何优化,甚至更多,保持函数体(汇编)完全由于寄存器分配和堆栈的原因会很尴尬管理问题。它可能仍然有点好处(由于删除了 call
; 并且可能由于寄存器分配具有有关所用寄存器的附加信息,不太可能分配非易失性寄存器),但任何编译器都不太可能可以选择这样做。这将需要一个特殊的内联通道,该通道实际上会在后端发生(由于要求保持汇编原样)。
你可以做什么:如果你真的希望 kernel
在汇编中完全是某种方式 - 使用汇编编写你的 kernel
函数(作为一个选项:内联汇编)。如果您的问题确实是其他问题(例如编译器优化计算或您不希望的负载)- 可能还有其他解决方案。
没有让 GCC 做你想做的事情的选项;这对实际程序的性能没有用。 (仅可能用于基准测试。)
如果您希望内联版本的优化与独立版本大致相同,则需要阻止对 args 的任何常量传播,诸如此类。也许通过将它们存储到 volatile
局部变量并将它们传递给函数来隐藏编译器。
这并不能保证 相同 asm,但它应该足够相似以用于基准测试。当然,如果您想在另一个循环中执行此操作,volatile 将意味着从内存中额外加载。因此,您可能只希望像 asm("" : "+g"(var))
这样的内联 asm 使编译器忘记它所知道的有关变量值的任何信息,并将该值具体化在编译器选择的寄存器或内存中。 (有clang的话,大概会选"+r"
,因为它无缘无故喜欢用内存)
不过,这可能不会阻止编译器在内联后将循环不变的工作提升到循环之外。为了打败它,您可能需要类似的 DoNotOptimize 转义符或函数本身内部的 asm volatile
内容,以让它在不打败基准的情况下内联。 (call/ret 确实很便宜,所以尝试不让它内联并不是没有道理的,尽管这会在调用点产生更多的开销,并且它可能需要 save/restore 一些寄存器。)
或者只是构建一个真实反映您真实用例的测试用例,包括周围的代码乱序执行可能与之重叠的内容。
有这样的代码:
#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
的调用中kernel
在 main
中。我尝试了很多不同的东西(用 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.1
和 gcc 10.2.0
获得的(对于此测试,它们之间存在细微差别)。
编辑 0:
另外两个细节:
- 通过使用类似方法(IPO、内联标志等)的 ICC,我获得了类似的结果,即内联 + 优化。我还没有尝试过 Clang。
- 上面的代码,将
kernel
内联到 main 上,只是基本上避免了tmp0
和tmp1
的负载,只是将其乘法结果添加到a[0]
;我知道这很聪明,但我不想要它,我想保留原始代码形式。
内联通常发生在 IR(中间表示)或字节码级别。这意味着它是在源代码的抽象机器独立(在一定程度上)表示上执行的。然后是其他优化过程,这将利用内联代码。这是内联的主要好处之一。
在汇编级别内联,没有任何优化,甚至更多,保持函数体(汇编)完全由于寄存器分配和堆栈的原因会很尴尬管理问题。它可能仍然有点好处(由于删除了 call
; 并且可能由于寄存器分配具有有关所用寄存器的附加信息,不太可能分配非易失性寄存器),但任何编译器都不太可能可以选择这样做。这将需要一个特殊的内联通道,该通道实际上会在后端发生(由于要求保持汇编原样)。
你可以做什么:如果你真的希望 kernel
在汇编中完全是某种方式 - 使用汇编编写你的 kernel
函数(作为一个选项:内联汇编)。如果您的问题确实是其他问题(例如编译器优化计算或您不希望的负载)- 可能还有其他解决方案。
没有让 GCC 做你想做的事情的选项;这对实际程序的性能没有用。 (仅可能用于基准测试。)
如果您希望内联版本的优化与独立版本大致相同,则需要阻止对 args 的任何常量传播,诸如此类。也许通过将它们存储到 volatile
局部变量并将它们传递给函数来隐藏编译器。
这并不能保证 相同 asm,但它应该足够相似以用于基准测试。当然,如果您想在另一个循环中执行此操作,volatile 将意味着从内存中额外加载。因此,您可能只希望像 asm("" : "+g"(var))
这样的内联 asm 使编译器忘记它所知道的有关变量值的任何信息,并将该值具体化在编译器选择的寄存器或内存中。 (有clang的话,大概会选"+r"
,因为它无缘无故喜欢用内存)
不过,这可能不会阻止编译器在内联后将循环不变的工作提升到循环之外。为了打败它,您可能需要类似的 DoNotOptimize 转义符或函数本身内部的 asm volatile
内容,以让它在不打败基准的情况下内联。 (call/ret 确实很便宜,所以尝试不让它内联并不是没有道理的,尽管这会在调用点产生更多的开销,并且它可能需要 save/restore 一些寄存器。)
或者只是构建一个真实反映您真实用例的测试用例,包括周围的代码乱序执行可能与之重叠的内容。