与c#和Java相比,编译c++文件需要很长时间。编译一个c++文件比运行一个正常大小的Python脚本花费的时间要长得多。我目前使用vc++,但它与任何编译器是一样的。为什么会这样?
我能想到的两个原因是加载头文件和运行预处理器,但这似乎不能解释为什么需要这么长时间。
与c#和Java相比,编译c++文件需要很长时间。编译一个c++文件比运行一个正常大小的Python脚本花费的时间要长得多。我目前使用vc++,但它与任何编译器是一样的。为什么会这样?
我能想到的两个原因是加载头文件和运行预处理器,但这似乎不能解释为什么需要这么长时间。
当前回答
另一个原因是使用C预处理器来定位声明。即使使用了头保护,.h仍然必须在每次包含它们时被反复解析。一些编译器支持预编译的头文件,可以帮助解决这个问题,但它们并不总是被使用。
参见:c++常见问题答案
其他回答
在大型面向对象项目中,重要的原因是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&);
};
如果在头文件的依赖树中重复使用这种模式,就会产生一些间接包含项目中大部分头文件的“神头文件”。它们就像上帝对象一样无所不知,只是在绘制它们的包含树之前,这一点并不明显。
这会以两种方式增加编译时间:
它们添加到包含它们的每个编译单元(.cpp文件)的代码量很容易比cpp文件本身多很多倍。从这个角度来看,catch2.hpp是18000行,而大多数人(甚至是ide)开始难以编辑超过1000-10000行的文件。 编辑头文件时必须重新编译的文件数量不包含在依赖它的真实文件集中。
是的,有一些缓解措施,比如前向声明,它有缺点,或者pimpl习惯用法,它是非零成本抽象。尽管c++在你能做的事情上是无限的,但如果你偏离了它的本意,你的同事会想知道你到底在吸什么。
最糟糕的是:如果你仔细想想,在它们的公共头中声明私有函数的需求甚至是不必要的:成员函数的道德等效可以在C中被模仿,而且通常也被模仿,这不会重现这个问题。
c++被编译成机器代码。所以你有预处理器,编译器,优化器,最后是汇编器,所有这些都必须运行。
Java和c#被编译成字节码/IL, Java虚拟机/。NET框架执行(或JIT编译成机器代码)之前执行。
Python是一种解释型语言,它也被编译成字节码。
我相信还有其他原因,但总的来说,不需要编译为本机机器语言可以节省时间。
几个原因
头文件
每个编译单元都需要(1)加载和(2)编译数百甚至数千个头文件。 每个编译单元通常都需要重新编译它们, 因为预处理器确保编译头文件的结果可能在每个编译单元之间有所不同。 (宏可以在一个编译单元中定义,它会改变头文件的内容)。
这可能是主要原因,因为它需要为每个编译单元编译大量的代码, 此外,每个头文件都必须编译多次 (对于包含它的每个编译单元一次)。
链接
一旦编译完成,所有的目标文件都必须链接在一起。 这基本上是一个整体过程,不能很好地并行化,并且必须处理您的整个项目。
解析
语法非常复杂,难以解析,严重依赖上下文,并且很难消除歧义。 这要花很多时间。
模板
在c#中,List<T>是唯一被编译的类型,无论程序中有多少个List实例化。 在c++中,vector<int>和vector<float>是一个完全独立的类型,它们必须分别编译。
Add to this that templates make up a full Turing-complete "sub-language" that the compiler has to interpret, and this can become ridiculously complicated. Even relatively simple template metaprogramming code can define recursive templates that create dozens and dozens of template instantiations. Templates may also result in extremely complex types, with ridiculously long names, adding a lot of extra work to the linker. (It has to compare a lot of symbol names, and if these names can grow into many thousand characters, that can become fairly expensive).
当然,它们加剧了头文件的问题,因为模板通常必须在头文件中定义, 这意味着必须为每个编译单元解析和编译更多的代码。 在纯C代码中,标头通常只包含前向声明,但实际代码很少。 在c++中,几乎所有的代码都驻留在头文件中是很常见的。
优化
c++允许进行一些非常戏剧化的优化。 c#或Java不允许完全消除类(为了反射的目的,它们必须存在), 但即使是一个简单的c++模板元程序也可以很容易地生成几十个或数百个类, 所有这些都在优化阶段被内联并再次消除。
此外,c++程序必须由编译器完全优化。 c#程序可以依赖JIT编译器在加载时执行额外的优化, c++没有任何这样的“第二次机会”。编译器生成的是它将要得到的优化。
机
c++被编译成机器代码,这可能比Java或. net使用的字节码更复杂(特别是在x86的情况下)。 (这只是出于完整性而提到的,因为它是在评论中提到的。 在实践中,这一步所花费的时间不太可能超过编译时间的一小部分)。
结论
这些因素中的大多数是由C代码共享的,这实际上是相当有效的编译。 在c++中,解析步骤要复杂得多,可能会占用更多的时间,但主要的问题可能是模板。 它们很有用,并使c++成为一种更强大的语言,但它们也在编译速度方面付出了代价。
你得到的代价是程序运行得稍微快一点。在开发期间,这对您来说可能是一种冷漠的安慰,但一旦开发完成,并且程序只是由用户运行时,它就会变得非常重要。
解析和代码生成实际上相当快。真正的问题是打开和关闭文件。记住,即使使用include守卫,编译器仍然打开. h文件,读取每一行(然后忽略它)。
有一次,我的一个朋友(在工作无聊的时候)把他公司的应用程序——所有的源文件和头文件——放到一个大文件中。编译时间从3小时下降到7分钟。