为什么C++编译需要这么长时间?

Dan*_*ein 518 c++ performance compilation

与C#和Java相比,编译C++文件需要很长时间.编译C++文件所需的时间比运行普通大小的Python脚本要长得多.我目前正在使用VC++,但它与任何编译器都是一样的.为什么是这样?

我能想到的两个原因是加载头文件和运行预处理器,但这似乎不应该解释为什么它需要这么长时间.

jal*_*alf 779

几个原因

头文件

每个编译单元都需要数百甚至数千个头文件(1)加载和(2)编译.通常必须为每个编译单元重新编译它们中的每一个,因为预处理器确保编译头的结果可能在每个编译单元之间变化.(可以在一个编译单元中定义宏,该编译单元改变标题的内容).

这可能主要原因,因为它需要为每个编译单元编译大量代码,此外,每个头文件必须多次编译(每个编译单元包含它一次).

链接

编译完成后,所有目标文件必须链接在一起.这基本上是一个单一的过程,不能很好地并行化,并且必须处理整个项目.

解析

解析时语法极其复杂,在很大程度上取决于上下文,并且很难消除歧义.这需要很多时间.

模板

在C#中,List<T>无论你在程序中有多少个List实例,它都是唯一被编译的类型.在C++中,vector<int>是一个完全独立的类型vector<float>,每个都必须单独编译.

除此之外,模板构成了编译器必须解释的完整图灵完整的"子语言",这可能变得非常复杂.即使是相对简单的模板元编程代码也可以定义递归模板,这些模板可以创建数十个模板实例.模板也可能导致极其复杂的类型,名称冗长,为链接器添加了大量额外的工作.(它必须比较许多符号名称,如果这些名称可以增长到数千个字符,那可能会变得相当昂贵).

当然,它们会加剧头文件的问题,因为模板通常必须在头文件中定义,这意味着必须为每个编译单元解析和编译更多的代码.在普通的C代码中,标头通常只包含前向声明,但实际代码很少.在C++中,几乎所有代码都驻留在头文件中并不罕见.

优化

C++允许一些非常戏剧性的优化.C#或Java不允许完全删除类(它们必须用于反射目的),但即使是简单的C++模板元程序也可以轻松生成数十个或数百个类,所有这些类都在优化中被内联并消除相.

此外,编译器必须完全优化C++程序.AC#程序可以依赖JIT编译器在加载时执行额外的优化,C++没有得到任何这样的"第二次机会".编译器生成的内容是最优化的.

C++被编译为机器代码,这可能比字节码Java或.NET使用更复杂(特别是在x86的情况下).(这完全是出于完整性而被提及,因为它在评论等中提到过.在实践中,这一步骤不太可能占用总编译时间的一小部分).

结论

大多数这些因素都由C代码共享,实际上编译效率相当高.解析步骤在C++中要复杂得多,并且可以占用更多的时间,但主要的攻击者可能是模板.它们很有用,并且使C++成为一种更强大的语言,但它们也会在编译速度方面付出代价.

  • 关于模板:不仅vector <int>必须与vector <double>分开编译,而vector <int>在每个使用它的编译单元中重新编译.链接器消除了冗余定义. (68认同)
  • 关于第3点:C编译明显快于C++.它肯定是导致速度减慢的前端,而不是代码生成. (37认同)
  • dribeas:是的,但这不是模板特有的.内联函数或标题中定义的任何内容都将在包含它的任何位置重新编译.但是,是的,模板特别痛苦.:) (14认同)
  • @configurator:Visual Studio和gcc都允许预编译头文件,这可以为编译带来一些严重的加速. (14认同)
  • 不确定优化是否是问题,因为我们的DEBUG构建实际上比发布模式构建慢.pdb生成也是罪魁祸首. (5认同)
  • @ColeJohnson:但这与其他任何语言都没有什么不同.我试图列出C++独有的东西 (3认同)
  • @linquize不正确; 至少在我的经验中.我已经在同一台机器上在有和没有固态驱动器的情况下在MSVC中编译大型C++代码库进行基准测试...... SSD将性能提高了大约5%......而不是花费12分钟来编译,它需要11和一点点.大多数情况下,CPU是瓶颈,一遍又一遍地重新编译所有这些头文件 (3认同)
  • 关于第 1 点:不能缓存编译的头文件,也许每个宏配置一次? (2认同)
  • @configurator:是的,它们可以被缓存。Visual Studio 执行此操作,但我不知道详细信息。我认为 gcc 默认不做任何缓存,但似乎是可能的。 (2认同)
  • 根据我们的经验,特别是模板很难(慢)编译 - 在我们的项目中,直到预编译头不再重要.我们使用模板的次数越多,我们用它们做的高级内容就越多(比如多个封装级别,特征,策略甚至元编程),编译时间越长. (2认同)

