C++小函数没有内联

C++ small function not inlining

本文关键字:函数 C++      更新时间:2023-10-16

我的C++17应用程序中有一个热关键路径函数(根据perf record,大约占cycles:ppp的45%),它没有像我预期的那样内联。这是一个很小的函数——它只是返回一个原子指针成员的值。反汇编确认该函数仅为四个汇编指令,包括retq。此外,在整个构建中,该函数只有一个调用方。我甚至已经将这个函数声明为__attribute__((always_inline))。然而,正在生成一个对该函数的调用和返回。

调用者在文件A中,被调用者在文件B中。

一些附加说明:

  • 我正在使用-O3-march=native进行编译
  • 被调用者声明为const,并且不访问任何静态成员
  • 链接时我正在通过-flto进行链接类型优化
  • 我正在使用icc(icc)19.0.3.199进行编译
  • 这些函数是简单的成员函数,是const,而不是模板,所有这些函数都有十几条或更少的x86汇编指令

实际上,我已经简化了一点——实际上,在我的应用程序中,有两个地方缺少内联。文件B有一个函数F1,它调用文件a的F2,调用文件B的F3(F2和F3是上面列出的)。

文件A:

F2() {
F3();
}

文件B:

F1() {
F2();
}
F3() {}

如何将所有这些内联到一个函数中?另一个更基本的问题是:在不同的文件中定义的函数是否可以内联(也许使用LTO)?

PS

always_inline属性可能并不代表您所认为的含义。通常,当没有打开优化时,g++不会内联任何内容(我认为这会使调试更容易)。通过添加此属性(always_inline),编译器将在未优化时内联(可能不是您想要的),但这不会使未内联(able)的函数变成可以或将要内联(ed)的函数。

请参阅:https://gcc.gnu.org/onlinedocs/gcc/Inline.html

根据您的意见,您有以下内容:

文件A.h

void F2();

文件B.h

void F1();
void F3() __attribute__((always_inline));

文件A.cpp

#include "A.h"
#include "B.h"
void F2() {
F3();
}

文件B.cpp

#include "B.h"
#include "A.h"
void F1() {
F2();
}
void F3() {}

在未来,这将是你应该提交的最低可行的申请,因为它有所有的类型信息,足以重建你的情况。

你提供的代码是不可编译的,需要大量的认知负荷才能将你提供的英语描述分解为可编译的代码。

如果您已经设置了编译器,那么可以将F3()内联到A.cpp中,但情况可能并不总是如此。要进行这种优化,翻译单元必须能够访问F3()的源,或者您必须能够跨翻译单元进行优化。

您可以通过将F3()的主体移动到头文件中来简化此过程。然后它将可以直接内联到翻译单元。

文件A.h

void F2();

文件B.h

void F1();
void F3() __attribute__((always_inline)); // I would not add this.
// Let the compiler not inline in debug mode.
inline void F3() {}

文件A.cpp

#include "A.h"
#include "B.h"
void F2() {
F3();
}

文件B.cpp

#include "B.h"
#include "A.h"
void F1() {
F2();
}

使inline函数实际内联的标准方法是在同一转换单元中定义它们(例如,在头文件中),并使用inline说明符。

标准C++中没有关于跨翻译单元内联函数的规定,但有时它可以由编译器作为LTO(IPO,WPO)扩展的一部分来完成。

ICC称之为过程间优化(IPO),您要查找的编译标志是-ipo

另请参阅使用IPO。


注意:还有-inline-level=2,但从-O2开始已经设置

一些编译器(GCC、ICC,但不是Clang)在构建共享对象(-fPIC标志)时,永远不会在ELF目标上内联具有公共可见性的函数。这是因为主可执行文件中的函数可能会被新函数所取代。

如果您希望它们内联,您可以尝试以下操作:

  • 将函数放入头文件中,然后使用inline
  • 对于GCC,请使用-fno-semantic-interposition标志来禁用此行为
  • 通过-fvisibility=protected__attribute__((visibility("protected")))使用受保护的可见性。这并不总是有效的,因为这可能会使编译器生成链接器无法处理的重新定位
  • 如果这些函数不需要在共享对象之外可见,请使用专用(-fvisibility=hidden)可见性

-fsemantic-interposition上的GCC文档应该说明一些事情:

一些对象格式,如ELF,允许通过动态链接器。这意味着,对于从DSO导出的符号编译器无法执行过程间传播、内联和预期中的函数或变量这个问题可能会改变。虽然此功能很有用,例如通过调试实现重写内存分配函数在代码质量方面是昂贵的。

注意关于副作用的部分,它解释了-fno-semantic-interposition如何提供与inline:类似的编译器保证

使用-fno语义插入,编译器假设如果函数发生插入,覆盖函数将具有完全相同的语义(和副作用)类似地,如果发生插入对于变量,变量的构造函数将是相同的。这个标志对于显式内联声明的函数(在从不允许插入来更改语义)和符号明确声明为弱。

您也可以在此处查看我的答案。

"内联函数是在头中定义的,因为为了内联函数调用,编译器必须能够看到函数体。为了实现这一点,初级编译器必须将函数体与调用放在同一个翻译单元中。"翻译单元指的是一个源文件及其头,这些文件可编译为一个编译单元。因此,在这种情况下,您可以尝试在同一源文件或包含的标头中声明该函数。