可以使用哪些技术来加快c++编译时间?

这个问题出现在一些关于Stack Overflow问题c++编程风格的评论中,我很有兴趣听听有什么想法。

我看到过一个相关的问题,为什么c++编译要花这么长时间?,但这并没有提供很多解决方案。


我有个关于使用内存驱动器的主意。事实证明,对于我的项目来说,这并没有太大的区别。但它们仍然很小。试一试!我很想知道这有多大帮助。


如果您有一个多核处理器,Visual Studio(2005及以后版本)和GCC都支持多处理器编译。当然,如果你有硬件,这是可以实现的。


我将链接到我的另一个答案:如何减少Visual c++项目(原生c++)的编译时间和链接时间?我想补充的另一点是使用预编译的头文件,这经常会导致问题。但是,请只将它们用于几乎不会改变的部分(如GUI工具箱头)。否则,他们会花费你更多的时间,而不是他们最终节省你的时间。

另一个选项是,当你使用GNU make时,打开-j<N>选项:

  -j [N], --jobs[=N]          Allow N jobs at once; infinite jobs with no arg.

我通常把它设置为3,因为我这里有一个双核。然后,它将为不同的翻译单元并行运行编译器,前提是它们之间没有依赖关系。链接不能并行完成,因为只有一个链接器进程将所有目标文件链接在一起。

但是链接器本身可以被线程化,这就是GNU gold ELF链接器所做的。它是优化的多线程c++代码,据说它链接ELF对象文件的速度比旧的ld快很多(实际上包含在binutils中)。


以下是一些例子:

通过启动一个多编译作业(make -j2就是一个很好的例子)来使用所有处理器内核。 关闭或降低优化(例如,GCC使用-O1时比使用-O2或-O3时快得多)。 使用预编译的头文件。


升级你的电脑 获得一个四芯(或双四芯系统) 获得大量的内存。 使用RAM驱动器可以大大减少文件I/O延迟。(有些公司生产的IDE和SATA RAM驱动器就像硬盘驱动器一样)。 然后你有了所有其他典型的建议 如果可用,使用预编译头文件。 减少项目各部分之间的耦合。更改一个头文件通常不需要重新编译整个项目。


Use

#pragma once

在头文件的顶部,因此如果它们在翻译单元中包含了多次,则头文件的文本只会被包含和解析一次。


当我从大学毕业时,我看到的第一个真正有生产价值的c++代码有这些神秘的#ifndef…#endif指令在它们之间定义头文件的地方。我以一种非常天真的方式问了那个写代码的人关于这些重要的东西,他被介绍给了大规模编程的世界。

回到正题,当谈到减少编译时间时,使用指令来防止重复的头定义是我学到的第一件事。


语言技巧

痘痘成语

看看这里和这里的impl习惯用法,也称为不透明指针或句柄类。它不仅加快了编译速度,还在与非抛出交换函数结合使用时提高了异常安全性。impl习惯用法让您减少了头文件之间的依赖关系,并减少了需要重新编译的数量。

提出声明

尽可能使用前向声明。如果编译器只需要知道SomeIdentifier是一个结构体或指针或其他什么,就不要包含整个定义,这会迫使编译器做更多的工作。这可能会产生级联效应,使这种方式比他们需要的更慢。

I/O流以降低构建速度而闻名。如果你需要它们在头文件中,尝试#包含<iosfwd>而不是<iostream>和#只在实现文件中包含<iostream>头。<iosfwd>报头只保存前向声明。不幸的是,其他标准标头没有各自的声明标头。

在函数签名中首选引用传递而不是值传递。这将消除在头文件中#include各自的类型定义的需要,您只需要向前声明类型。当然,更喜欢const引用而不是非const引用,以避免模糊的错误,但这是另一个问题。

守卫条件

使用保护条件可以防止头文件在一个翻译单元中被多次包含。

#pragma once
#ifndef filename_h
#define filename_h

// Header declarations / definitions

#endif

通过同时使用pragma和ifndef,你可以获得普通宏解决方案的可移植性,以及一些编译器在使用pragma once指令时可以实现的编译速度优化。

减少相互依存

一般来说,你的代码设计越模块化,依赖性越小,你需要重新编译的次数就越少。您还可以减少编译器同时在任何单个块上所做的工作量,因为它需要跟踪的工作量更少了。

编译器选项

预编译头文件

