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

时间:2008-11-25 18:25:14

标签: c++ performance compilation

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

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

16 个答案:

答案 0 :(得分:762)

有几个原因

头文件

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

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

链接

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

解析

解析时语法极其复杂,在很大程度上取决于上下文,并且很难消除歧义。 这需要很多时间。

模板

在C#中,List<T>是唯一被编译的类型,无论您在程序中有多少个List实例化。 在C ++中,vector<int>是与vector<float>完全独立的类型,每个类型都必须单独编译。

除此之外,模板构成了一个完整的图灵完整的“子语言”,编译器必须解释, 这可能会变得非常复杂。 即使是相对简单的模板元编程代码也可以定义递归模板,这些模板可以创建数十个模板实例。 模板也可能导致极其复杂的类型,名称冗长,为链接器添加了大量额外的工作。 (它必须比较很多符号名称,如果这些名称可以增长到数千个字符,那么这可能会变得相当昂贵)。

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

优化

C ++允许一些非常戏剧性的优化。 C#或Java不允许完全删除类(它们必须用于反射目的), 但即使是简单的C ++模板元程序也可以轻松生成数十个或数百个类, 所有这些都在优化阶段再次内联和消除。

此外,编译器必须完全优化C ++程序。 C#程序可以依赖JIT编译器在加载时执行其他优化, C ++没有获得任何这样的“第二次机会”。编译器生成的内容是最优化的。

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

结论

这些因素中的大多数都是由C代码共享的,而C代码实际上是相当有效地编译的。 解析步骤在C ++中要复杂得多,并且可以占用更多的时间,但主要的攻击者可能是模板。 它们很有用,并且使C ++成为一种更强大的语言,但它们在编译速度方面也会受到影响。

答案 1 :(得分:37)

任何编译器的减速都不一定。

我没有使用Delphi或Kylix但是在MS-DOS时代,Turbo Pascal程序几乎可以立即编译,而等效的Turbo C ++程序只会抓取。

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

编译速度当然不是C ++编译器开发人员的优先考虑因素,但C / C ++语法中也存在一些固有的复杂性,使得处理起来更加困难。 (我不是C的专家,但Walter Bright是,并且在构建各种商业C / C ++编译器之后,他创建了D语言。One of his changes是强制执行无上下文语法以使语言更容易解析。)

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

答案 2 :(得分:35)

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

一位朋友曾经(在工作时感到厌倦),拿走了公司的应用程序并将所有内容 - 所有源文件和头文件 - 放入一个大文件中。编译时间从3小时降至7分钟。

答案 3 :(得分:15)

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

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

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

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

答案 4 :(得分:15)

另一个原因是使用C预处理器来定位声明。即使有标题保护,每次包含它们时,仍然必须反复解析.h。有些编译器支持预编译的头文件,可以帮助解决这个问题,但并不总是使用它们。

另请参阅:C++ Frequently Questioned Answers

答案 5 :(得分:12)

最大的问题是:

1)无限头重新分析。已经提到了。缓解(如#pragma一次)通常只对每个编译单元有效,而不是每个构建。

2)工具链经常被分成多个二进制文件(极端情况下的make,预处理器,编译器,汇编器,归档器,impdef,链接器和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系统也只是为了获取信息而调用某些程序。

答案 6 :(得分:11)

构建C / C ++:真正发生了什么以及为什么需要这么长时间

相当大一部分软件开发时间不用于编写,运行,调试甚至设计代码,而是等待它完成编译。 为了使事情变得快速,我们首先要了解编译C / C ++软件时发生的事情。步骤大致如下:

  • 配置
  • 构建工具启动
  • 依赖性检查
  • 汇编
  • 链接

我们现在将更详细地研究每个步骤,重点关注如何更快地制作它们。

<强>配置

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

此步骤相对较少发生。只需在更改配置或更改构建配置时运行它。如果没有更改构建系统,那么要做得更快,就没有太多工作要做。

构建工具启动

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

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

应该注意,Make的原因是如此慢,这不是一个实现错误。 Makefile的语法有一些怪癖,它们实现了一个非常快速的实现,但几乎不可能。与下一步结合使用时,这个问题更加明显。

依赖关系检查

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

<强>汇编

此时我们终于调用了编译器。切割一些角落,这是采取的大致步骤。

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

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

使用它们需要一点肘部油脂,但好处是不可否认的。更快的构建时间可以带来更快乐的开发人员,更高的灵活性以及最终更好的代码。

答案 7 :(得分:7)

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

#include "BigClass.h"

class SmallClass
{
   BigClass m_bigClass;
}

编译速度慢于:

class BigClass;

class SmallClass
{
   BigClass* m_bigClass;
}

答案 8 :(得分:6)

在较大的C ++项目中减少编译时间的一种简单方法是制作一个* .cpp包含文件,其中包含项目中的所有cpp文件并进行编译。这会将标题爆炸问题减少一次。这样做的好处是编译错误仍将引用正确的文件。

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

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

然后通过make everything.cpp

编译项目

答案 9 :(得分:4)

有些原因是:

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

2)(更重要)C ++编译器生成机器代码并在编译期间执行所有优化。 C#和Java只走了一半,将这些步骤留给了JIT。

