假设我有以下代码:
struct Foo { void helper() { ... } void fast_path() { ...; helper(); ... } void slow_path1() { ...; helper(); ... } void slow_path2() { ...; helper(); ... } };
该方法对fast_path()
性能至关重要,因此应尽一切努力使其尽可能快.这些方法slow_path1()
和slow_path2()
是不是性能的关键.
根据我的理解,典型的编译器可能会查看此代码并决定不内联helper()
是否足够复杂,以减少总指令大小,这helper()
在多个方法函数之间共享.helper()
如果慢路径方法不存在,那么相同的编译器可能会内联.
鉴于我们期望的性能特征,我们希望编译器内联调用helper()
内部fast_path()
,但更喜欢编译器在slow_path1()
和中的默认行为slow_path2()
.
一种解决方法是让慢速路径函数定义和调用生成fast_path()
在单独的编译单元中,这样编译器就不会看到helper()
共享的用法fast_path()
.但是保持这种分离需要特别小心,不能通过编译器强制执行.此外,文件(Foo.h,FooINLINES.cpp,现在还有Foo.cpp)的扩散是不可取的,并且额外的编译单元使可能只是标题库的构建变得复杂.
有没有更好的办法?
理想情况下,我想要一个新的do_not_inline_function_calls_inside_me
c ++关键字,我可以这样使用:
do_not_inline_function_calls_inside_me void slow_path1() { ... } do_not_inline_function_calls_inside_me void slow_path2() { ... }
或者,一个inline_function_calls_inside_me
关键字,如下所示:
inline_function_calls_inside_me void fast_path() { ... }
请注意,这些假设关键字装饰*_path*()
方法,而不是helper()
方法.
您可能具有这些性能需求的示例上下文是编程竞赛,其中每个参与者编写侦听类型A和B的稀疏全局数据广播的应用程序.当接收到类型B广播时,每个应用程序必须执行计算取决于先前广播的A类消息的顺序,并将计算结果提交给中央服务器.每个B型广播的第一个正确响应者得分.计算问题的性质可能允许对A类更新执行预计算; 快速做这些没有任何好处.
一般来说,您不应尝试比编译器更聪明。现代编译器在决定如何内联函数方面做得非常出色,而众所周知,人类对此一无所知。
以我的经验,最好的办法是将所有相关功能作为inline
函数放在同一个翻译单元中,以便编译器可以看到它们的定义并可以按需内联它们。但是,是否保留将给定函数内联到编译器的最终决定是最重要的决定,并且非常谨慎地使用“强制内联”,除非您有证据表明它在给定情况下具有有益的作用。
为了简化编译器的工作,您可以向其提供有关程序的其他信息。在GCC和Clang中,您可以为此使用函数属性。
struct Foo { void helper(); void fast_path() __attribute__ ((hot)); void slow_path1() __attribute__ ((cold)); void slow_path2() __attribute__ ((cold)); }; inline void Foo::helper() { … } inline void Foo::fast_path() { … } inline void Foo::slow_path1() { … } inline void Foo::slow_path2() { … }
这将暗示编译器优化Foo::fast_path
更积极的速度和Foo::slow_path1
以及Foo::slow_path2
对小型高速缓存足迹。如果这些函数之一调用Foo::helper
,它可以根据情况决定是否内联它。(有关注释的确切效果,请参见链接的手册中的文档。)
提示编译器的一种更好的方法是为它提供实际的性能分析数据。使用GCC,您可以使用-fprofile-generate
选项编译程序。这将使用收集配置文件统计信息的代码来检测您的二进制文件。现在,使用一组具有代表性的输入来运行您的程序。这样做将*.gcda
使用收集的配置文件数据创建一个文件。现在使用该-fprofile-use
选项重新编译。GCC将使用收集到的配置文件信息来确定代码中的哪些路径是热门路径以及它们如何相互影响。这项技术称为配置文件导向的优化(PGO)。
当然,如果您担心这些事情,请首先确保启用适当的优化级别(-O2
)。尤其是繁重的C +代码(即,几乎所有使用标准库或Boost的代码)在编译时都可能会产生难看的机器代码,而无需进行适当的优化。还要考虑是否要将assert
离子编译到您的代码(-DNDEBUG
)中。