tan*_*orm 37

任何编译器的减速都不一定相同.

我没有使用Delphi或Kylix但是在MS-DOS时代,Turbo Pascal程序几乎可以立即编译,而等效的Turbo C++程序只会抓取.

两个主要区别是一个非常强大的模块系统和允许单通道编译的语法.

编译速度当然不是C++编译器开发人员的优先考虑因素,但C/C++语法中也存在一些固有的复杂性,使得处理起来更加困难.(我不是C的专家,但Walter Bright是,并且在构建各种商业C/C++编译器之后,他创建了D语言.他的一个变化是强制执行无上下文语法以使语言更易于解析.)

此外,您会注意到通常设置Makefile,以便每个文件都在C中单独编译,因此如果10个源文件都使用相同的包含文件,则包含文件将被处理10次.

  • 比较Pascal很有意思,因为Niklaus Wirth使用编译器在设计语言和编译器时将自身编译为基准的时间.有一个故事,在仔细编写了一个快速符号查找模块之后,他用一个简单的线性搜索取而代之,因为减小的代码大小使编译器自己编译得更快. (37认同)

Jam*_*ran 36

解析和代码生成实际上相当快.真正的问题是打开和关闭文件.请记住,即使使用包含保护,编译器仍然打开.H文件,并读取每一行(然后忽略它).

一位朋友曾经(虽然在工作中感到无聊)拿走了他公司的应用程序并将所有内容 - 所有源文件和头文件 - 放入一个大文件中.编译时间从3小时降至7分钟.

  • 好吧,文件访问肯定有这方面,但正如jalf所说,其中的主要原因将是其他东西,即重复解析许多,许多,(嵌套!)头文件,在你的情况下完全退出. (13认同)
  • 解析是一个大问题.对于N对具有相互依赖性的类似大小的源/头文件,有O(N ^ 2)遍历头文件.将所有文本放入单个文件中会减少重复的解析. (11认同)
  • 正是在这一点上,你的朋友需要设置预编译的头文件,打破不同头文件之间的依赖关系(尽量避免一个头包括另一个头,而不是前向声明)并获得更快的硬盘.除此之外,一个非常惊人的指标. (9认同)
  • 小旁注:包含防护措施,防止每个编译单元进行多次解析.总体上不反对多个解析. (8认同)
  • 如果整个头文件(可能的注释和空行除外)在标题保护范围内,gcc能够记住该文件并在定义了正确的符号时跳过它. (6认同)
  • 添加到@MarcovandeVoort,一些编译器认包括它们是什么警卫,让编译器避免重新打开该文件在所有.其他(在这一点上,大多数)以`#pragma once`为特色,明确地做了包含守卫的内容(将文件标记为仅包含一次),这避免了重复重复打开文件.但这些都不是解决了单独的编译单位的问题; 每个C/C++源文件将重新读取所有这些标题,包括guard和`#pragma once`只是意味着它们不会多次重读它们. (2认同)

Ala*_*lan 15

C++被编译成机器代码.所以你有预处理器,编译器,优化器,最后是汇编器,所有这些都必须运行.

Java和C#编译成字节码/ IL,Java虚拟机/ .NET Framework在执行之前执行(或JIT编译成机器代码).

Python是一种解释语言,也可以编译成字节码.

我确信还有其他原因,但一般来说,不必编译为本机机器语言可以节省时间.

  • 预处理增加的成本是微不足道的.减速的主要"其他原因"是编译被拆分为单独的任务(每个目标文件一个),因此一次又一次地处理公共标题.这是O(N ^ 2)最坏情况,与大多数其他语言O(N)解析时间相比. (15认同)
  • 您可以从相同的论证中判断出C,Pascal等编译器的速度很慢,平均而言并非如此.它更多地与C++的语法和C++编译器必须维护的巨大状态有关. (11认同)
  • C很慢.它遇到与接受的解决方案相同的头解析问题.例如,采取一个简单的Windows GUI程序,其中包括WINDOWS.H在几个编译单元,并测量在添加(短)编译单元的编译性能. (2认同)

Dav*_*Ray 15

另一个原因是使用C预处理器来定位声明.即使有标题保护,每次包含它们时,仍然必须反复解析.h.有些编译器支持预编译的头文件,可以帮助解决这个问题,但并不总是使用它们.

