为什么不将内容标记为内联?

时间:2010-10-22 18:27:51

标签: c++ optimization inline tradeoff

首先,我正在寻找一种方法来强制编译器内联每个函数的实现。

要降低误导性答案的级别,请务必了解inline关键字的实际含义。这是一个很好的描述,inline vs static vs extern

所以我的问题是,为什么不标记每个函数定义inline?即理想情况下,唯一的编译单元是main.cpp。或者可能还有一些用于无法在头文件中定义的函数(pimpl习惯用法等)。

这个奇怪的请求背后的理论是,它将为优化器提供最大的信息。它当然可以内联函数实现,但它也可以进行“跨模块”优化,因为只有一个模块。还有其他优势吗?

有没有人尝试使用真正的应用程序?性能是否提高了?减少?!?

标记所有函数定义inline有什么缺点?

  • 编译速度可能较慢,会占用更多内存。
  • 迭代构建被破坏,每次更改后都需要重建整个应用程序。
  • 链接时间可能是天文数字

所有这些缺点只影响开发人员。运行时缺点是什么?

11 个答案:

答案 0 :(得分:24)

你真的是说#include一切吗?这只会给你一个模块,让优化器一次看到整个程序。

实际上,当您使用/GL (Whole Program Optimization) switch时,Microsoft的Visual C ++就是这样做的,它实际上并没有编译任何东西,直到链接器运行并且可以访问所有代码。其他编译器也有类似的选择。

答案 1 :(得分:15)

sqlite使用这个想法。在开发过程中,它使用传统的源结构。但实际使用中有一个巨大的c文件(112k行)。他们这样做是为了最大化优化。声称性能提升约5-10%

http://www.sqlite.org/amalgamation.html

答案 2 :(得分:9)

我们(以及其他一些游戏公司)通过制作一个#include所有其他游戏的超级-..CPP来尝试它。这是一种已知的技术。在我们的例子中,它似乎并没有对运行时有多大影响,但是你提到的编译时间的缺点却完全瘫痪了。在每次更改后编译半小时,就无法有效地进行迭代。 (这是应用程序分为十几个不同的库。)

我们尝试进行不同的配置,这样我们在调试时会有多个.objs,然后只在release-opt版本中使用uber-CPP,但后来又遇到了编译器内存不足的问题。对于足够大的应用程序,这些工具根本无法编译数百万行的cpp文件。

我们也尝试了LTCG,这提供了一个小但很好的运行时提升,在极少数情况下它不会在链接阶段简单地崩溃。

答案 3 :(得分:7)

这是半关联的,但请注意,Visual C ++确实能够进行跨模块优化,包括跨模块的内联。有关信息,请参阅http://msdn.microsoft.com/en-us/library/0zza0de8%28VS.80%29.aspx

要为原始问题添加答案,我不认为在运行时会出现缺点,假设优化器足够智能(因此在Visual Studio中将其添加为优化选项)。只需使用足够智能的编译器自动完成,而不会产生您提到的所有问题。 :)

答案 4 :(得分:7)

有趣的问题!你肯定是正确的,所有列出的缺点都是开发人员特有的。但是,我建议弱势开发者生产优质产品的可能性要小得多。可能没有运行时缺点,但想象一下,如果每次编译需要数小时(甚至数天)才能完成,那么开发人员不愿意进行小的更改。

我会从“过早优化”的角度来看待这个问题:多个文件中的模块化代码使程序员的生活更轻松,因此以这种方式做事情有明显的好处。只有当一个特定的应用程序运行得太慢时,并且可以证明内联所有内容都会带来明显的改进,我甚至会认为会给开发人员带来不便。即使这样,也会在大部分开发完成之后(以便可以测量)并且可能只在生产构建中完成。

答案 5 :(得分:3)

利益不大 对于现代平台的良好编译器,inline将仅影响极少数函数。它只是编译器的一个提示,现代编译器本身就很擅长做出这个决定,并且函数调用的开销变得相当小(通常,内联的主要好处不在于减少呼叫开销,但开放进一步的优化)。

编译时间 但是,由于内联也会改变语义,因此您必须将#include所有内容都放入一个巨大的编译单元中。这个通常会显着增加编译时间,这对大型项目来说是一个杀手锏。

代码大小
如果你摆脱目前的桌面平台及其高性能编译器,事情会发生很大变化。在这种情况下,由不太聪明的编译器生成的增加的代码大小将是一个问题 - 这使得代码显着变慢。在嵌入式平台上,代码大小通常是第一个限制。

