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

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

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

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

135728 次浏览

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

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

以下是一些例子:

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

    1. 获得一个四芯(或双四芯系统)
    2. 获得大量的内存。
    3. 使用RAM驱动器可以大大减少文件I/O延迟。(有些公司生产的IDE和SATA RAM驱动器就像硬盘驱动器一样)。
    4. 李< / ol > < / >
    5. 然后你有所有其他典型的建议

      1. 如果可用,使用预编译头文件。
      2. 减少项目各部分之间的耦合。更改一个头文件通常不需要重新编译整个项目。
      3. 李< / ol > < / >

使用

#pragma once

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

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

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

语言技巧

Pimpl成语

看一下皮条客成语 在这里在这里,也称为不透明的指针或句柄类。它不仅加快了编译速度,还增加了与non-throwing交换函数结合使用时的异常安全性。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使中(通常与GCC一起使用),使用-j [N]选项。在Visual Studio中,在首选项下有一个选项,允许它并行构建多个项目。你也可以将/MP选项用于文件级并行,而不仅仅是项目级并行。

其他并行工具:

  • Incredibuild
  • < a href = " http://buffered。io/posts/the-magic-of-unity-builds/" rel="noreferrer">Unity Build . Build
  • distcc

使用较低的优化级别

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

共享库

将修改频率较低的代码移到库中可以减少编译时间。通过使用共享库(.so.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》中的以下文章:

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

你可以使用统一的构建

​​

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

阅读递归使被认为有害 (PDF)以获得在Unix环境中关于此主题的讨论。

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

在gcc/g++下,你看过ccache吗?如果你经常做make clean; make,它会很有帮助。

一旦你应用了上面所有的代码技巧(前向声明,在公共头中将头的包含减少到最小,将大多数细节放入Pimpl的实现文件中…),并且在语言方面没有其他可以获得的东西,考虑一下你的构建系统。如果你使用Linux,考虑使用distcc(分布式编译器)和ccache(缓存编译器)。

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

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

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

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

更大的内存。

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

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

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

在Linux(也许还有其他一些* nix)上,你可以通过不盯着输出并更改为另一个< < em > / em > 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中,增加编译时间的一个选项是增量链接(/增量)。它与链接时代码生成(/ LTCG)不兼容,因此在进行版本构建时请记住禁用增量链接。

更快的硬盘。

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

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

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

  • 如果你在工作时必须重建相同的文件,请使用ccache 在你的构建文件

  • 使用ninja-build代替make我目前正在编译一个项目 有~100个源文件和所有缓存的ccache。使需要 5分钟,忍者小于1.

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

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

找到最耗时的部分

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

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

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

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

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

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

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

方法3 -使用灯光

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

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

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

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

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

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

编译完成后,您将得到一个.trace.memory。PBF和。trace。在同一文件夹中生成PBF。为了可视化这些跟踪,可以使用Templight工具,它可以将这些跟踪转换为其他格式。遵循这些指令来安装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打开,你可以在其中跟踪最耗时/内存消耗的实例化。

减少模板实例化的数量

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

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

例如,如果你有一门课,

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

并且TU都可以有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.ccfoo2.ccfoo3.cc,它们都包括STL中的tuple。你可以创建一个foo-all.cc,它看起来像:

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

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

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

预编译头文件

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

$ g++ YOUR_HEADER.hpp

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

这种技术有两个问题:

  1. 你必须确保被预编译的头文件是稳定的,不会改变(你可以随时更改你的makefile)
  2. 每个编译单元只能包含一个PCH(在大多数编译器上)。这意味着如果你有多个头文件要预编译,你必须将它们包含在一个文件中(例如,all-my-headers.hpp)。但这意味着您必须将新文件包含在所有位置。幸运的是,GCC为这个问题提供了解决方案。使用-include并给它一个新的头文件。您可以使用此技术用逗号分隔不同的文件。

例如:

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模板实例将不相同。这违反了一个定义规则 (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中不可见(foo1foo4将可见)。如果你用-visibility=hidden编译,那么只有foo1是可见的。甚至foo4也会被隐藏。

你可以在GCC维基上阅读更多关于可见性的信息。

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

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

你可以有更多的信息在这篇文章中

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

如果你使用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

    DO包含常用的系统、运行时和第三方头文件 PCH < / p >

    在PCH中包含很少改变项目特定的头

    不包括经常变化的头

    是否定期审核PCH以保持产品流失的最新情况

    使用/ mp

    是否移除/Gm以支持/MP

    是否解决与#import和use /MP的冲突

    是否使用连接器开关/增量

    使用链接器开关/调试:fastlink

    是否考虑使用第三方构建加速器