另请参阅:C++常见问题解答

  • 如果整个头文件(可能的注释和空行除外)在标题保护范围内,gcc能够记住该文件并在定义了正确的符号时跳过它. (6认同)
  • @CesarB:每个编译单元(.cpp文件)仍需要完整处理一次. (5认同)

Mar*_*ort 12

最大的问题是:

1)无限头重新分析.已经提到了.缓解(如#pragma一次)通常只对每个编译单元有效,而不是每个构建.

2)工具链通常被分成多个二进制文件(make,预处理器,编译器,汇编器,归档器,impdef,链接器和dlltool,在极端情况下),所有这些都必须为每次调用重新初始化并重新加载所有状态(编译器,汇编程序)或每两个文件(归档程序,链接程序和dlltool).

另见comp.compilers的讨论:http://compilers.iecc.com/comparch/article/03-11-078 特别是这个:

http://compilers.iecc.com/comparch/article/02-07-128

请注意,comp.compilers的主持人John似乎同意,并且这意味着如果完全集成工具链并实现预编译头文件,那么C也应该可以实现类似的速度.许多商业C编译器在某种程度上做到了这一点.

请注意,将所有内容分解为单独的二进制文件的Unix模型是Windows的最坏情况模型(其创建过程缓慢).在比较Windows和*nix之间的GCC构建时间时,这是非常值得注意的,特别是如果make/configure系统也只是为了获取信息而调用某些程序.


小智 11

构建C/C++:真正发生了什么,为什么需要这么长时间

软件开发时间的相当大一部分不用于编写,运行,调试甚至设计代码,而是等待它完成编译.为了使事情变得快速,我们首先要了解编译C/C++软件时发生的事情.步骤大致如下:

  • 组态
  • 构建工具启动
  • 依赖性检查
  • 汇编
  • 链接

现在,我们将更详细地研究每个步骤,重点关注如何更快地制作它们.

组态

这是开始构建的第一步.通常意味着运行配置脚本或CMake,Gyp,SCons或其他一些工具.对于非常大的基于Autotools的配置脚本,这可能需要一秒到几分钟的时间.

这个步骤相对很少发生.只需在更改配置或更改构建配置时运行它.如果没有更改构建系统,那么要做得更快,就没有太多工作要做.

构建工具启动

当您运行make或单击IDE上的构建图标(通常是make的别名)时会发生这种情况.构建工具二进制文件启动并读取其配置文件以及构建配置,这通常是相同的.

根据构建的复杂性和大小,这可能需要从几分之一秒到几秒钟.这本身就不会那么糟糕.不幸的是,大多数基于make的构建系统会导致make为每个构建调用数十到数百次.通常这是由递归使用make引起的(这很糟糕).

应该注意的是,Make的原因是如此缓慢并不是一个实现错误.Makefile的语法有一些怪癖,它们实现了一个非常快速的实现,但几乎不可能.与下一步结合使用时,此问题更加明显.

依赖性检查

一旦构建工具读取了其配置,就必须确定哪些文件已更改以及哪些文件需要重新编译.配置文件包含描述构建依赖关系的有向非循环图.此图通常在配置步骤中构建.构建工具启动时间和依赖扫描程序在每个构建上运行.它们的组合运行时确定了编辑 - 编译 - 调试周期的下限.对于小型项目,这个时间通常是几秒钟左右.这是可以忍受的.Make还有其他选择.其中最快的是Ninja,它是由Google工程师为Chromium构建的.如果您使用CMake或Gyp构建,只需切换到他们的Ninja后端.您无需在构建文件中自行更改任何内容,只需享受速度提升即可.但是,Ninja并没有打包在大多数发行版上,因此您可能必须自己安装它.

汇编

此时我们终于调用了编译器.切割一些角落,这是采取的近似步骤.

  • 合并包括
  • 解析代码
  • 代码生成/优化

与流行的看法相反,编译C++实际上并不是那么慢.STL很慢,用于编译C++的大多数构建工具都很慢.但是,有更快的工具和方法来缓解语言的缓慢部分.

使用它们需要一点肘部油脂,但好处是不可否认的.更快的构建时间可以带来更快乐的开发人员,更高的灵活性以及最终更好的代码.


Nem*_*vic 9

一些原因是:

1) C++ 语法比 C# 或 Java 更复杂,解析需要更多时间。