尽管如此,一些项目可以并且从“内联一切”中获利。它为您提供与链接时间优化相同的效果,至少如果您的编译器不盲目地遵循inline

答案 6 :(得分:3)

在某些情况下已经完成了。它与unity builds的概念非常相似,优点和缺点并非来自你的描述:

  • 编译器更有可能优化
  • 链接时间基本消失(如果一切都在一个翻译单元中,没有什么可以链接的,真的)
  • 编译时间就是这样或那样。正如您所提到的,增量构建变得不可能。另一方面,完整的构建将比其他方式更快(因为每行代码只编译一次。在常规构建中,头中的代码最终在每个包含头的转换单元中编译)

但是在你已经有很多只有头文件的代码的情况下(例如,如果你使用很多Boost),在构建时间和可执行性能方面,它可能是一个非常有价值的优化。

与往常一样,当涉及到性能时,它取决于。这不是一个坏主意,但它也不是普遍适用的。

就时间而言,基本上有两种方法可以优化它:

  • 最小化翻译单元的数量(因此您的标题包含在较少的位置)或
  • 最小化标头中的代码量(以便在多个翻译单元中包含标头的成本降低)

C代码通常采用第二种选择,几乎是极端的:除了前向声明和宏之外几乎没有任何东西保存在头文件中。 C ++通常位于中间位置,这是您获得最差的总构建时间(但PCH和/或增量构建可能会再次减少一些时间),但是在另一个方向上更进一步,最小化翻译单元的数量可以真的为总建造时间做了奇迹。

答案 7 :(得分:2)

这就是Whole Program Optimization和链接时间代码生成(LTCG)背后的哲学:优化机会最适合全球知识。

从实际的角度来看,这有点痛苦,因为现在您所做的每一个更改都需要重新编译整个源代码树。一般来说,您需要的优化构建频率低于进行任意更改所需的频率。

我在Metrowerks时代尝试过这种方法(使用“Unity”样式构建很容易设置)并且编译从未完成。我提到它只是指出它是一个工作流程设置,可能会以他们没有预料到的方式对工具链征税。

答案 8 :(得分:2)

这里的假设是编译器无法跨函数进行优化。这是特定编译器的限制,而不是一般问题。将此作为特定问题的一般解决方案可能会很糟糕。编译器很可能只是使用在其他地方编译的相同内存地址(开始使用缓存)的可重用函数来膨胀你的程序(并且由于缓存而失去性能)。

通常在优化时花费大量功能,局部变量的开销与函数中的代码量之间存在平衡。将函数中的变量数量(传入,本地和全局)保持在平台的一次性变量数量内导致大多数事物都能够保留在寄存器中而不必被驱逐到ram,也就是堆栈不需要帧(取决于目标),因此功能调用开销明显减少。在现实世界的应用程序中一直很难做到,但是替代了少量具有大量局部变量的大函数,代码将花费大量时间驱逐和向ram加载带变量的寄存器(取决于靶)。

尝试llvm它可以优化整个程序而不仅仅是功能。第27版已经赶上了gcc的优化器,至少在一两次测试中,我没有做过详尽的性能测试。 28是出来所以我认为它更好。即使使用少量文件,调谐旋钮组合的数量也太多而无法使用。我发现最好不要进行优化,直到你将整个程序放到一个文件中,然后执行优化,给优化程序整个程序使用,基本上你要用内联做什么,但没有行李。 / p>

答案 9 :(得分:1)

假设foo()bar()都调用了一些helper()。如果所有内容都在一个编译单元中,编译器可能会选择不内联helper(),以减少总指令大小。这会导致foo()helper()进行非内联函数调用。

编译器并不知道foo()的运行时间的纳秒改善会在预期中为您的底线增加100美元/天。它并不知道foo()以外的任何内容的性能提升或降级对您的底线没有影响。

只有你作为程序员知道这些事情(经过仔细的分析和分析)。不内联bar()的决定是告诉编译器你知道什么的一种方式。

答案 10 :(得分:0)

内联的问题在于您希望高性能函数适合缓存。你可能认为函数调用开销是一个很大的性能损失,但在许多架构中,缓存未命中会让这对夫妇推出并弹出水面。例如,如果你有一个很大(可能很深)的函数需要很少从你的主要高性能路径调用,它可能会导致你的主要高性能循环增长到它不适合L1 icache的程度。这将减慢代码的速度,远远超过偶尔的函数调用。

相关问题