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


540

与C#和Java相比,编译C ++文件要花费很长时间。与运行正常大小的Python脚本相比,编译C ++文件花费的时间明显更长。我目前正在使用VC ++,但任何编译器都相同。为什么是这样?

我可以想到的两个原因是加载头文件并运行预处理器,但这似乎不能解释为什么要花这么长时间。


58
VC ++支持预编译头。使用它们会有所帮助。很多。
布赖恩

1
是的,在我的情况下(大多数情况下为C,带有一些类-没有模板),预编译的标头可将速度提高约10倍
Lothar

@Brian我永远不会在库中使用预编译的头
Cole Johnson

13
It takes significantly longer to compile a C++ file-是2秒,而不是1秒?当然,它的长度是它的两倍,但意义不大。还是说10分钟而不是5秒?请量化。
尼克·加蒙

2
我押注模块。我不认为C ++项目的构建速度会比仅使用模块的其他编程语言快,但是对于大多数具有某种管理能力的项目而言,它确实可以接近。我希望在模块之后能看到一个具有人工集成功能的优秀软件包管理器
Abdurrahim

Answers:


800

几个原因

头文件

每个单独的编译单元都需要数百个甚至数千个标头来进行加载(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 ++中,解析步骤要复杂得多,并且可能要花费更多的时间,但是主要的冒犯者可能是模板。它们很有用,并使C ++成为更强大的语言,但是它们在编译速度方面也付出了巨大的代价。


38
关于点3:C编译明显比C ++快。绝对是导致速度降低的前端,而不是代码生成。
汤姆(Tom)”

72
关于模板:不仅vector <int>必须与vector <double>分开编译,而且vector <int>在使用它的每个编译单元中都重新编译。链接器消除了冗余定义。
大卫·罗德里格斯(DavidRodríguez)-德里贝斯

15
dribeas:是的,但这不是专门针对模板的。内联函数或标头中定义的任何其他内容将在包含它的任何地方重新编译。但是,是的,使用模板尤其麻烦。:)
jalf

15
@configurator:Visual Studio和gcc都允许使用预编译的头文件,这可以使编译速度大大提高。
small_duck

5
不确定优化是否是问题所在,因为我们的DEBUG版本实际上比发布模式版本慢。pdb生成也是罪魁祸首。
gast128

40

减慢速度与任何编译器不一定相同。

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

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

毫无疑问,编译速度并不是C ++编译器开发人员的首要任务,但是C / C ++语法还存在一些固有的复杂性,使处理起来更加困难。(我不是C语言方面的专家,但Walter Bright是,在构建了各种商业C / C ++编译器之后,他创建了D语言。他的更改之一是强制执行上下文无关的语法,以使该语言易于解析。 )

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


38
比较Pascal很有意思,因为Niklaus Wirth在设计他的语言和编译器时会花费编译器自行编译的时间作为基准。有一个故事,在仔细编写了用于快速符号查找的模块后,他用简单的线性搜索替换了它,因为减小的代码大小使编译器自身的编译速度更快。
Dietrich Epp

1
@DietrichEpp经验主义得到了回报。
Tomas Zubiri

39

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

曾经有一个朋友(在工作时很无聊),拿了他公司的应用程序,并将所有文件(所有源文件和头文件)都放入一个大文件中。编译时间从3小时减少到7分钟。


14
好吧,文件访问肯定有帮助,但是正如jalf所说,主要原因是其他原因,即重复解析很多(很多!嵌套的)头文件,在您的情况下完全会消失。
康拉德·鲁道夫,

9
正是在这一点上,您的朋友需要设置预编译的头文件,打破不同头文件之间的依赖关系(尝试避免一个头文件包含另一个头文件,而是向前声明),并获得更快的HDD。除此之外,还有一个非常惊人的指标。
汤姆·莱斯

6
如果整个头文件(可能的注释和空行除外)都在头保护范围之内,则gcc可以记住该文件,如果定义了正确的符号,则可以跳过该文件。
CesarB

11
解析很重要。对于N个具有相互依赖性的大小相似的源/头文件对,头文件中有O(N ^ 2)个传递。将所有文本放入单个文件将减少重复的分析。
汤姆(Tom)”