答案 10 :(得分:4)

你得到的折衷是程序运行得更快。在开发过程中,这对您来说可能是一种冷淡的安慰,但是一旦开发完成,它可能会很重要,而且程序只是由用户运行。

答案 11 :(得分:2)

大多数答案都有点不清楚,提到C#总是运行得慢,因为执行C ++的动作只在编译时执行一次,这个性能成本也因运行时依赖性而受到影响(需要加载更多的东西)能够运行),更不用说C#程序将始终具有更高的内存占用,所有这些都导致性能与可用硬件的能力更密切相关。对于解释或依赖VM的其他语言也是如此。

答案 12 :(得分:1)

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

可能的问题#1 - 编译标题:(这可能已经或可能没有通过其他答案或评论解决。)Microsoft Visual C ++(AKA VC ++)支持预编译标题,我高度评价推荐。当您创建一个新项目并选择您正在制作的程序类型时,屏幕上会出现一个设置向导窗口。如果您点击底部的“下一步&gt;”按钮,该窗口将带您进入一个包含多个功能列表的页面;确保选中“预编译头”选项旁边的框。 (注意:这是我在C ++中使用Win32控制台应用程序的经验,但对于C ++中的各种程序可能并非如此。)

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

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

答案 13 :(得分:1)

在大型的面向对象的项目中,重要的原因是C ++很难限制依赖项。

私有函数需要在其各自类的公共头文件中列出,这使得依赖关系比它们需要的更具传递性(传染性):

// 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&);
};

如果这种模式被幸福地重复到标题的依赖树中,则倾向于创建一些“上帝标题”,这些“上帝标题”间接包括项目中所有标题的大部分。它们像god objects一样广为人知,只是在绘制它们的包含树之前这是不明显的。

这通过两种方式增加了编译时间:

  1. 它们添加到包含它们的每个编译单元(.cpp文件)中的代码量很容易比cpp文件本身多很多倍。从角度来看,catch2.hpp是18000行,而大多数人(甚至是IDE)也开始努力编辑1000-10000行以上的文件。
  2. 编辑标头时必须重新编译的文件数不包含在依赖该文件的真实文件集中。

是的,有一些缓解措施,例如前向声明which has perceived downsidespimpl idiom,这是一个非零成本抽象。即使C ++的功能无止境,但您的同龄人仍会想知道,如果您偏离了预期目标,就会吸烟。

最糟糕的部分:如果您考虑一下,甚至没有必要在公共标头中声明私有函数:成员函数的道德等同性通常可以用C来模仿,而C不会重新创建它问题。

答案 14 :(得分:1)

为简单地回答这个问题,C ++是比市场上其他语言复杂得多的语言。它具有可多次解析代码的旧式包含模型,并且其模板库并未针对编译速度进行优化。

语法和ADL

通过考虑一个非常简单的示例,让我们看一下C ++的语法复杂性:

x*y;

尽管您可能会说上面是一个带乘法的表达式,但在C ++中不一定是这种情况。如果x是类型,则该语句实际上是指针声明。这意味着C ++语法是上下文相关的。

这是另一个例子:

foo<x> a;

同样,您可能会认为这是foo类型的变量“ a”的声明,但也可以将其解释为:

(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规则指出,我们将考虑函数调用的所有参数来寻找名称“ foo”。在这种情况下,将考虑所有名为“ foo”的函数进行重载解析。此过程可能会花费一些时间,尤其是在有很多函数重载的情况下。在模板化上下文中,ADL规则变得更加复杂。

#include

此命令可能会严重影响编译时间。根据所包含文件的类型,预处理器可能仅复制几行代码,也可能复制数千行。

此外,编译器无法优化此命令。如果头文件取决于宏,则可以在包含之前复制可以修改的不同代码。

有一些针对这些问题的解决方案。您可以使用预编译的标头,它们是标头中解析的内容的编译器内部表示。但是,这需要用户的努力才能完成,因为预编译的标头假定标头不依赖宏。

模块功能为该问题提供了语言级别的解决方案。从C ++ 20版本开始可用。

模板

模板的编译速度具有挑战性。每个使用模板的翻译单元都需要包含它们,并且这些模板的定义必须可用。模板的某些实例化最终导致其他模板的实例化。在某些极端情况下,模板实例化会消耗大量资源。使用模板而不是为编译速度而设计的库可能会带来麻烦,如您在以下链接提供的元编程库的比较中所看到的:http://metaben.ch/。它们在编译速度上的差异非常明显。

如果您想了解为什么某些元编程库在编译时间上比其他元编程库更好,请查看this video about the Rule of Chiel

结论

C ++是一种缓慢编译的语言,因为在最初开发该语言时,编译性能并不是最高优先级。结果,C ++的功能可能在运行时有效,但不一定在编译时有效。

P.S –我在Incredibuild(一家致力于加速C ++编译的软件开发加速公司)工作,欢迎您try it for free

答案 15 :(得分:0)

正如已经评论过的,编译器花了很多时间实例化并重新实例化模板。在某种程度上,有些项目专注于该特定项目,并声称在一些非常有利的情况下可观察到30倍的加速。请参阅http://www.zapcc.com