【问题标题】:C++ small function not inliningC ++小函数不内联
【发布时间】:2020-06-03 01:24:24
【问题描述】:

我的 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)?

【问题讨论】:

  • 你简化得太多了。标题中的内容 源中的内容。
  • 所有函数定义都在源码中;标头只有声明
  • 虽然编译器可以进行交叉翻译单元优化,但这更难,需要您正确设置编译器。要让内联在最简单的情况下工作,请将内联函数定义放入头文件中。如果函数很简单,这应该不会导致任何问题(您需要将它们标记为 inline 才能成为合法的 C++)
  • 您是否正在使用-fPIC 构建?因为由于可能的函数插入,ICC 似乎从不内联非内联函数。并且 ICC 不支持-fno-semantic-interposition
  • @StaceyGirl 实际上我正在与-fPIC一起构建

标签: c++ inlining lto


【解决方案1】:

PS

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

见:https://gcc.gnu.org/onlinedocs/gcc/Inline.html

鉴于您的 cmets,您有以下几点:

文件 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();
}

【讨论】:

  • 这是有道理的,你关于always_inline 的观点确实解决了我的一个误解。
【解决方案2】:

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

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

ICC 将其称为 Interprocedural Optimization (IPO),而您要查找的编译标志是 -ipo

另见Using IPO


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

【讨论】:

    【解决方案3】:

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

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

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

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

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

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

    使用 -fno-semantic-interposition,编译器假定如果函数发生插入,则覆盖函数 将精确地具有 相同的语义(和副作用)。 如果发生插入,则类似 对于变量,变量的构造函数将是相同的。这 flag 对显式声明为 inline 的函数无效(其中 不允许插入来改变语义)和 符号显式声明为弱。

    您也可以查看我的回答here

    【讨论】:

      【解决方案4】:

      “内联函数是在头文件中定义的,因为为了内联函数调用,编译器必须能够看到函数体。对于一个简单的编译器来说,函数体必须在同一个翻译单元中作为电话。”一个翻译单元意味着一个源文件连同它的头文件一起编译成一个编译单元。因此,在这种情况下,您可以尝试在同一源文件或包含的头文件中声明该函数。

      【讨论】:

        猜你喜欢
        • 2017-01-05
        • 2011-08-23
        • 1970-01-01
        • 2014-12-24
        • 1970-01-01
        • 2023-03-10
        • 2011-03-07
        • 2010-11-06
        相关资源
        最近更新 更多