关于性能:为什么C ++编译需要这么长时间?

Why does C++ compilation take so long?

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

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


若干原因

头文件

每个编译单元都需要(1)加载甚至(2)编译成百上千的头文件。它们中的每一个通常都必须为每个编译单元重新编译,因为预处理器确保编译头的结果可能在每个编译单元之间有所不同。(宏可以在一个修改头内容的编译单元中定义)。

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

链接

编译后,所有对象文件都必须链接在一起。这基本上是一个不能很好并行化的整体过程,必须处理整个项目。

句法分析

语法分析极其复杂,严重依赖上下文,而且很难消除歧义。这需要很多时间。

模板

在C_中,List是唯一编译的类型,不管程序中有多少个列表实例。在C++中,EDCOX1×1是完全独立的类型,与EDCOX1,2,每个都必须单独编译。

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

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

优化

C++允许一些非常戏剧性的优化。C或Java不允许类被完全消除(它们必须在那里进行反射),但是,即使是一个简单的C++模板元程序也可以很容易地生成几十个或几百个类,所有这些都在优化阶段被内联并再次消除。

此外,编译器必须完全优化C++程序。C程序可以依赖于JIT编译器在加载时执行其他优化,C++没有得到这样的"第二次机会"。编译器生成的是尽可能优化的。

机器

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

结论

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


对于任何编译器来说,这种减速并不一定是相同的。

我还没有使用Delphi或Kylix,但回到MS-DOS时代,Turbo PASCAL程序几乎可以即时编译,而等效的Turbo C++程序则只是爬行。

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

编译速度当然不是C++编译器开发人员的首要任务,但是C/C++语法中也存在一些固有的复杂性,这使得处理过程变得更加困难。(我不是C的专家,但Walter Bright是,并且在建立各种商业C/C++编译器之后,他创建了D语言。他的其中一个改变是强制执行上下文无关的语法,以使语言更容易解析。)

另外,您会注意到,通常会设置makefiles,以便在C中分别编译每个文件,因此如果10个源文件都使用相同的include文件,那么include文件将被处理10次。


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

有一次,一个朋友(在工作中感到无聊)拿起他的公司的应用程序,把所有的东西——所有的源文件和头文件——放进一个大文件中。编译时间从3小时下降到7分钟。


另一个原因是使用C预处理器查找声明。即使有了头罩,.h仍然需要反复分析,每次都包括在内。有些编译器支持预编译的头文件,这有助于实现这一点,但并不总是使用它们。

参见:C++经常质疑答案


C++被编译成机器代码。所以您有预处理器、编译器、优化器,最后还有汇编程序,所有这些都必须运行。

Java和C语言被编译成字节代码/IL,Java虚拟机/.NET框架在执行之前执行(或JIT编译成机器代码)。

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

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


最大的问题是:

1)无限头修复。已经提到了。缓解措施(如pragma once)通常只对每个编译单元有效,而不是对每个构建有效。

2)工具链通常被分为多个二进制文件(make、预处理器、编译器、汇编程序、archiver、impdef、linker和dlltool,在极端情况下),每个调用(编译器、汇编程序)或每对文件(archiver、linker和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系统也调用一些程序只是为了获取信息时,这一点非常明显。


建筑C/C++:究竟发生了什么,为什么要花这么长时间?

软件开发的大部分时间不是花在编写、运行、调试甚至设计代码上,而是等待代码完成编译。为了使事情快速进行,我们首先必须了解当C/C++软件被编译时发生了什么。步骤大致如下:

  • 配置
  • 生成工具启动
  • 依赖项检查
  • 汇编
  • 链接

现在,我们将更详细地研究每一步,重点是如何更快地完成这些步骤。

配置

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

这一步的发生率相对较低。它只需要在更改配置或更改生成配置时运行。除了更改构建系统之外,没有太多工作可以使这一步更快。

生成工具启动

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

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

应该注意的是,make这么慢的原因并不是一个实现错误。makefiles的语法有一些奇怪之处,使得真正快速的实现几乎是不可能的。当结合下一步时,这个问题更加明显。

依赖项检查

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

汇编

此时,我们最终调用编译器。切一些角,下面是所采取的大致步骤。

  • 合并包含
  • 正在分析代码
  • 代码生成/优化

与流行的信念相反,编译C++实际上并不是那么缓慢。STL速度慢,用于编译C++的大多数构建工具都很慢。然而,有更快的工具和方法来减轻语言的缓慢部分。

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


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

1
2
3
4
5
6
#include"BigClass.h"

class SmallClass
{
   BigClass m_bigClass;
}

编译速度比:

1
2
3
4
5
6
class BigClass;

class SmallClass
{
   BigClass* m_bigClass;
}


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

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

1
2
3
#include"a.cpp"
#include"b.cpp"
#include"c.cpp"

然后通过生成everything.cpp编译项目


你得到的折中办法是程序运行得快一点。在开发过程中,这对您来说可能是一种冷冰冰的安慰,但是一旦开发完成,并且程序只是由用户运行,这就非常重要了。


一些原因是:

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

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


大多数的答案都有点不清楚,因为C的运行速度总是慢,因为在编译时只执行一次C++的操作,这个性能开销也会因为运行时的依赖性而受到影响(更多的东西要加载才能运行),更不用说C语言程序总是会有更高的内存占用。所有这些都会导致性能与可用硬件的性能更为密切相关。其他解释或依赖虚拟机的语言也是如此。


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

可能的问题1编译标题:(这可能已经或可能还没有被另一个答案或评论解决)。微软Visual C++(A.K.V++)支持预编译头,这是我高度推荐的。当您创建一个新项目并选择正在制作的程序类型时,屏幕上会出现一个安装向导窗口。如果您点击它底部的"下一步>"按钮,窗口将带您进入一个具有多个功能列表的页面;请确保选中"预编译头"选项旁边的框。(注:这是我在C++中使用Win32控制台应用程序的经验,但C++中的各种程序可能不是这样的。)

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

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


正如已经评论过的,编译器花费了大量时间重新实例化和实例化模板。在这样一个范围内,有一些项目将重点放在特定的项目上,并声称在一些真正有利的情况下可以观察到30倍的加速。请访问http://www.zapcc.com。