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

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


当前回答

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

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

其他回答

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

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

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

编译速度当然可能不是c++编译器开发人员的优先考虑事项,但C/ c++语法中也有一些固有的复杂性,这使得处理起来更加困难。(我不是C方面的专家,但Walter Bright是,在构建了各种商业C/ c++编译器之后,他创建了D语言。他的改变之一是强制使用上下文无关的语法,使语言更容易解析。)

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

简单地回答这个问题,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版本开始使用。

模板

The compilation speed for templates is challenging. Each translation unit that uses templates needs to have them included, and the definitions of these templates need to be available. Some instantiations of templates end up in instantiations of other templates. In some extreme cases, template instantiation can consume lots of resources. A library that uses templates and that was not designed for compilation speed can become troublesome, as you can see in a comparison of metaprogramming libraries provided at this link: http://metaben.ch/. Their differences in compilation speed are significant.

如果你想了解为什么一些元编程库在编译时间上比其他的更好,看看这个关于Chiel规则的视频。

结论

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

附注:我在Incredibuild工作,这是一家软件开发加速公司,专门加速c++编译,欢迎您免费试用。

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

参见:c++常见问题答案

构建C/ c++:到底发生了什么,为什么要花这么长时间

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

配置 构建工具启动 依赖项检查 编译 链接

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

配置

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

这一步很少发生。它只需要在更改配置或更改构建配置时运行。如果不改变构建系统,就没有多少事情可以加快这一步。

构建工具启动

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

根据构建的复杂性和大小,这可能需要几秒到几秒的时间。这本身并没有那么糟糕。不幸的是,大多数基于make的构建系统在每次构建时都会调用几十到几百次make。这通常是由递归使用make(这是不好的)引起的。

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

依赖项检查

Once the build tool has read its configuration, it has to determine what files have changed and which ones need to be recompiled. The configuration files contain a directed acyclic graph describing the build dependencies. This graph is usually built during the configure step. Build tool startup time and the dependency scanner are run on every single build. Their combined runtime determines the lower bound on the edit-compile-debug cycle. For small projects this time is usually a few seconds or so. This is tolerable. There are alternatives to Make. The fastest of them is Ninja, which was built by Google engineers for Chromium. If you are using CMake or Gyp to build, just switch to their Ninja backends. You don’t have to change anything in the build files themselves, just enjoy the speed boost. Ninja is not packaged on most distributions, though, so you might have to install it yourself.

编译

此时,我们最终调用编译器。省事起见,以下是大致采取的步骤。

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

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

使用它们需要一些体力,但好处是不可否认的。更快的构建时间会让开发人员更快乐,更敏捷,并最终产生更好的代码。

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

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