2)(更重要)C++ 编译器生成机器代码并在编译期间进行所有优化。C# 和 Java 只走了一半,将这些步骤留给 JIT。


And*_*ice 7

编译语言总是需要比解释语言更大的初始开销.另外,也许你没有很好地构建你的C++代码.例如:

#include "BigClass.h"

class SmallClass
{
   BigClass m_bigClass;
}
Run Code Online (Sandbox Code Playgroud)

编译速度比以下慢很多:

class BigClass;

class SmallClass
{
   BigClass* m_bigClass;
}
Run Code Online (Sandbox Code Playgroud)

  • 这可能是一个原因.但是Pascal例如只需要等同于C++程序的编译时间的十分之一.这不是因为gcc:s优化需要更长时间,而是Pascal更容易解析而不必处理预处理器.另请参阅Digital Mars D编译器. (7认同)
  • 特别是如果BigClass恰好包含它使用的另外5个文件,最终包括程序中的所有代码. (2认同)
  • 它不是更容易解析,它是模块化的,避免重新解释windows.h和每个编译单元的其他标题.是的,Pascal解析更容易(虽然成熟的,像Delphi一样更复杂),但这并不是最重要的. (2认同)

T.E*_*.D. 6

你得到的权衡是程序运行得更快一点。在开发过程中,这对您来说可能是一种冰冷的安慰,但是一旦开发完成,并且程序只是由用户运行,这可能会很重要。


ril*_*ton 6

在较大的C++项目中减少编译时间的一种简单方法是制作一个*.cpp包含文件,其中包含项目中的所有cpp文件并进行编译.这会将标题爆炸问题减少一次.这样做的好处是编译错误仍将引用正确的文件.

例如,假设你有a.cpp,b.cpp和c.cpp ..创建一个文件:everything.cpp:

#include "a.cpp"
#include "b.cpp"
#include "c.cpp"
Run Code Online (Sandbox Code Playgroud)

然后通过make everything.cpp编译项目

  • @rileyberton(因为有人赞成你的评论)让我拼出来:不,它不会加速汇编.实际上,它确保任何编译都通过_not_隔离转换单位来获取_maximum time_.关于它们的好处是,如果它们没有改变,你就不需要重新编译所有.cpp-s.(这无视风格论点).适当的依赖管理和[预编译头文件](http://en.wikipedia.org/wiki/Precompiled_header)要好得多. (8认同)
  • 抱歉,这**可以是一种非常有效的加速编译的方法,因为你(1)几乎消除了链接,(2)只需要处理常用的头文件一次.此外,它在实践中**,如果你懒得尝试它.不幸的是,它使得增量重建变得不可能,因此每个构建都是完全从头开始.但是用这种方法进行完全重建*要比你得到的快得多 (7认同)
  • @BartekBanachewicz肯定,但你所说的*是"它没有加快编译速度",没有限定词.正如你所说,它使每次编译都花费了最多的时间(没有部分重建),但与此同时,它大大降低了最大值.我只是说它比"不要这样做"更加细致入微 (4认同)
  • 我没有看到对这种方法的反对意见.假设您从脚本或Makefile生成包含,这不是维护问题.事实上它确实加速了编译而没有混淆编译问题.您可以在编译时争论内存消耗,但这在现代机器上很少出现问题.那么这种方法的目标是什么(除了断言这是错误的)? (3认同)
  • 享受静态变量和函数.如果我想要一个大的编译单元,我将创建一个大的.cpp文件. (2认同)

Pan*_*nic 5

大多数答案都有些不清楚,提到 C# 将始终运行得更慢,因为执行操作的成本在 C++ 中仅在编译时执行一次,此性能成本也因运行时依赖性而受到影响(需要加载更多的东西才能运行),更不用说 C# 程序总是有更高的内存占用,所有这些都导致性能与可用硬件的能力更密切相关。对于解释或依赖于 VM 的其他语言也是如此。


小智 5

我能想到的两个问题可能会影响 C++ 程序的编译速度。

可能的问题 #1 - 编译标头:(这可能已经或可能没有由另一个答案或评论解决。)Microsoft Visual C++(又名 VC++)支持预编译标头,我强烈推荐。当您创建一个新项目并选择您正在制作的程序类型时,屏幕上应该会出现一个设置向导窗口。如果您点击底部的“下一步>”按钮,该窗口将带您进入一个包含多个功能列表的页面;确保选中“预编译头”选项旁边的框。(注意:这是我在 C++ 中使用 Win32 控制台应用程序的经验,但对于 C++ 中的所有类型的程序,情况可能并非如此。)

可能的问题 #2 - 正在编译的位置:今年夏天,我参加了一个编程课程,我们不得不将所有项目存储在 8GB 闪存驱动器上,因为我们使用的实验室中的计算机每晚都在午夜被擦除,这会抹去我们所有的工作。如果您出于便携性/安全性等原因编译到外部存储设备,则可能需要很长时间时间(即使使用我上面提到的预编译头文件)让您的程序编译,特别是如果它是一个相当大的程序。在这种情况下,我对您的建议是在您使用的计算机的硬盘驱动器上创建和编译程序,并且无论何时您想/需要停止处理您的项目,无论出于何种原因,将它们转移到您的外部存储设备,然后单击“安全删除硬件并弹出媒体”图标,该图标应该显示为一个小闪存驱动器,位于一个带有白色复选标记的绿色小圆圈后面,以断开它。

我希望这可以帮助你; 如果有,请告诉我!:)


