590

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

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

4

15 回答 15

860

几个原因

头文件

每个单独的编译单元都需要(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++ 成为一种更强大的语言,但它们也在编译速度方面付出了代价。

于 2008-11-25T18:38:22.417 回答
50

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

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

于 2008-11-25T19:01:22.387 回答
45

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

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

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

当然,编译速度可能并不是 C++ 编译器开发人员的优先考虑因素,但 C/C++ 语法中也存在一些固有的复杂性,使其更难处理。(我不是 C 方面的专家,但 Walter Bright 是,在构建了各种商业 C/C++ 编译器之后,他创建了 D 语言。他的一项更改是强制执行上下文无关语法以使该语言更易于解析.)

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

于 2008-11-25T18:55:04.630 回答
18

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

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

Python 是一种解释型语言,它也被编译成字节码。

我敢肯定还有其他原因,但总的来说,不必编译为本地机器语言可以节省时间。

于 2008-11-25T18:28:21.157 回答
18

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

另请参阅:C++ 常见问题解答

于 2008-11-25T18:32:37.937 回答
16

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

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

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

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

配置

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

此步骤相对很少发生。它只需要在更改配置或更改构建配置时运行。除了更改构建系统之外,没有太多工作可以让这一步更快。

构建工具启动

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

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

需要注意的是,Make 这么慢的原因不是实现错误。Makefiles 的语法有一些怪癖,使得真正快速的实现几乎是不可能的。当与下一步结合时,这个问题会更加明显。

依赖检查

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

汇编

此时我们终于调用了编译器。偷工减料,这里是采取的大致步骤。

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

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

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

于 2015-04-23T15:30:26.420 回答
15

最大的问题是:

1)无限头重新解析。已经提到了。缓解措施(如 #pragma once)通常仅适用于每个编译单元,而不适用于每个构建。

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 系统也调用一些程序只是为了获取信息。

于 2009-05-02T11:30:47.243 回答
12

一些原因是:

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

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

于 2008-11-25T18:27:36.730 回答
11

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

#include "BigClass.h"

class SmallClass
{
   BigClass m_bigClass;
}

编译速度比:

class BigClass;

class SmallClass
{
   BigClass* m_bigClass;
}
于 2008-11-25T18:33:53.283 回答
9

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

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

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

然后通过只制作everything.cpp 来编译项目

于 2013-03-03T22:35:22.540 回答
6

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

于 2008-12-31T15:08:32.133 回答
5

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

于 2009-06-20T05:10:56.983 回答
5

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

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

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

我希望这可以帮助你; 让我知道是否有!:)

于 2016-08-18T02:11:17.270 回答
2

简单地回答这个问题,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 规则变得更加复杂。

#包括

该命令可能会显着影响编译时间。根据您包含的文件类型,预处理器可能只复制几行代码,也可能复制数千行。

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

这些问题有一些解决方案。您可以使用预编译的标头,它是编译器对标头中所解析内容的内部表示。然而,如果没有用户的努力,这是无法完成的,因为预编译的头文件假定头文件不依赖于宏。

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

模板

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

如果您想了解为什么某些元编程库的编译时间比其他库更好,请查看有关 Chiel 规则的视频

结论

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

PS——我在Incredibuild工作,这是一家专门从事加速C++编译的软件开发加速公司,欢迎大家免费试用

于 2020-09-10T14:08:10.090 回答
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&);
};

如果这种模式被幸福地重复到头文件的依赖树中,这往往会创建一些“上帝头文件”,这些头文件间接包含项目中所有头文件的大部分。它们就像上帝对象一样无所不知,只是在绘制它们的包含树之前这并不明显。

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

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

是的,有一些缓解措施,比如前向声明,它已经感知到了缺点,或者pimpl idiom,这是一个非零成本抽象。尽管 C++ 在你能做的事情上是无限的,但如果你离它的本意太远,你的同行会想知道你一直在抽烟。

最糟糕的部分:如果您考虑一下,甚至不需要在其公共头文件中声明私有函数:成员函数的道德等价物可以并且通常在 C 中被模仿,这不会重现此问题。

于 2020-09-05T15:31:50.107 回答