与C#和Java相比,编译C ++文件要花费很长时间。与运行正常大小的Python脚本相比,编译C ++文件花费的时间明显更长。我目前正在使用VC ++,但任何编译器都相同。为什么是这样?
我可以想到的两个原因是加载头文件并运行预处理器,但这似乎不能解释为什么要花这么长时间。
It takes significantly longer to compile a C++ file
-是2秒,而不是1秒?当然,它的长度是它的两倍,但意义不大。还是说10分钟而不是5秒?请量化。
与C#和Java相比,编译C ++文件要花费很长时间。与运行正常大小的Python脚本相比,编译C ++文件花费的时间明显更长。我目前正在使用VC ++,但任何编译器都相同。为什么是这样?
我可以想到的两个原因是加载头文件并运行预处理器,但这似乎不能解释为什么要花这么长时间。
It takes significantly longer to compile a C++ file
-是2秒,而不是1秒?当然,它的长度是它的两倍,但意义不大。还是说10分钟而不是5秒?请量化。
Answers:
几个原因
每个单独的编译单元都需要数百个甚至数千个标头来进行加载(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 ++成为更强大的语言,但是它们在编译速度方面也付出了巨大的代价。
减慢速度与任何编译器不一定相同。
我还没有使用过Delphi或Kylix,但是在MS-DOS时代,Turbo Pascal程序几乎可以即时编译,而等效的Turbo C ++程序只是在抓取。
两个主要区别是非常强大的模块系统和允许单遍编译的语法。
毫无疑问,编译速度并不是C ++编译器开发人员的首要任务,但是C / C ++语法还存在一些固有的复杂性,使处理起来更加困难。(我不是C语言方面的专家,但Walter Bright是,在构建了各种商业C / C ++编译器之后,他创建了D语言。他的更改之一是强制执行上下文无关的语法,以使该语言易于解析。 )
另外,您会注意到通常会设置Makefile,以便每个文件都用C单独编译,因此,如果10个源文件都使用相同的包含文件,则该包含文件将处理10次。
解析和代码生成实际上相当快。真正的问题是打开和关闭文件。请记住,即使有了包含保护,编译器仍会打开.H文件,并读取每一行(然后忽略它)。
曾经有一个朋友(在工作时很无聊),拿了他公司的应用程序,并将所有文件(所有源文件和头文件)都放入一个大文件中。编译时间从3小时减少到7分钟。
C ++被编译为机器代码。因此,您具有预处理器,编译器,优化器,最后是汇编器,所有这些都必须运行。
Java和C#被编译为字节代码/ IL,并且Java虚拟机/.NET Framework在执行之前先执行(或将JIT编译为机器代码)。
Python是一种解释型语言,也已编译为字节码。
我敢肯定还有其他原因,但是总的来说,不必编译为本地机器语言可以节省时间。
最大的问题是:
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系统还调用某些程序只是为了获取信息的情况下。
构建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 ++的构建工具都很慢。但是,有更快的工具和方法可以缓解语言的慢速部分。
使用它们需要一点肘油脂,但是好处是不可否认的。更快的构建时间可以使开发人员更快乐,更敏捷,并最终获得更好的代码。
编译语言总是比解释语言需要更大的初始开销。另外,也许您没有很好地构建C ++代码。例如:
#include "BigClass.h"
class SmallClass
{
BigClass m_bigClass;
}
编译速度比:
class BigClass;
class SmallClass
{
BigClass* m_bigClass;
}
减少大型C ++项目中的编译时间的一种简单方法是制作一个* .cpp包含文件,该文件包含项目中的所有cpp文件并进行编译。这样可以将集管爆炸问题减少到一次。这样做的好处是编译错误仍将引用正确的文件。
例如,假设您有一个a.cpp,b.cpp和c.cpp ..创建一个文件:everything.cpp:
#include "a.cpp"
#include "b.cpp"
#include "c.cpp"
然后仅通过制作all.cpp来编译项目
原因如下:
1)C ++语法比C#或Java更复杂,并且需要更多的时间来解析。
2)(更重要的是)C ++编译器生成机器代码,并在编译过程中进行所有优化。C#和Java仅进行一半,而将这些步骤留给JIT。
我想到的两个问题可能会影响C ++程序的编译速度。
可能的问题#1-编译标题:此问题可能已经解决,也可能尚未通过其他答案或注释解决。)Microsoft Visual C ++(AKA VC ++)支持预编译的标头,我强烈建议这样做。当您创建一个新项目并选择要制作的程序类型时,安装向导窗口将出现在屏幕上。如果单击其底部的“下一步>”按钮,则窗口将带您进入具有多个功能列表的页面;确保选中“预编译头”选项旁边的框。(注意:这是我在C ++中使用Win32控制台应用程序的经验,但是对于C ++中的所有类型的程序来说可能并非如此。)
可能的问题2-位置被限制为:今年夏天,我参加了编程课程,由于我们实验室中的计算机每天晚上午夜都被抹掉,所以我们不得不将所有项目存储在8GB闪存驱动器上,这会抹去我们所有的工作。如果出于便携性/安全性等原因而编译到外部存储设备,则可能需要很长时间时间(甚至使用我上面提到的预编译头文件)进行编译,尤其是在程序很大的情况下。在这种情况下,我的建议是在正在使用的计算机的硬盘驱动器上创建和编译程序,并且无论何时出于任何原因而希望/需要停止对项目进行处理,都应将其传输到外部单击存储设备,然后单击“安全删除硬件并弹出媒体”图标,以断开连接,该图标应显示为一个小的闪存驱动器,在绿色圆圈后面带有白色复选标记。
我希望这可以帮助你; 让我知道是否可以!:)