它们用于为多个翻译单元编译包含标题的公共部分。编译器编译它一次,并保存它的内部状态。然后可以快速加载该状态,以便在编译具有相同头集的另一个文件时抢占先机。

注意,在预编译的头文件中只包含很少更改的内容,否则您可能会更频繁地进行完全重建。这是一个存放STL头文件和其他库包含文件的好地方。

Ccache是另一个利用缓存技术来加快速度的实用程序。

使用并行性

许多编译器/ ide支持使用多个内核/ cpu同时进行编译。在GNU Make(通常与GCC一起使用)中,使用-j [N]选项。在Visual Studio中,在首选项下有一个选项,允许它并行构建多个项目。您还可以使用/MP选项进行文件级并行,而不仅仅是项目级并行。

其他并行工具:

Incredibuild 建立统一 distcc

使用较低的优化级别

编译器尝试优化的次数越多,它的工作就越困难。

共享库

将修改频率较低的代码移到库中可以减少编译时间。通过使用共享库(。或者。dll),你也可以减少链接时间。

换一台更快的电脑

更多的RAM,更快的硬盘驱动器(包括ssd),更多的cpu /内核都会使编译速度有所不同。


尽可能使用前向声明。如果类声明只使用指向类型的指针或引用,则可以向前声明它,并在实现文件中包含该类型的头文件。

例如:

// T.h
class Class2; // Forward declaration

class T {
public:
    void doSomething(Class2 &c2);
private:
    Class2 *m_Class2Ptr;
};

// T.cpp
#include "Class2.h"
void Class2::doSomething(Class2 &c2) {
    // Whatever you want here
}

更少的包含意味着更少的预处理器工作,如果你做得足够多的话。


我推荐《Games from Within, Indie Game Design And Programming》中的以下文章:

物理结构和c++ -第1部分:概览 物理结构和c++ -第2部分:构建时间 甚至更多的包含实验 《超人总动员》有多不可思议? 预编译头文件的处理和提供 追求完美的建造系统 追求完美的构建系统(下)

当然,它们已经很旧了——你必须用最新的版本(或你可用的版本)重新测试所有的东西,才能得到真实的结果。无论如何,这都是一个很好的创意来源。


你可以使用Unity Builds。

​​


只是为了完整性:构建可能很慢,因为构建系统很愚蠢,也因为编译器花了很长时间来完成它的工作。

关于Unix环境中这个主题的讨论,请阅读递归使其被认为是有害的(PDF)。


你的时间都花在哪里了?你的CPU受限吗?内存约束?磁盘绑定?你能使用更多的内核吗?更多的内存?是否需要RAID?您只是想提高当前系统的效率吗?

在gcc/g++下,您查看过ccache了吗?如果你在做清洁,这是有帮助的;做很多。


一旦您应用了上面所有的代码技巧(前向声明、将公共头中的头包含减少到最低限度、使用Pimpl将大多数细节推入实现文件中……),并且在语言方面没有任何其他收获,那么考虑您的构建系统。如果您使用Linux,请考虑使用distcc(分布式编译器)和ccache(缓存编译器)。

第一个函数distcc在本地执行预处理器步骤,然后将输出发送到网络中第一个可用的编译器。它要求网络中所有配置节点的编译器和库版本相同。

后者ccache是编译器缓存。它再次执行预处理器,然后检查内部数据库(保存在本地目录中),该预处理器文件是否已经用相同的编译器参数编译过。如果是,它只弹出编译器第一次运行时的二进制文件和输出。

这两种方法可以同时使用,这样如果ccache没有本地副本,它可以通过网络将副本发送到带有distcc的另一个节点,或者它可以只注入解决方案而不进行进一步处理。


动态链接(.so)比静态链接(.a)快得多。特别是当你的网络驱动器很慢的时候。这是因为.a文件中有所有需要处理和写入的代码。此外,需要将一个更大的可执行文件写入磁盘。


更大的内存。

有人在另一个回答中谈到了RAM驱动器。我用80286和Turbo c++(显示年龄)做到了这一点,结果是惊人的。就像机器崩溃时数据丢失一样。


有一本书是关于这个主题的,书名是《大规模c++软件设计》(由John Lakos撰写)。

这本书的年代早于模板,所以在书的内容中加上“使用模板也会使编译器变慢”。


在Linux(也许还有其他一些* nix)上,您可以通过不盯着输出并更改到另一个TTY来加速编译。

下面是实验:printf使我的程序变慢


