53

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

为了减少错误答案的水平,请确保您了解inline关键字的实际含义。这是很好的描述,inline vs static vs extern

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

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

有没有人用真实的应用程序试过这个?性能提高了吗?减少?!?

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

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

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

4

11 回答 11

26

你真的意味着#include一切吗?那只会给你一个模块,让优化器一次看到整个程序。

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

于 2010-10-22T18:35:23.443 回答
16

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

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

于 2010-10-22T19:11:40.597 回答
10

我们(和其他一些游戏公司)确实尝试过制作一个 uber-.CPP 来#include编辑所有其他游戏;这是一种已知的技术。在我们的例子中,它似乎对运行时影响不大,但是你提到的编译时缺点被证明是完全瘫痪的。每次更改后编译半小时,就不可能有效地迭代。(这是应用程序分为十几个不同的库。)

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

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

于 2010-10-22T22:43:46.807 回答
9

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

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

于 2010-10-22T18:36:47.113 回答
7

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

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

于 2010-10-22T18:33:01.123 回答
4

一点好处 在现代平台上的一个好的编译器上,inline只会影响很少的功能。这只是给编译器的一个提示,现代编译器本身很擅长做出这个决定,而且函数调用的开销已经变得相当小(通常,内联的主要好处不是减少调用开销,而是开放进一步优化)。

编译时间 然而,由于 inline 也改变了语义,你必须把#include所有东西都放在一个巨大的编译单元中。这通常会显着增加编译时间,这是大型项目的杀手锏。

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

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

于 2010-10-23T23:42:40.370 回答
3

在某些情况下已经完成了。它与unity builds的想法非常相似,并且优点和缺点与您所描述的不同:

  • 编译器有更多的优化潜力
  • 链接时间基本上消失了(如果所有内容都在一个翻译单元中,那么真的没有什么要链接的)
  • 编译时间以一种或另一种方式进行。正如您所提到的,增量构建变得不可能。另一方面,完整的构建将比其他方式更快(因为每一行代码都只编译一次。在常规构建中,标题中的代码最终会在包含标题的每个翻译单元中编译)

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

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

就 buld time 而言,您基本上有两种优化方法:

  • 最小化翻译单元的数量(这样你的标题包含在更少的地方),或者
  • 最小化标头中的代码量(从而降低在多个翻译单元中包含标头的成本)

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

于 2010-10-24T00:02:17.423 回答
3

这几乎就是整个程序优化和链接时间代码生成 (LTCG) 背后的理念:利用全球知识,优化机会是最好的。

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

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

于 2010-10-22T18:40:13.040 回答
2

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

大函数在优化上一般成本,在局部变量的开销和函数中的代码量之间存在平衡。将函数中的变量数量(传入的、本地的和全局的)保持在平台的一次性变量数量之内,结果大多数东西都可以留在寄存器中,而不必被驱逐到 ram,还有一个堆栈不需要框架(取决于目标),因此函数调用开销显着减少。在现实世界的应用程序中一直很难做到,但是替代少数具有大量局部变量的大函数,代码将花费大量时间将带有变量的寄存器逐出和加载到/从 ram(取决于目标)。

试试 llvm,它可以优化整个程序,而不仅仅是逐个函数。Release 27 已经赶上了 gcc 的优化器,至少一两次测试,我没有做详尽的性能测试。28 已经出来了,所以我认为它会更好。即使有几个文件,调谐旋钮组合的数量也太多了。我发现最好在将整个程序放入一个文件之前根本不进行优化,然后执行优化,让优化器可以使用整个程序,基本上就是你试图用内联做的事情,但没有包袱。

于 2010-10-22T21:56:15.690 回答
1

假设foo()bar()都调用 some helper()。如果一切都在一个编译单元中,编译器可能会选择不内联helper(),以减少总指令大小。这会导致foo()helper().

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

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

于 2016-01-05T02:52:32.640 回答
0

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

于 2010-10-22T18:37:21.263 回答