C++ 小函数不内联
C++ small function not inlining
我的 C++17 应用程序中有一个热点和关键路径函数(根据 perf record
,约占 cycles:ppp
的 45%),它没有像我预期的那样被内联。这是一个很小的函数——它只是 return 原子指针成员的值。反汇编确认函数只是四个汇编指令,包括 retq
。此外,在整个构建中只有一个调用此函数的人。我什至将此函数声明为 __attribute__((always_inline))
。然而,正在生成此函数的调用和 return。
调用者在文件A,被调用者在文件B.
一些补充说明:
- 我正在使用
-O3
和 -march=native
进行编译
- 被调用者被声明
const
并且不访问任何静态成员
- 我在 linking
时通过 -flto
进行 link 类型的优化
- 我使用的是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 functions are defined in the header because, in order to inline a function call, the compiler must be able to see the function body. For a naive compiler to do that, the function body must be in the same translation unit as the call." 一个翻译单元是指一个源文件连同它 header 编译成一个编译单元。因此,在这种情况下,您可以尝试在同一源文件或包含的 header.
中声明该函数
使 inline
函数实际内联的标准方法是在同一个翻译单元中定义它们(例如,在头文件中),并使用 inline
说明符。
标准 C++ 中没有规定跨翻译单元内联函数,但有时它可以由编译器作为 LTO(IPO、WPO)扩展的一部分来完成。
ICC 将其称为 Interprocedural Optimization (IPO),您要查找的编译标志是 -ipo
。
另见 Using IPO。
注意:还有-inline-level=2
,但是从-O2
开始就已经设置了。
PS
always_inline
属性可能并不代表您认为的意思。通常,当没有打开优化时,g++ 不会内联任何内容(因为这使调试更容易,我想)。通过添加此属性 (always_inline
),编译器将在未优化时内联(可能不是您想要的),但这不会将无法内联(能够)的函数变成可以或将要内联(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();
}
一些编译器(GCC、ICC,但不是 Clang)在构建共享对象(-fPIC
标志)时永远不会在 ELF 目标上内联具有 public 可见性的函数。这是因为该函数可能会被主可执行文件中的新函数替换。
如果您希望内联它们,您可以尝试以下操作:
- 将函数放在头文件中并使用
inline
.
- 对于 GCC,使用
-fno-semantic-interposition
标志来禁用此行为。
- 通过
-fvisibility=protected
或 __attribute__((visibility("protected")))
使用受保护的可见性。这并不总是有效,因为这可能会使编译器生成链接器无法处理的重定位。
- 如果这些函数不需要在您的共享对象之外可见,请使用私有 (
-fvisibility=hidden
) 可见性。
GCC documentation 在 -fsemantic-interposition
上应该说明一些事情:
Some object formats, like ELF, allow interposing of symbols by the
dynamic linker. This means that for symbols exported from the DSO, the
compiler cannot perform interprocedural propagation, inlining and
other optimizations in anticipation that the function or variable in
question may change. While this feature is useful, for example, to
rewrite memory allocation functions by a debugging implementation, it
is expensive in the terms of code quality.
请注意关于副作用的部分,它解释了 -fno-semantic-interposition
如何为编译器提供与 inline
:
类似的保证
With -fno-semantic-interposition the compiler assumes that if interposition happens for functions the overwriting function will have precisely the
same semantics (and side effects). Similarly if interposition happens
for variables, the constructor of the variable will be the same. The
flag has no effect for functions explicitly declared inline (where it
is never allowed for interposition to change semantics) and for
symbols explicitly declared weak.
你也可以查看我的回答。
我的 C++17 应用程序中有一个热点和关键路径函数(根据 perf record
,约占 cycles:ppp
的 45%),它没有像我预期的那样被内联。这是一个很小的函数——它只是 return 原子指针成员的值。反汇编确认函数只是四个汇编指令,包括 retq
。此外,在整个构建中只有一个调用此函数的人。我什至将此函数声明为 __attribute__((always_inline))
。然而,正在生成此函数的调用和 return。
调用者在文件A,被调用者在文件B.
一些补充说明:
- 我正在使用
-O3
和-march=native
进行编译
- 被调用者被声明
const
并且不访问任何静态成员 - 我在 linking 时通过
- 我使用的是icc(ICC)19.0.3.199编译
- 这些函数是
const
而不是模板的简单成员函数,所有函数都有十几个或更少的 x86 汇编指令
-flto
进行 link 类型的优化
实际上,我已经简化了一点——在我的应用程序中实际上有两个地方缺少内联。文件 B 有一个函数 F1,它调用文件 A 的 F2,它调用文件 B 的 F3(F2 和 F3 就是上面列出的)。
文件A:
F2() {
F3();
}
文件 B:
F1() {
F2();
}
F3() {}
如何将所有这些内联到一个函数中?另一个更基本的问题:可以内联定义在不同文件中的函数(可能使用 LTO)吗?
"Inline functions are defined in the header because, in order to inline a function call, the compiler must be able to see the function body. For a naive compiler to do that, the function body must be in the same translation unit as the call." 一个翻译单元是指一个源文件连同它 header 编译成一个编译单元。因此,在这种情况下,您可以尝试在同一源文件或包含的 header.
中声明该函数使 inline
函数实际内联的标准方法是在同一个翻译单元中定义它们(例如,在头文件中),并使用 inline
说明符。
标准 C++ 中没有规定跨翻译单元内联函数,但有时它可以由编译器作为 LTO(IPO、WPO)扩展的一部分来完成。
ICC 将其称为 Interprocedural Optimization (IPO),您要查找的编译标志是 -ipo
。
另见 Using IPO。
注意:还有-inline-level=2
,但是从-O2
开始就已经设置了。
PS
always_inline
属性可能并不代表您认为的意思。通常,当没有打开优化时,g++ 不会内联任何内容(因为这使调试更容易,我想)。通过添加此属性 (always_inline
),编译器将在未优化时内联(可能不是您想要的),但这不会将无法内联(能够)的函数变成可以或将要内联(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();
}
一些编译器(GCC、ICC,但不是 Clang)在构建共享对象(-fPIC
标志)时永远不会在 ELF 目标上内联具有 public 可见性的函数。这是因为该函数可能会被主可执行文件中的新函数替换。
如果您希望内联它们,您可以尝试以下操作:
- 将函数放在头文件中并使用
inline
. - 对于 GCC,使用
-fno-semantic-interposition
标志来禁用此行为。 - 通过
-fvisibility=protected
或__attribute__((visibility("protected")))
使用受保护的可见性。这并不总是有效,因为这可能会使编译器生成链接器无法处理的重定位。 - 如果这些函数不需要在您的共享对象之外可见,请使用私有 (
-fvisibility=hidden
) 可见性。
GCC documentation 在 -fsemantic-interposition
上应该说明一些事情:
Some object formats, like ELF, allow interposing of symbols by the dynamic linker. This means that for symbols exported from the DSO, the compiler cannot perform interprocedural propagation, inlining and other optimizations in anticipation that the function or variable in question may change. While this feature is useful, for example, to rewrite memory allocation functions by a debugging implementation, it is expensive in the terms of code quality.
请注意关于副作用的部分,它解释了 -fno-semantic-interposition
如何为编译器提供与 inline
:
With -fno-semantic-interposition the compiler assumes that if interposition happens for functions the overwriting function will have precisely the same semantics (and side effects). Similarly if interposition happens for variables, the constructor of the variable will be the same. The flag has no effect for functions explicitly declared inline (where it is never allowed for interposition to change semantics) and for symbols explicitly declared weak.
你也可以查看我的回答