【问题标题】:Do modern C++ compilers inline functions which are called exactly once?现代 C++ 编译器是否内联只调用一次的函数?
【发布时间】:2011-10-29 02:42:45
【问题描述】:

例如,假设我的头文件是:

class A
{
    void Complicated();
}

还有我的源文件

void A::Complicated()
{
    ...really long function...
}

我可以把源文件拆分成

void DoInitialStuff(pass necessary vars by ref or value)
{
    ...
}
void HandleCaseA(pass necessary vars by ref or value)
{
    ...
}
void HandleCaseB(pass necessary vars by ref or value)
{
    ...
}
void FinishUp(pass necessary vars by ref or value)
{
    ...
}
void A::Complicated()
{
    ...
    DoInitialStuff(...);
    switch ...
        HandleCaseA(...)
        HandleCaseB(...)
    ...
    FinishUp(...)
}

完全是为了可读性而不用担心影响性能?

【问题讨论】:

  • 也许,也许不是。编译器程序员可能是您最好的选择,具体取决于您使用的编译器。
  • 这一切都没有发生在循环中?您希望从避免几个函数调用的开销中获得多少时间?一纳秒?
  • 被调用的小函数从内联中受益匪浅。一个比函数调用开销大​​得多的函数不会从内联中受益,所以我不会担心。
  • 将您的内部函数声明为static 以赋予它们文件范围。即使您不这样做,它们也可能被内联。但如果它们不是static,则必须导出它们,这意味着即使从未使用过,也必须生成非内联版本。
  • @UncleBens:是的,它可能处于循环中。我只是在代码中说该函数只会被引用一次。

标签: c++ performance code-organization


【解决方案1】:

您应该标记函数static,以便编译器知道它们是该翻译单元的本地函数。

如果没有static,编译器不能假设(除非 LTO / WPA)该函数只被调用一次,因此不太可能内联它。

使用LLVM Try Out 页面进行演示。

也就是说,代码的可读性优先,微优化(这种调整微优化)应该只在性能测量之后进行。


例子:

#include <cstdio>

static void foo(int i) {
  int m = i % 3;
  printf("%d %d", i, m);
}

int main(int argc, char* argv[]) {
  for (int i = 0; i != argc; ++i) {
    foo(i);
  }
}

使用static生产:

; ModuleID = '/tmp/webcompile/_27689_0.bc'
target datalayout = "e-p:64:64:64-i1:8:8-i8:8:8-i16:16:16-i32:32:32-i64:64:64-f32:32:32-f64:64:64-v64:64:64-v128:128:128-a0:0:64-s0:64:64-f80:128:128-n8:16:32:64"
target triple = "x86_64-unknown-linux-gnu"

