【问题标题】:Link-time optimization and inline链接时优化和内联
【发布时间】:2011-10-26 04:03:18
【问题描述】:

根据我的经验,有很多代码显式使用内联函数,这是一个折衷方案:

  1. 代码变得不那么简洁,可维护性也有所降低。
  2. 有时,内联可以大大提高运行时性能。
  3. 内联是在一个固定的时间点决定的,可能没有很好地预知它的用途,或者没有考虑所有(未来的)周围环境。

问题是:链接时优化(例如,在 GCC 中)是否呈现手动内联,例如,在 C99 中声明一个函数“内联”并提供一个实现,过时了?我们自己不需要考虑对大多数函数进行内联,这是真的吗?那些总是从内联中受益的函数呢,例如 deg_to_rad(x)?

澄清:无论如何,我并不是在考虑在同一个翻译单元中的函数,而是在逻辑上应该驻留在不同翻译单元中的函数。

更新:我经常看到反对“内联”的反对意见,并且建议它已过时。然而,就个人而言,我确实经常看到显式内联函数:作为在类主体中定义的函数。

【问题讨论】:

  • 请注意,没有“总是受益于内联的函数”这样的东西。如果您的deg_to_rad 在代码中的许多不同位置被多次调用,则会大大增加代码大小,从而导致缓存/分页问题。
  • 声明一个函数inline 几乎是无操作的。一个好的编译器会忽略内联决策的关键字,并自行选择是否内联。
  • @Oli Charlesworth:您假设调用的指令数小于函数的指令数。内联int add(int x,int y) {return x+y;} 总是有益的,因为不仅调用成本而且进行调用的指令数都高于函数体的成本。我认为 deg_rad() 也属于这一类,因为它非常简单。
  • 我经常看到 Crashworks 看到的东西。在缺乏运行时知识和完美的 CPU 架构知识之间,编译器不可能做出始终如一的最佳内联决策。这对人类来说也很困难,但严格来说,并非不可能,尤其是在专注于一小段关键代码时。
  • @Martin:你是说编译器总是可以在他们想要的时候内联,还是他们总是做出正确的决定? LTO 现在当然允许前者,但后者不是任何编译器技术都可以声称的。

标签: c++ c optimization gcc


【解决方案1】:

即使使用 LTO,编译器仍然必须使用启发式方法来确定是否为每个 调用 内联函数(注意,它不是根据函数,而是根据调用做出决定)。启发式考虑了以下因素 - 它是否在循环中,循环是否展开,函数有多大,全局调用的频率等。编译器肯定永远无法准确地确定代码被调用的频率,以及代码扩展是否有可能在编译时炸毁特定 CPU 的指令/跟踪/循环/微码缓存。

配置文件引导优化应该是解决这个问题的一步,但如果您曾经尝试过,您可能已经注意到您可以获得大约 0-2% 的性能波动,而且可以在任何一个方向! :-) 它仍在进行中。

如果性能是您的最终目标,并且您确实知道自己在做什么,并且确实对代码进行了彻底的分析,那么人们真正需要的是一种告诉编译器在每次调用时内联或不内联的方法基础,不是每个功能的提示。在实践中,我通过使用编译器特定的“force_no_inline”类型提示来处理我不想要内联的情况,并在我想要内联时使用函数的单独“force_inline”副本(或在极少数情况下失败的宏)来管理它.如果有人知道如何使用编译器特定提示(适用于任何 C/C++ 编译器)以更简洁的方式执行此操作,请告诉我。

具体说明您的观点:

1. 代码变得不那么简洁,可维护性也有所降低。

一般来说,不 - 它只是一个关键字提示,用于控制它的内联方式。但是,如果您像我在上一段中描述的那样跳过箍,那么可以。

2.有时,内联可以大大提高运行时性能。

将编译器留给自己的设备时 - 是的,它当然可以,但大多数情况下不能。编译器具有很好的启发式方法,虽然并不总是最佳的内联决策,但可以做出好的决策。特别是对于关键字,编译器可能会完全忽略该关键字,或者使用 to 关键字作为一个弱提示 - 一般来说,它们似乎对内联代码会标记其启发式方法(例如将 16k 函数内联到展开 16 倍的循环中)。

3.内联是在一个固定的时间点决定的,可能没有很好地预知它的用途,或者没有考虑所有(未来的)周围环境。

是的,它使用静态分析。动态分析可以来自您的洞察力和在每次调用的基础上手动控制内联,或者理论上来自 PGO(这仍然很糟糕)。

【讨论】:

  • 既然你有一个精心设计的内联系统,你如何检查编译器是否内联了一个特定的函数调用?
  • 我也对如何给编译器提示以内联单个函数调用非常感兴趣。
  • 真的很老派:我查看使用 objdump 生成 gcc 或 .asm 输出生成 msvc 的代码,看看实际生成了什么。我也有一些脚本通过 grep "call" 和 wc 管道 objdum -d 输出,以获得总调用数。假设您尝试内联(或不内联)的函数的调用次数少于或多于 1 次,您可以快速获得有关您的代码更改是否产生影响的反馈。
  • 酷。我想知道是否可以使用 gdb 或其他调试器/分析器来完成。
【解决方案2】:

GCC 9 Binutils 2.33 实验证明 LTO 可以内联

对于那些好奇ld 是否跨目标文件内联的人,这里有一个快速实验,确认它可以:

main.c

int notmain(void);

int main(void) {
    return notmain();
}

notmain.c