有一种技术在过去对我来说非常有效:不要独立编译多个c++源文件,而是生成一个包含所有其他文件的c++文件,就像这样:

// myproject_all.cpp
// Automatically generated file - don't edit this by hand!
#include "main.cpp"
#include "mainwindow.cpp"
#include "filterdialog.cpp"
#include "database.cpp"

当然,这意味着您必须重新编译所有包含的源代码,以防任何源代码发生更改,因此依赖关系树变得更糟。但是,将多个源文件编译为一个翻译单元更快(至少在我使用MSVC和GCC的实验中),并且生成更小的二进制文件。我还怀疑编译器被赋予了更大的优化潜力(因为它可以一次看到更多代码)。

这种技术在各种情况下都会失效;例如,如果两个或多个源文件声明了同名的全局函数,编译器就会退出。我在其他答案中找不到这个技巧,这就是为什么我在这里提到它。

不管怎样,KDE项目从1999年起就使用了这种完全相同的技术来构建优化的二进制文件(可能是为了发布)。转换到构建配置脚本的过程叫做——enable-final。出于对考古的兴趣,我翻出了宣布这个功能的帖子:http://lists.kde.org/?l=kde-devel&m=92722836009368&w=2


虽然不是一个“技术”,我不知道Win32项目与许多源文件编译速度比我的“Hello World”空项目。因此,我希望这能帮助到像我这样的人。

在Visual Studio中,增加编译时间的一个选项是增量链接(/ Incremental)。它与链接时代码生成(/LTCG)不兼容,因此在进行版本构建时请记住禁用增量链接。


更快的硬盘。

编译器将许多(可能很大)文件写入磁盘。使用SSD而不是典型的硬盘,编译时间要低得多。


网络共享将大大降低您的构建速度,因为查找延迟很高。对于像Boost这样的东西,它给我带来了巨大的不同,尽管我们的网络共享驱动器非常快。当我从网络共享切换到本地SSD时,编译一个玩具Boost程序的时间从大约1分钟缩短到1秒。


不是关于编译时间,而是关于构建时间:

如果在工作时必须重新构建相同的文件,请使用ccache 在构建文件上 使用忍者建造而不是制造。我目前正在编译一个项目 有~100个源文件和所有缓存的ccache。使需要 5分钟,忍者不到1分钟。

你可以用-GNinja从cmake生成忍者文件。


我从事STAPL项目,这是一个大量模板化的c++库。有时,我们不得不重新审视所有的技术以减少编译时间。在这里,我总结了我们使用的技术。上面已经列出了其中一些技巧:

找到最耗时的部分

虽然没有证明符号长度和编译时间之间的相关性,但我们观察到较小的平均符号大小可以提高所有编译器上的编译时间。所以你的第一个目标是找到代码中最大的符号。

方法1 -根据大小对符号进行排序

你可以使用nm命令根据符号的大小列出它们:

nm --print-size --size-sort --radix=d YOUR_BINARY

在这个命令中——radix=d让你看到十进制的大小(默认是十六进制)。现在,通过查看最大的符号,确定是否可以分解相应的类,并尝试通过在基类中分解非模板化部分或将类拆分为多个类来重新设计它。

方法2 -根据长度对符号进行排序

您可以运行常规的nm命令并将其输送到您最喜欢的脚本(AWK, Python等),以根据符号的长度对其进行排序。根据我们的经验,这种方法比方法1更好地识别出最大的麻烦。

方法3 -使用灯光

temlight是一个基于clang的工具,用于分析模板实例化的时间和内存消耗,并执行交互式调试会话,以获得对模板实例化过程的内省。

你可以通过查看LLVM和Clang(说明)来安装temlight,并在其上应用temlight补丁。LLVM和Clang的默认设置是调试和断言,这可能会严重影响编译时间。看起来temlight两者都需要,所以你必须使用默认设置。安装LLVM和Clang的过程大约需要一个小时左右。

应用补丁后,您可以使用位于安装时指定的build文件夹中的temight++来编译代码。

确保temight++在你的PATH中。现在要编译,将以下开关添加到Makefile中的CXXFLAGS或命令行选项中:

CXXFLAGS+=-Xtemplight -profiler -Xtemplight -memory -Xtemplight -ignore-system

Or

templight++ -Xtemplight -profiler -Xtemplight -memory -Xtemplight -ignore-system