@.str = private constant [6 x i8] c"%d %d\00"     ; <[6 x i8]*> [#uses=1]

define i32 @main(i32 %argc, i8** nocapture %argv) nounwind {
entry:
  %cmp4 = icmp eq i32 %argc, 0                    ; <i1> [#uses=1]
  br i1 %cmp4, label %for.end, label %for.body

for.body:                                         ; preds = %for.body, %entry
  %0 = phi i32 [ %inc, %for.body ], [ 0, %entry ] ; <i32> [#uses=3]
  %rem.i = srem i32 %0, 3                         ; <i32> [#uses=1]
  %call.i = tail call i32 (i8*, ...)* @printf(i8* getelementptr inbounds ([6 x i8]* @.str, i64 0, i64 0), i32 %0, i32 %rem.i) nounwind ; <i32> [#uses=0]
  %inc = add nsw i32 %0, 1                        ; <i32> [#uses=2]
  %exitcond = icmp eq i32 %inc, %argc             ; <i1> [#uses=1]
  br i1 %exitcond, label %for.end, label %for.body

for.end:                                          ; preds = %for.body, %entry
  ret i32 0
}

declare i32 @printf(i8* nocapture, ...) nounwind

没有static

; ModuleID = '/tmp/webcompile/_27859_0.bc'
target datalayout = "e-p:64:64:64-i1:8:8-i8:8:8-i16:16:16-i32:32:32-i64:64:64-f32:32:32-f64:64:64-v64:64:64-v128:128:128-a0:0:64-s0:64:64-f80:128:128-n8:16:32:64"
target triple = "x86_64-unknown-linux-gnu"

@.str = private constant [6 x i8] c"%d %d\00"     ; <[6 x i8]*> [#uses=1]

define void @foo(int)(i32 %i) nounwind {
entry:
  %rem = srem i32 %i, 3                           ; <i32> [#uses=1]
  %call = tail call i32 (i8*, ...)* @printf(i8* getelementptr inbounds ([6 x i8]* @.str, i64 0, i64 0), i32 %i, i32 %rem) ; <i32> [#uses=0]
  ret void
}

declare i32 @printf(i8* nocapture, ...) nounwind

define i32 @main(i32 %argc, i8** nocapture %argv) nounwind {
entry:
  %cmp4 = icmp eq i32 %argc, 0                    ; <i1> [#uses=1]
  br i1 %cmp4, label %for.end, label %for.body

for.body:                                         ; preds = %for.body, %entry
  %0 = phi i32 [ %inc, %for.body ], [ 0, %entry ] ; <i32> [#uses=3]
  %rem.i = srem i32 %0, 3                         ; <i32> [#uses=1]
  %call.i = tail call i32 (i8*, ...)* @printf(i8* getelementptr inbounds ([6 x i8]* @.str, i64 0, i64 0), i32 %0, i32 %rem.i) nounwind ; <i32> [#uses=0]
  %inc = add nsw i32 %0, 1                        ; <i32> [#uses=2]
  %exitcond = icmp eq i32 %inc, %argc             ; <i1> [#uses=1]
  br i1 %exitcond, label %for.end, label %for.body

for.end:                                          ; preds = %for.body, %entry
  ret i32 0
}

【讨论】:

  • 根据“原则”,我认为您应该将它们放在一个未命名的命名空间中。
  • Clang 在您的示例中的静态和非静态版本中都内联了它......我认为它是否内联调用没有显着差异。函数的代码是否会被发出是有区别的。
  • @GMan:我喜欢static,因为我发现它对人类来说更具可读性,一个未命名的命名空间需要人类记住他在一个中,这并不明显。 static 不需要这个“上下文”。
  • @Matthieu 我一定错过了什么。我正在查看您的演示,它已内联调用。整个main都没有调用foo
【解决方案2】:

取决于别名(指向该函数的指针)和函数长度(在一个分支中内联的大函数可能会将另一个分支抛出缓存,从而影响性能)。

让编译器担心,你担心你的代码:)

【讨论】:

  • 编译器是否也可以将较长的函数本身拆分为较小的函数?
  • OP 是在询问上面写代码和手动内联所有代码的比较。所以我猜他/她想要一个答案来解释分解一个功能是否会带来不利影响。
  • 这听起来不太可能,有什么好处?我认为你更有可能发现编译器一个接一个地复制你的块:)
  • @Oli,是的,我的回复基本上是没关系。
  • @Blindy:如果这段代码构成每秒迭代数百万次的内循环的一部分,那么是的,它确实确​​实很重要。
【解决方案3】:

一个复杂的函数很可能会被函数内部的操作所支配;即使没有内联,函数调用的开销也不会很明显。

您对函数的内联没有太多控制权,最好的方法是尝试并找出答案。

编译器的优化器可能对较短的代码段更有效,因此即使没有内联,您也可能会发现它变得更快。

【讨论】:

  • 那么一个简单的函数呢?
  • @Oli,无论如何,简单的函数更有可能被自动内联。
  • 没有太多控制权?这就是为什么有一个“内联”关键字......简单的函数不能被内联,除非它们被声明为静态的。或者更具体地说,非内联版本必须可供外部调用者使用。此外,如果将一个大函数分解为 15 个小函数(就像 OP 想要做的那样),那将是调用开销的 15 倍。
  • @phkahler:inline 关键字不会强制编译器内联函数。
  • @Bill:是的,这只是大多数编译器也会听取的建议(与旧的 register 关键字不同)。它存在并被使用的事实驳斥了海报的第二句话,即你没有太多的控制权。
【解决方案4】:

如果您将代码拆分为逻辑分组,编译器会做它认为最好的事情:如果它简短易行,编译器应该内联它并且结果是相同的。但是,如果代码很复杂,则进行额外的函数调用实际上可能比内联完成所有工作更快,因此您也可以让编译器选择这样做。最重要的是,逻辑上拆分的代码对于维护者来说更容易理解并避免未来的错误。

【讨论】:

  • 如果函数只被调用一次,我不确定我们是否会通过不内联代码来加快速度... 除非你真的想跳过函数调用并内联函数因此会破坏您的指令缓存。是最新的吗?
  • 我的想法是,通过将代码分解为逻辑功能块,编译器可以更智能地利用寄存器,以及其他我无法想象的可能性。
【解决方案5】:

我建议您创建一个辅助类来将您的复杂函数分解为方法调用,就像您提议的那样,但是没有将参数传递给这些较小函数中的每一个的冗长、无聊和不可读的任务。通过将这些参数设为帮助程序类的成员变量,只传递一次这些参数。

此时不要专注于优化,确保您的代码是可读的,并且您在 99% 的情况下都可以正常工作。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2017-07-02
    • 2010-11-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-05-19
    • 1970-01-01
    • 2012-01-17
    相关资源
    最近更新 更多