9
小附注:包含防护措施可防止每个编译单元进行多次解析。总体上不反对多重解析。
Marco van de Voort,2012年

16

另一个原因是使用C预处理器来定位声明。即使有了头文件后缀,.h仍然必须一遍又一遍地进行解析。一些编译器支持预编译的标头可以帮助您解决此问题,但是并不总是使用它们。

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


我认为您应该将预编译标题的注释加粗,以指出答案的这一重要部分。
凯文

6
如果整个头文件(可能的注释和空行除外)都在头保护范围之内,则gcc可以记住该文件,并且如果定义了正确的符号,则可以跳过该文件。
CesarB

5
@CesarB:它仍然必须每个编译单元(.cpp文件)全部处理一次。
山姆·哈威尔

16

C ++被编译为机器代码。因此,您具有预处理器,编译器,优化器,最后是汇编器,所有这些都必须运行。

Java和C#被编译为字节代码/ IL,并且Java虚拟机/.NET Framework在执行之前先执行(或将JIT编译为机器代码)。

Python是一种解释型语言,也已编译为字节码。

我敢肯定还有其他原因,但是总的来说,不必编译为本地机器语言可以节省时间。


15
预处理所增加的成本是微不足道的。造成速度变慢的主要原因是将编译分为单独的任务(每个目标文件一个),因此,常见的标头会一遍又一遍地处理。与大多数其他语言的O(N)解析时间相比,这是O(N ^ 2)最坏的情况。
汤姆”

12
从同一论点可以看出,C,Pascal等编译器的运行速度很慢,这在平均水平上是不正确的。它更多地与C ++的语法和C ++编译器必须维护的巨大状态有关。
塞巴斯蒂安·马赫,

2
C很慢。它遭受与接受的解决方案相同的报头解析问题。例如,以一个简单的Windows GUI程序为例,该程序在几个编译单元中包含windows.h,并在您添加(简短)编译单元时评估编译性能。
Marco van de Voort 2014年

13

最大的问题是:

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系统还调用某些程序只是为了获取信息的情况下。


12

构建C / C ++:实际发生的事情以及为什么要花这么长时间

相当大部分的软件开发时间都没有花费在编写,运行,调试甚至设计代码上,而是等待其完成编译。为了使事情更快,我们首先必须了解编译C / C ++软件时发生的情况。步骤大致如下:

  • 组态
  • 构建工具启动
  • 依赖检查
  • 汇编
  • 连结中

现在,我们将更详细地研究每个步骤,重点在于如何使它们更快。

组态

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

此步骤很少发生。仅在更改配置或更改构建配置时才需要运行它。由于没有更改构建系统,因此没有什么要做的可以使此步骤更快。

构建工具启动

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

根据构建的复杂性和大小,此过程可能花费几分之一秒到几秒钟的时间。就其本身而言,这还不错。不幸的是,大多数基于make的构建系统会导致每次构建都要动用数十到数百次make。通常这是由于对make的递归使用造成的(不好)。

应该注意的是,Make这么慢的原因不是实现错误。Makefile的语法有一些古怪之处,这些古怪之处使实现真正快速的实现几乎是不可能的。与下一步结合使用时,此问题甚至更明显。

依赖检查

一旦构建工具读取了它的配置,它就必须确定哪些文件已更改以及哪些文件需要重新编译。配置文件包含描述构建依赖关系的有向无环图。该图通常是在配置步骤中构建的。构建工具的启动时间和依赖项扫描程序在每个单独的构建上运行。它们的组合运行时确定了edit-compile-debug周期的下限。对于小型项目,此时间通常为几秒钟左右。这是可以容忍的。有一些替代方法。其中最快的是Ninja,这是由Google工程师为Chromium构建的。如果您使用CMake或Gyp进行构建,则只需切换到其Ninja后端即可。您无需更改构建文件本身的任何内容,只需享受速度的提升。但是,大多数发行版中并未打包忍者,

汇编

至此,我们终于调用了编译器。偷工减料,这里是大概的步骤。

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