int notmain(void) {
    return 42;
}

用 LTO 编译和反汇编:

gcc -O3 -flto -ggdb3 -std=c99 -Wall -Wextra -pedantic -c -o main.o main.c
gcc -O3 -flto -ggdb3 -std=c99 -Wall -Wextra -pedantic -c -o notmain.o notmain.c
gcc -O3 -flto -ggdb3 -std=c99 -Wall -Wextra -pedantic -o main.out notmain.o main.o
gdb -batch -ex "disassemble/rs main" main.out

反汇编输出:

   0x0000000000001040 <+0>:     b8 2a 00 00 00  mov    $0x2a,%eax
   0x0000000000001045 <+5>:     c3      retq 

所以我们看到没有callq 或其他跳转,这意味着调用是跨两个目标文件内联的。

没有-flto 但是我们看到:

   0x0000000000001040 <+0>:     f3 0f 1e fa     endbr64 
   0x0000000000001044 <+4>:     e9 f7 00 00 00  jmpq   0x1140 <notmain>

那么怎么有一个JMPQ,这意味着调用没有内联。

请注意,编译器选择了 JMPQ,它不会像更天真的 CALLQ 作为优化所做的那样进行任何堆栈更改,我认为这是 tail call optimization 的一个微不足道的最小情况。

所以是的,如果您使用的是-flto,则无需担心将定义放在标题中以便内联。

在头文件中定义的主要缺点是它们可能会减慢编译速度。对于 C++ 模板,您可能还对显式模板实例化感兴趣:Explicit template instantiation - when is it used?

在 Ubuntu 19.10 amd64 中测试。

【讨论】:

    【解决方案3】:

    问题是:链接时优化(例如,在 GCC 中)是否呈现 手动内联,例如,在 C99 中声明一个函数“内联”和 提供一个实现,过时了吗?

    This article 似乎会回答“是:”

    想一想:是什么让一个函数成为一个好的候选者 内联?除了尺寸因素,优化器还需要知道如何 经常调用这个函数,从哪里调用它,还有多少其他的 程序中的函数是内联的可行候选者,并且 - 信不信由你——函数是否被调用过。优化 (即内联)一个甚至一次都没有调用的函数是浪费 时间和资源。但是优化器如何知道一个函数是 从来没有打电话?好吧,它不能。除非它已经扫描了整个 程序。这就是[链接时间优化]变得至关重要的地方。

    【讨论】:

    • 当然,LTO 是让“内联”过时的必要条件,问题是它在现实生活中是否过时。
    • 不幸的是,这篇文章主要是推测性的,并且是在 LTO 不像今天这样普遍的时候写的。
    • “问题是:链接时优化是否会使手动内联过时。”问题中没有“现实生活”。无论如何,手动内联实际上是一个编译器提示。 LTO 允许编译器和链接器就内联内容做出更明智的选择。
    • 此外,本文还解释了 LTO(又名 WPO)如何在“Visual C++ 7.0 及更高版本,包括最新的 Visual C++ 2005 beta 2”中运行。这怎么投机?如果它处于测试阶段,则它已被实施。
    • 我想,我应该把“现实生活”放在问题中。但是,由于完美的编译器和优化器会使整个讨论和问题变得无效,因此它已经是隐含的。
    【解决方案4】:

    如果链接时优化与编译时优化一样快,那么它就不需要编译器提示了。不幸的是,它通常不会比编译时优化快,因此需要在整体构建速度和构建优化的整体质量之间进行权衡。

    此外,在标题中定义函数时,您仍然需要使用内联。否则,如果在多个翻译单元中使用这些函数的多个定义,您将收到链接器错误。

    【讨论】:

    • 这是一个有效的评论。但是,当假设启用 LTO 时,“内联”真的过时了吗?
    • “否则,如果在多个翻译单元中使用这些函数的多个定义,则会出现链接器错误”——这里需要 static 关键字。
    【解决方案5】:
    1. 我认为 inline 关键字不会影响可维护性,而且只会影响简洁性。 (意见)
    2. 有时内联会降低运行时性能:http://www.parashift.com/c++-faq-lite/inline-functions.html#faq-9.3
    3. 编译器对于内联非常聪明,我听说 Visual Studio 几乎完全忽略它们并自行决定内联。

    does link-time optimization render manual inlining, obsolete? 一点也不,使内联关键字几乎过时的优化器在链接时间之前就开始了。

    【讨论】:

    • 可能使内联过时的优化器无法在链接时间之前启动,b/c 定义可能不在(并且在大多数有趣的情况下)在同一个翻译单元中。
    • 关于 2:这就是为什么我在里面加上“有时”并有“3”。
    • @Billy: inline 不提供函数内部链接,static 提供。也就是说,效果通常是相同的,但在实现方面却不同。
    • @Billy: stackoverflow.com/questions/4957582/… 7.1.2/3 footnote says The inline keyword has no effect on the linkage of a function.
    • @Billy:不,这不是内部链接。
    【解决方案6】:

    第 33 项 - Scott Myers - 第 2 版 - 有效的 C++ 浮现在脑海。

    您必须牢记关键字 static wrt inline!现在有一个马蜂窝!

    【讨论】:

    • 您愿意为我们这些没有那本书的人分享该项目的内容吗?
    • @Oli:特别是当那个项目#来自一个过时的版本时...... :)
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-06-10
    • 2013-03-10
    • 2013-09-24
    • 1970-01-01
    相关资源
    最近更新 更多