编译完成后,您将得到一个.trace.memory。PBF和。trace。在同一文件夹中生成PBF。为了可视化这些跟踪,您可以使用temlight Tools将这些跟踪转换为其他格式。按照下面的说明安装templight-convert。我们通常使用callgrind输出。如果你的项目很小,你也可以使用GraphViz输出:

$ templight-convert --format callgrind YOUR_BINARY --output YOUR_BINARY.trace

$ templight-convert --format graphviz YOUR_BINARY --output YOUR_BINARY.dot

生成的callgrind文件可以使用kcachegrind打开,在kcachegrind中可以跟踪最耗时/内存消耗的实例化。

减少模板实例化的数量

虽然没有确切的解决方案来减少模板实例化的数量,但有一些指导方针可以提供帮助:

重构具有多个模板参数的类

例如,如果你有一门课,

template <typename T, typename U>
struct foo { };

T和U都可以有10个不同的选项,你已经将这个类的模板实例化增加到100个。解决这个问题的一种方法是将代码的公共部分抽象到不同的类中。另一种方法是使用继承反转(反转类层次结构),但在使用这种技术之前,请确保您的设计目标没有受到损害。

将非模板代码重构为单独的翻译单元

使用这种技术,您可以一次性编译公共部分,并在以后将其链接到其他tu(翻译单元)。

使用extern模板实例化(自c++ 11起)

如果您知道一个类的所有可能的实例化,您可以使用这种技术在不同的翻译单元中编译所有案例。

例如,在:

enum class PossibleChoices = {Option1, Option2, Option3}

template <PossibleChoices pc>
struct foo { };

我们知道这个类有三种可能的实例化:

template class foo<PossibleChoices::Option1>;
template class foo<PossibleChoices::Option2>;
template class foo<PossibleChoices::Option3>;

把上面的内容放在一个翻译单元中,并在头文件中使用extern关键字,在类定义的下面:

extern template class foo<PossibleChoices::Option1>;
extern template class foo<PossibleChoices::Option2>;
extern template class foo<PossibleChoices::Option3>;

如果使用一组公共实例化编译不同的测试,这种技术可以节省时间。

注意:MPICH2此时忽略显式实例化,总是在所有编译单元中编译实例化的类。

使用统一构建

unity构建背后的整个想法是将你使用的所有.cc文件包含在一个文件中,并且只编译该文件一次。使用这种方法,您可以避免重新实例化不同文件的公共部分,如果您的项目包含许多公共文件,您可能还可以节省磁盘访问。

例如,假设您有三个文件foo1。cc, foo2。cc, foo3。它们都包含来自STL的tuple。你可以创建一个傻瓜。抄送,看起来像:

#include "foo1.cc"
#include "foo2.cc"
#include "foo3.cc"

您只需编译此文件一次,这可能会减少三个文件之间的公共实例化。一般来说,很难预测这种改善是否显著。但是一个明显的事实是,您将失去构建中的并行性(您不能再同时编译这三个文件)。

此外,如果这些文件中的任何一个恰好占用了大量内存,那么在编译结束之前就可能耗尽内存。在一些编译器上,比如GCC,这可能会导致你的编译器内存不足而导致ICE(内部编译错误)。所以不要使用这种方法,除非你知道所有的利弊。

预编译头文件

预编译头文件(PCHs)通过将头文件编译为编译器可识别的中间表示形式,可以节省大量编译时间。要生成预编译的头文件,只需要使用常规编译命令编译头文件。例如,在GCC上:

$ g++ YOUR_HEADER.hpp

这将生成一个YOUR_HEADER.hpp。GCH文件(。gch是GCC中PCH文件的扩展名)。这意味着如果你在其他文件中包含YOUR_HEADER.hpp,编译器将使用你的YOUR_HEADER.hpp。gch而不是YOUR_HEADER.hpp在之前的相同文件夹。

这种技术有两个问题:

You have to make sure that the header files being precompiled is stable and is not going to change (you can always change your makefile) You can only include one PCH per compilation unit (on most of compilers). This means that if you have more than one header file to be precompiled, you have to include them in one file (e.g., all-my-headers.hpp). But that means that you have to include the new file in all places. Fortunately, GCC has a solution for this problem. Use -include and give it the new header file. You can comma separate different files using this technique.

例如:

g++ foo.cc -include all-my-headers.hpp

使用未命名或匿名的名称空间

未命名名称空间(又称匿名名称空间)可以显著减小生成的二进制大小。未命名的名称空间使用内部链接,这意味着在这些名称空间中生成的符号对其他TU(翻译或编译单元)是不可见的。编译器通常为未命名的名称空间生成唯一的名称。这意味着如果你有一个foo.hpp文件:

