与c#和Java相比,编译c++文件需要很长时间。编译一个c++文件比运行一个正常大小的Python脚本花费的时间要长得多。我目前使用vc++,但它与任何编译器是一样的。为什么会这样?
我能想到的两个原因是加载头文件和运行预处理器,但这似乎不能解释为什么需要这么长时间。
一些原因是:
1) c++语法比c#或Java更复杂,需要更多的时间来解析。
2)(更重要的是)c++编译器生成机器代码,并在编译期间进行所有优化。c#和Java只走了一半,将这些步骤留给JIT。
c++被编译成机器代码。所以你有预处理器,编译器,优化器,最后是汇编器,所有这些都必须运行。
Java和c#被编译成字节码/IL, Java虚拟机/。NET框架执行(或JIT编译成机器代码)之前执行。
Python是一种解释型语言,它也被编译成字节码。
我相信还有其他原因,但总的来说,不需要编译为本机机器语言可以节省时间。
另一个原因是使用C预处理器来定位声明。即使使用了头保护,.h仍然必须在每次包含它们时被反复解析。一些编译器支持预编译的头文件,可以帮助解决这个问题,但它们并不总是被使用。
参见:c++常见问题解答
编译语言总是比解释语言需要更大的初始开销。此外,也许您没有很好地组织您的c++代码。例如:
#include "BigClass.h" class SmallClass { BigClass m_bigClass; }
编译速度比:
class BigClass; class SmallClass { BigClass* m_bigClass; }
几个原因
List<T>
vector<int>
vector<float>
任何编译器的减速都不一定相同。
我没有使用过Delphi或Kylix,但在MS-DOS时代,Turbo Pascal程序几乎可以立即编译,而等效的Turbo c++程序只能爬行。
两个主要的区别是一个非常强大的模块系统和允许单次编译的语法。
编译速度当然可能不是c++编译器开发人员的优先考虑事项,但C/ c++语法中也有一些固有的复杂性,这使得处理起来更加困难。(我不是C方面的专家,但Walter Bright是,在构建了各种商业C/ c++编译器之后,他创建了D语言。他的一个变化是为了加强上下文无关的语法,使语言更容易解析。)
此外,您还会注意到,makefile通常设置为每个文件都单独用C编译,因此如果10个源文件都使用相同的包含文件,则该包含文件将被处理10次。
解析和代码生成实际上相当快。真正的问题是打开和关闭文件。记住,即使使用include守卫,编译器仍然打开. h文件,读取每一行(然后忽略它)。
有一次,我的一个朋友(在工作无聊的时候)把他公司的应用程序——所有的源文件和头文件——放到一个大文件中。编译时间从3小时下降到7分钟。
你得到的代价是程序运行得稍微快一点。在开发期间,这对您来说可能是一种冷漠的安慰,但一旦开发完成,并且程序只是由用户运行时,它就会变得非常重要。
最大的问题是:
1)无限头解析。已经提到过。缓解(如#pragma once)通常只适用于每个编译单元,而不是每个构建。
2)事实上,工具链经常被分离成多个二进制文件(make、预处理器、编译器、汇编器、归档器、impdef、链接器和dll工具),这些二进制文件必须在每次调用(编译器、汇编器)或每一对文件(归档器、链接器和dll工具)时重新初始化和重新加载所有状态。
另请参阅关于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#程序总是会有更高的内存占用,所有这些都导致性能与可用硬件的能力更密切相关。对于其他解释性语言或依赖于VM的语言也是如此。
在大型c++项目中减少编译时间的一个简单方法是创建一个包含项目中所有cpp文件的*.cpp包含文件并编译该文件。这将头爆炸问题减少到一次。这样做的好处是,编译错误仍然会引用正确的文件。
例如,假设你有a.cpp, b.cpp和c.cpp。创建一个文件:everything.cpp:
#include "a.cpp" #include "b.cpp" #include "c.cpp"
然后通过将everything.cpp编译项目
构建C/ c++:到底发生了什么,为什么花了这么长时间
现在,我们将更详细地查看每个步骤,重点关注如何使它们更快。
配置
这是开始构建的第一步。通常意味着运行配置脚本或CMake、Gyp、SCons或其他工具。对于非常大的基于autotools的配置脚本,这可能需要一秒钟到几分钟的时间。
这一步很少发生。它只需要在更改配置或更改构建配置时运行。如果不改变构建系统,就没有多少事情可以加快这一步。
构建工具启动
这是在IDE上运行make或单击构建图标(通常是make的别名)时发生的情况。构建工具二进制文件启动并读取其配置文件以及构建配置,这通常是同一件事。
根据构建的复杂性和大小,这可能需要几秒到几秒的时间。这本身并没有那么糟糕。不幸的是,大多数基于make的构建系统在每次构建时都会调用几十到几百次make。这通常是由递归使用make(这是不好的)引起的。
应该注意的是,Make如此缓慢的原因并不是实现错误。Makefiles的语法有一些怪癖,使得真正快速的实现几乎不可能。当与下一步结合使用时,这个问题会更加明显。
依赖项检查
编译
此时,我们最终调用编译器。省事起见,以下是大致采取的步骤。
与流行的观点相反,编译c++实际上并没有那么慢。STL很慢,大多数用于编译c++的构建工具都很慢。然而,有更快的工具和方法来减轻语言中缓慢的部分。
使用它们需要一些体力,但好处是不可否认的。更快的构建时间会让开发人员更快乐,更敏捷,并最终产生更好的代码。
我能想到有两个问题可能会影响c++程序的编译速度。
可能的问题#1 -编译头文件:(这可能已经被另一个答案或评论解决了,也可能没有。)Microsoft Visual c++ (A.K.A. vc++)支持预编译头文件,我强烈推荐。当您创建一个新项目并选择要制作的程序类型时,屏幕上将出现一个设置向导窗口。如果你点击它底部的“Next >”按钮,窗口将带你进入一个有几个功能列表的页面;确保“Precompiled header”选项旁边的复选框被选中。(注:这是我使用c++中的Win32控制台应用程序的经验,但这可能不适用于c++中的所有程序。)
今年夏天,我上了一门编程课程,我们必须把所有的项目都存储在8GB的闪存驱动器上,因为我们实验室里的电脑每天午夜都会被擦除,这将会擦除我们所有的工作。如果你是出于可移植性/安全性等考虑而编译到外部存储设备。,它可能需要很长时间的时间(即使使用我上面提到的预编译头)来编译您的程序,特别是如果它是一个相当大的程序。在这种情况下,我给你的建议是在你正在使用的计算机的硬盘上创建和编译程序,无论什么时候你想要/需要停止你的项目,无论什么原因,将它们转移到你的外部存储设备,然后点击“安全移除硬件和弹出媒体”图标,它应该出现在一个小的闪存驱动器后面,上面有一个白色的检查标记,断开它。
我希望这对你有帮助;如果有,请告诉我!:)
在大型面向对象项目中,重要的原因是c++很难限制依赖关系。
私有函数需要在它们各自的类的public头文件中列出,这使得依赖关系比它们需要的更具传递性(传染性):
// Ugly private dependencies #include <map> #include <list> #include <chrono> #include <stdio.h> #include <Internal/SecretArea.h> #include <ThirdParty/GodObjectFactory.h> class ICantHelpButShowMyPrivatePartsSorry { public: int facade(int); private: std::map<int, int> implementation_detail_1(std::list<int>); std::chrono::years implementation_detail_2(FILE*); Intern::SecretArea implementation_detail_3(const GodObjectFactory&); };
如果这个模式被愉快地重复到头的依赖树中,这将倾向于创建一些“神头”。间接地包括项目中所有头文件的大部分。它们和上帝的对象一样无所不知,只是在绘制它们的包含树之前,这一点并不明显。
这会以两种方式增加编译时间:
是的,有一些缓解措施,比如前向声明、哪个已经察觉到了缺点或pimpl成语,这是非零成本抽象。尽管c++在你能做的事情上是无限的,但如果你偏离了它的本意,你的同事会想知道你到底在吸什么。
最糟糕的是:如果你仔细想想,在它们的公共头中声明私有函数的需求甚至是不必要的:成员函数的道德等效可以在C中被模仿,而且通常也被模仿,这不会重现这个问题。
简单地回答这个问题,c++是一种比市场上其他可用语言复杂得多的语言。它有一个遗留的包含模型,可以多次解析代码,并且它的模板库没有针对编译速度进行优化。
语法和ADL
让我们通过一个非常简单的例子来看看c++的语法复杂性:
x*y;
虽然你可能会说上面是一个带有乘法的表达式,但在c++中不一定是这样。如果x是一个类型,那么该语句实际上是一个指针声明。这意味着c++语法是上下文敏感的。
下面是另一个例子:
foo<x> a;
同样,您可能认为这是变量"a"的声明;类型为foo,但它也可以被解释为:
(foo < x) > a;
这将使它成为比较表达式。
c++有一个叫做参数依赖查找(ADL)的特性。ADL建立规则来控制编译器如何查找名称。考虑下面的例子:
namespace A{ struct Aa{}; void foo(Aa arg); } namespace B{ struct Bb{}; void foo(A::Aa arg, Bb arg2); } namespace C{ struct Cc{}; void foo(A::Aa arg, B::Bb arg2, C::Cc arg3); } foo(A::Aa{}, B::Bb{}, C::Cc{});
ADL规则规定,我们将寻找名称“;food "考虑函数调用的所有参数。在这种情况下,将考虑所有名为“foo”的函数进行重载解析。这个过程可能需要时间,特别是如果有很多函数重载。在模板化上下文中,ADL规则变得更加复杂。
# include
这个命令可能会极大地影响编译时间。根据所包含文件的类型,预处理器可能只复制几行代码,也可能复制数千行。
此外,编译器不能优化此命令。如果头文件依赖于宏,则可以复制可以在包含前修改的不同代码段。
对于这些问题,有一些解决方案。您可以使用预编译头文件,这是编译器在头文件中解析内容的内部表示。然而,这离不开用户的努力,因为预编译的头文件假定头文件不依赖于宏。
模块特性为这个问题提供了语言级的解决方案。它可以从c++ 20版本开始使用。
模板
模板的编译速度具有挑战性。每个使用模板的翻译单元都需要包含这些模板,并且需要提供这些模板的定义。一些模板的实例化最终在其他模板的实例化中结束。在某些极端情况下,模板实例化可能会消耗大量资源。一个使用模板的库,而不是为编译速度而设计的库可能会变得很麻烦,正如您可以在这个链接中看到的元编程库的比较:http://metaben.ch/。它们在编译速度上的差异是显著的。
如果您想了解为什么一些元编程库在编译时间上比其他库更好,请查看这个关于切尔法则的视频。
结论
c++是一种编译缓慢的语言,因为编译性能在该语言最初开发时并不是最高优先级。结果,c++的特性在运行时可能有效,但在编译时不一定有效。
附注:我在Incredibuild工作,这是一家软件开发加速公司,专注于加速c++编译,欢迎您访问免费试用。