与普遍的看法相反,编译C ++实际上并没有那么慢。STL很慢,并且大多数用于编译C ++的构建工具都很慢。但是,有更快的工具和方法可以缓解语言的慢速部分。

使用它们需要一点肘油脂,但是好处是不可否认的。更快的构建时间可以使开发人员更快乐,更敏捷,并最终获得更好的代码。


9

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

#include "BigClass.h"

class SmallClass
{
   BigClass m_bigClass;
}

编译速度比:

class BigClass;

class SmallClass
{
   BigClass* m_bigClass;
}

3
如果BigClass恰好包含另外5个使用的文件,并最终在程序中包含所有代码,则尤其如此。
汤姆·莱斯

7
这也许是原因之一。但是例如Pascal只需花费等效C ++程序所需的十分之一的编译时间。这不是因为gcc:s优化花费的时间更长,而是因为Pascal更易于解析并且不必使用预处理程序。另请参阅Digital Mars D编译器。
Daniel O

2
解析不是更容易,而是模块化避免了重新解释每个编译单元的windows.h和许多其他标头。是的,Pascal解析起来更容易(尽管像Delphi这样的成熟的解析器再次变得更加复杂),但这并不是什么大的区别。
Marco van de Voort 2013年

1
此处显示的可提高编译速度的技术称为前向声明
DavidRR

在一个文件中编写类。会不会是凌乱的代码?
Fennekin

7

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

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

#include "a.cpp"
#include "b.cpp"
#include "c.cpp"

然后仅通过制作all.cpp来编译项目


3
我看不到对此方法的反对。假设您是从脚本或Makefile生成include的,那么这不是维护问题。实际上,它确实可以加快编译速度,而不会混淆编译问题。您可以争辩说编译时会消耗内存,但这在现代计算机上很少出现问题。那么,这种方法的目标是什么(除了断言它是错误的)?
rileyberton

9
@rileyberton(因为有人反对您的评论)让我说清楚:不,它不会加快编译速度。实际上,它通过隔离翻译单元来确保任何编译都花费最大的时间。关于他们的伟大的事情是,你并不需要,如果他们不改变重新编译所有的.cpp-S。(这不考虑样式论据)。适当的依赖项管理以及可能的预编译头要好得多。
sehe

7
抱歉,但这可能是加快编译速度的非常有效的方法,因为您(1)几乎消除了链接,并且(2)只需要处理一次常用的标头。此外,如果您不愿意尝试,它实际上也可以工作。不幸的是,它使增量重建成为不可能,因此每个构建都是完全从头开始的。但是,用这种方法完全重建很多比你会得到更快,否则
jalf

4
@BartekBanachewicz当然可以,但是您所说的是“它没有加快编译速度”,没有限定符。如您所说,它使每个编译都花费最大的时间(不进行部分重建),但是与此同时,与其他情况相比,它极大地减少了最大时间。我只是说这是一个有点超过细致入微“不这样做”
jalf

2
玩弄静态变量和函数。如果我想要一个大的编译单元,我将创建一个大的.cpp文件。
gnasher729 2014年

6

原因如下:

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

2)(更重要的是)C ++编译器生成机器代码,并在编译过程中进行所有优化。C#和Java仅进行一半,而将这些步骤留给JIT。


5

您得到的折衷是该程序运行速度更快。在开发过程中,这可能给您带来凉意,但是一旦开发完成,并且该程序仅由用户运行,这可能会非常重要。


3

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


3

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

可能的问题#1-编译标题:此问题可能已经解决,也可能尚未通过其他答案或注释解决。)Microsoft Visual C ++(AKA VC ++)支持预编译的标头,我强烈建议这样做。当您创建一个新项目并选择要制作的程序类型时,安装向导窗口将出现在屏幕上。如果单击其底部的“下一步>”按钮,则窗口将带您进入具有多个功能列表的页面;确保选中“预编译头”选项旁边的框。(注意:这是我在C ++中使用Win32控制台应用程序的经验,但是对于C ++中的所有类型的程序来说可能并非如此。)

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

我希望这可以帮助你; 让我知道是否可以!:)

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.