namespace {

template <typename T>
struct foo { };
} // Anonymous namespace
using A = foo<int>;

您碰巧将该文件包含在两个tu中(两个.cc文件并分别编译它们)。这两个foo模板实例将不相同。这违反了One Definition Rule (ODR)。出于同样的原因,不鼓励在头文件中使用未命名的名称空间。可以在.cc文件中使用它们,以避免在二进制文件中出现符号。在某些情况下,更改.cc文件的所有内部细节显示生成的二进制大小减少了10%。

更改可见性选项

在较新的编译器中,您可以选择符号在动态共享对象(DSOs)中可见或不可见。理想情况下,改变可见性可以提高编译器性能、链接时间优化(lto)和生成的二进制大小。如果你看一下GCC中的STL头文件,你可以看到它被广泛使用。要启用可见性选择,您需要对每个函数、每个类、每个变量,更重要的是对每个编译器更改代码。

在可见性的帮助下,您可以从生成的共享对象中隐藏您认为是私有的符号。在GCC上,你可以通过将默认值或隐藏值传递给编译器的-visibility选项来控制符号的可见性。这在某种意义上类似于未命名的名称空间,但是以一种更精细和侵入的方式。

如果你想指定每个case的可见性,你必须添加以下属性到你的函数,变量和类:

__attribute__((visibility("default"))) void  foo1() { }
__attribute__((visibility("hidden")))  void  foo2() { }
__attribute__((visibility("hidden")))  class foo3   { };
void foo4() { }

GCC中的默认可见性是default (public),这意味着如果您将上述方法编译为共享库(-shared)方法,则foo2和类foo3将在其他tu中不可见(foo1和foo4将可见)。如果你使用-visibility=hidden编译,那么只有foo1是可见的。即使是foo4也会被隐藏。

您可以在GCC wiki上阅读更多关于可见性的信息。


从Visual Studio 2017开始,你可以有一些编译器度量需要花费的时间。

将这些参数添加到项目属性窗口中的C/ c++ ->命令行(附加选项): /Bt+ /d2cgsummary /d1reportTime . /Bt+ /d2cgsummary /d1reportTime . /

你可以在这篇文章中获得更多信息。


使用动态链接而不是静态链接会让你的编译器更快。

如果你使用t Cmake,激活属性:

set(BUILD_SHARED_LIBS ON)

Build Release,使用静态链接可以得到更多的优化。


首先,我们必须了解c++与其他语言的不同之处。

有人说c++有太多的特性。但是,有些语言有更多的特性,它们远没有那么慢。

有人说文件的大小很重要。不,源代码行与编译时间无关。

等等,这怎么可能呢?代码行数越多,编译时间就越长,这是怎么回事?

诀窍在于很多代码行隐藏在预处理器指令中。是的。仅仅一个#include就会破坏模块的编译性能。

你看,c++没有模块系统。所有*.cpp文件都是从头编译的。因此,拥有1000 *.cpp文件意味着编译项目1000次。你还有更多?太糟糕了。

这就是为什么c++开发人员不愿意将类拆分为多个文件的原因。所有这些头文件的维护都很乏味。

那么,除了使用预编译的头文件、将所有cpp文件合并为一个文件并保持头文件的数量最小化之外,我们还能做什么呢?

c++ 20给我们带来了模块的初步支持!最终,您将能够忘记#include以及头文件带来的糟糕编译性能。碰过一个文件?只重新编译该文件!需要编译一个新的签出?以秒为单位编译,而不是以分钟和小时为单位。

c++社区应该尽快迁移到c++ 20。c++编译器开发人员应该更加关注这一点,c++开发人员应该开始测试各种编译器的初步支持,并使用那些支持模块的编译器。这是c++历史上最重要的时刻!


来自微软:https://devblogs.microsoft.com/cppblog/recommendations-to-speed-c-builds-in-visual-studio/

具体建议包括:

项目使用PCH吗 是否包含常用的系统、运行时和第三方头文件 PCH 在PCH中包含很少改变项目特定的头 不包括经常变化的头 是否定期审核PCH以保持产品流失的最新情况 使用/ mp 是否移除/Gm以支持/MP 是否解决与#import和use /MP的冲突 是否使用连接器开关/增量 使用链接器开关/调试:fastlink 是否考虑使用第三方构建加速器