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

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

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

几个原因:

  • 头文件:每一个编译单元需要成百上千甚至上千个头被加载,2:编译。 它们中的每一个通常都必须针对每个编译单元重新编译,因为预处理器确保在每个编译单元之间编译头的结果可能不同。 (可以在一个编辑单元中定义一个macros来改变头部的内容)。

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

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

  • parsing:parsing的语法非常复杂,很大程度上依赖于上下文,而且很难消除歧义。 这需要很多时间

  • 模板:在C#中, List<T>是编译的唯一types,不pipe在程序中有多less个List实例。 在C ++中, vector<int>vector<float> vector<int>是一个完全独立的types,每个都必须单独编译。

    除此之外,模板构成了编译器必须解释的完整的图灵完整的“子语言”,这可能变得非常复杂。 即使相对简单的模板元编程代码也可以定义recursion模板,创build几十个和几十个模板实例。 模板也可能会导致非常复杂的types,名字很可笑,给连接器增加了很多额外的工作。 (它必须比较很多符号名称,如果这些名称可以变成几千个字符,那么可能会变得相当昂贵)。

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

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

    而且,C ++程序必须由编译器完全优化。 AC#程序可以依靠JIT编译器在加载时执行额外的优化,C ++没有得到任何这样的“第二次机会”。 编译器产生的东西就像它将要得到的那样优化。

  • 机器代码: C ++被编译成机器代码,可能比Java或.NET使用的字节代码复杂一些(特别是在x86的情况下)。
    (这是因为完整性而提到的,只是因为在评论中提到了这一点,实际上,这一步不太可能占用编译时间的一小部分)。

这些因素中的大部分都由C代码共享,实际上编译效率很高。 C ++中的parsing步骤要复杂得多,而且可能占用更多时间,但主要的犯罪者可能是模板。 它们是有用的,并且使得C ++成为一种更为强大的语言,但是它们在编译速度方面也付出了代价。

任何编译器的放缓都不一定一样。

我没有使用Delphi或Kylix,但回到MS-DOS时代,Turbo Pascal程序几乎可以立即编译,而相当于Turbo C ++的程序只能抓取。

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

对于C ++编译器开发人员来说,编译速度当然不是优先考虑的事情,但C / C ++语法中也存在一些固有的复杂性,使得处理起来更加困难。 (我不是C方面的专家,但是Walter Bright是在创build了各种商业C / C ++编译器之后,创build了D语言。 他的一个变化是强制使用上下文无关的语法来使语言更易于parsing。)

另外,你会注意到,一般来说Makefile文件都是用C语言编译的,所以如果10个源文件都使用相同的include文件,那么这个include文件会被处理10次。

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

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

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

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

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

我相信还有其他的原因,但是一般来说,不需要编译为本地机器语言就可以节省时间。

另一个原因是使用C预处理器来定位声明。 即使有头卫士,.h每次被包括在内时,仍然需要一遍又一遍地parsing。 一些编译器支持预编译头文件,可以帮助解决这个问题,但并不总是使用它们。

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

最大的问题是:

1)无限的头部重新parsing。 已经提到。 缓解(像#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的一种最糟糕的情况(创build过程很慢)。 比较Windows和* nix之间的GCC编译时间是非常明显的,特别是如果make / configure系统也调用一些程序来获取信息。

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

 #include "BigClass.h" class SmallClass { BigClass m_bigClass; } 

编译比以下慢很多:

 class BigClass; class SmallClass { BigClass* m_bigClass; } 

build设C / C ++:究竟发生了什么,为什么这么长时间

软件开发时间的相当大一部分并不花费在编写,运行,debugging甚至devise代码上,而是等待编译完成。 为了让事情变得更快,我们首先必须了解C / C ++软件编译时发生了什么。 步骤大致如下:

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

现在我们将更详细地看一下每一步,重点关注如何使它们变得更快。

组态

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

这一步发生的比较less见。 只需要在更改configuration或更改构buildconfiguration时运行。 在改变构build系统的情况下,为了更快地实现这一步,没有太多的工作要做。

构build工具启动

当你运行make或者点击IDE上的构build图标时(这通常是make的别名),会发生这种情况。 构build工具二进制文件启动并读取其configuration文件以及构buildconfiguration,这通常是相同的事情。

根据构build的复杂性和大小,这可能需要几秒到几秒的时间。 本身就不会那么糟糕。 不幸的是,大多数基于make的构build系统在每个构build中都会导致调用数十到数百次。 通常这是由recursion使用make(这是不好的)造成的。

应该注意的是,Make非常慢的原因不是一个执行错误。 Makefiles的语法有一些怪癖,使得一个非常快速的实现几乎不可能。 与下一步相结合时,这个问题更加明显。

依赖性检查

构build工具读取其configuration后,必须确定哪些文件已经更改,哪些需要重新编译。 configuration文件包含描述构build依赖关系的有向非循环图。 此图通常在configuration步骤中构build。 构build工具启动时间和依赖性扫描程序在每个构build版本上运行。 它们的组合运行时间决定了编辑 – 编译 – debugging周期的下限。 这个小项目通常是几秒钟左右。 这是可以忍受的。 有替代品。 其中最快的是由Google工程师为Chromium制作的Ninja。 如果你正在使用CMake或Gyp来构build,只需切换到他们的忍者后端。 你不需要改变构build文件本身的任何东西,只是享受速度提升。 忍者不是在大多数发行版上打包,所以你可能需要自己安装。

汇编

在这一点上,我们最终调用编译器。 切割一些angular落,这里是采取的大致步骤。

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

与stream行的观点相反,编译C ++并不是那么慢。 STL很慢,大多数用于编译C ++的构build工具都很慢。 但是,有更快的工具和方法来缓解语言的慢速部分。

使用它们需要一点点的润滑脂,但好处是不可否认的。 更快的构build时间带来更快乐的开发人员,更敏捷,最终更好的代码。

一些原因是:

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

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

你得到的权衡是程序运行速度稍快一点。 在开发过程中,这可能会让你感到很冷,但是一旦开发完成,这个问题就会变得很重要,而且这个程序只是由用户来运行。

大多数答案都不太清楚,提到C#总是会运行得更慢,因为在编译时只执行一次C ++执行的操作的代价,由于运行时依赖关系,性能成本也受到影响(要加载更多的东西运行),更不用说C#程序将总是有更高的内存占用,所有导致性能与硬件的可用性更密切相关。 其他解释或依赖于虚拟机的语言也是如此。

在较大的C ++项目中减less编译时间的简单方法是创build一个* .cpp包含文件,其中包含项目中的所有cpp文件并编译该文件。 这将头部爆炸问题减less到一次。 这样做的好处是编译错误仍然会引用正确的文件。

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

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

然后通过制作everything.cpp来编译这个项目

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

可能的问题#1 – 编译头文件:(这可能已经或可能不会被其他答案或评论解决。)Microsoft Visual C ++(AKA VC ++)支持预编译头文件,我强烈build议。 当您创build一个新项目并select正在制作的程序types时,屏幕上应显示一个设置向导窗口。 如果您点击底部的“下一步>”button,该窗口将带您进入一个有几个function列表的页面; 确保选中“预编译头”选项旁边的框。 (注意:这是我在C ++中使用Win32控制台应用程序的经验,但对于C ++中的各种程序,情况可能并非如此)。

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

我希望这可以帮助你; 让我知道如果它! 🙂

正如已经评论的,编译器花费了大量的时间来实例化和实例化模板。 在这样的情况下,有些项目专注于特定的项目,并且在某些真正有利的情况下声称可观察到的30倍加速。 见http://www.zapcc.com