小智 5

简单地回答这个问题,C++ 是一种比市场上其他语言复杂得多的语言。它有一个遗留的包含模型,可以多次解析代码,并且它的模板库没有针对编译速度进行优化。

\n

语法和 ADL

\n

让我们通过一个非常简单的例子来看看 C++ 的语法复杂性:

\n
x*y;\n
Run Code Online (Sandbox Code Playgroud)\n

虽然您\xe2\x80\x99d 可能会说上面是一个带有乘法的表达式,但在 C++ 中情况不一定如此。如果 x 是类型,则该语句实际上是指针声明。这意味着 C++ 语法是上下文相关的。

\n

这里\xe2\x80\x99是另一个例子:

\n
foo<x> a;\n
Run Code Online (Sandbox Code Playgroud)\n

同样,您可能认为这是 foo 类型的变量“a”的声明,但它也可以解释为:

\n
(foo < x) > a;\n
Run Code Online (Sandbox Code Playgroud)\n

这将使它成为一个比较表达式。

\n

C++ 有一个称为参数相关查找 (ADL) 的功能。ADL 建立了控制编译器如何查找名称的规则。考虑以下示例:

\n
namespace A{\n  struct Aa{}; \n  void foo(Aa arg);\n}\nnamespace B{\n  struct Bb{};\n  void foo(A::Aa arg, Bb arg2);\n}\nnamespace C{ \n  struct Cc{}; \n  void foo(A::Aa arg, B::Bb arg2, C::Cc arg3);\n}\n\nfoo(A::Aa{}, B::Bb{}, C::Cc{});\n
Run Code Online (Sandbox Code Playgroud)\n

ADL 规则规定,我们将考虑函数调用的所有参数来查找名称“foo”。在这种情况下,所有名为 \xe2\x80\x9cfoo\xe2\x80\x9d 的函数都将被考虑进行重载解析。此过程可能需要时间,尤其是在存在大量函数重载的情况下。在模板化环境中,ADL 规则变得更加复杂。

\n

#包括

\n

该命令可能会显着影响编译时间。根据包含的文件类型,预处理器可能仅复制几行代码,也可能复制数千行代码。

\n

此外,该命令无法被编译器优化。如果头文件依赖于宏,您可以复制可以在包含之前修改的不同代码段。

\n

这些问题有一些解决方案。您可以使用预编译头,它们是编译器对头中解析内容的内部表示。然而,如果没有用户的努力,这不能完成,因为预编译头假设头不依赖于宏。

\n

模块功能为这个问题提供了语言级的解决方案。它\xe2\x80\x99s 从 C++20 版本开始可用。

\n

模板

\n

模板的编译速度具有挑战性。每个使用模板的翻译单元都需要包含它们,并且这些模板的定义需要可用。模板的某些实例化最终会成为其他模板的实例化。在某些极端情况下,模板实例化可能会消耗大量资源。使用模板并且不是为编译速度而设计的库可能会变得很麻烦,正如您可以在以下链接提供的元编程库的比较中看到的: http: //metaben.ch/。它们的编译速度差异很大。

\n

如果您想了解为什么某些元编程库的编译时间比其他库更好,请观看有关 Chiel 规则的视频

\n

结论

\n

C++是一种编译缓慢的语言,因为在该语言最初开发时,编译性能并不是最优先考虑的。结果,C++ 最终获得了在运行时可能有效但在编译时不一定有效的功能。

\n

PS \xe2\x80\x93 我在 Incredibuild 工作,这是一家专门加速 C++ 编译的软件开发加速公司,欢迎大家免费试用

\n