什么技术可以用来加快C ++编译时间?

什么技术可以用来加快C ++编译时间?

这个问题出现在一些评论堆栈溢出问题的C + +编程风格 ,我很有兴趣听到有什么想法。

我见过一个相关的问题, 为什么C ++编译需要这么长时间? 但是这并不能提供很多解决scheme。


这里投票有Visual Studio支持共享项目之间的预编译头

语言技巧

皮姆普成语

看看这里的Pimpl成语 , 这里也被称为不透明指针或句柄类。 它不仅加快了编译速度,而且与非抛出交换function相结合,还增加了exception安全性。 Pimpl成语可以减less头文件之间的依赖关系,并减less需要完成的重新编译的数量。

转发声明

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

I / Ostream尤其以降低构build而闻名。 如果你在一个头文件中需要它们,只能在实现文件中使用#include <iosfwd>而不是<iostream>和#include <iostream>头文件。 <iosfwd>标题仅包含前向声明。 不幸的是,其他标准头文件没有相应的声明头文件。

在函数签名中优先使用传递引用来传递值。 这将消除#include头文件中的相应types定义的需要,并且只需要转发 – 声明types。 当然,更喜欢const引用非const引用来避免模糊的错误,但这是另一个问题的问题。

守卫条件

使用警戒条件来保持头文件不止一次被包含在单个翻译单元中。

 #pragma once #ifndef filename_h #define filename_h // Header declarations / definitions #endif 

通过使用pragma和ifndef,您可以获得普通macros解决scheme的可移植性,以及一些编译器在存在pragma once的情况下可以执行的编译速度优化。

减less相互依赖性

一般来说,代码devise越是模块化和相互依赖性越less,您将不得不重新编译所有内容。 你也可以最终减less编译器在任何一个块上同时做的工作量,因为它跟踪的事实较less。

编译器选项

预编译的头文件

这些用于为许多翻译单元编译包含的标题的常见部分。 编译器编译一次,并保存其内部状态。 然后可以快速加载该状态,以便在编译具有相同标题集的另一个文件时领先一步。

要小心,在预编译头文件中只包含很less更改的内容,否则最终可能会比必要时更频繁地进行完全重build。 这是STL头文件和其他库包含文件的好地方。

ccache是利用caching技术来加快速度的另一个实用工具。

使用并行

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

其他并行工具:

  • Incredibuild
  • 统一build设
  • distcc的

使用较低的优化级别

编译器试图优化的越多,就越难以工作。

共享库

将不经常修改的代码移动到库中可以减less编译时间。 通过使用共享库( .so.dll ),您也可以减less链接时间。

获得更快的计算机

更多的内存,更快的硬盘(包括固态硬盘),以及更多的CPU /核心都将在编译速度上有所不同。

我推荐这些“从内部游戏,独立游戏devise和编程”的文章:

  • 物理结构和C ++ – 第1部分:初看
  • 物理结构和C ++ – 第2部分:构build时间
  • 包含更多的实验
  • Incredibuild有多难以置信?
  • 关心和喂食预先编译的标题
  • 寻求完美的build筑系统
  • 寻求完美的构build系统(第二部分)

当然,它们已经很老了 – 你必须用最新版本(或者可用的版本)重新testing一切,才能得到实际的结果。 无论哪种方式,这是一个很好的想法来源。

我在STAPL项目上工作,这是一个模板严重的C ++库。 有一段时间,我们必须重新审视所有的技术,以减less编译时间。 在这里,我总结了我们使用的技术。 其中一些技术已在上面列出:

find最耗时的部分

虽然符号长度和编译时间之间没有经过certificate的相关性,但我们发现,较小的平均符号大小可以提高所有编译器的编译时间。 所以你的第一个目标就是在代码中find最大的符号。

方法1 – 根据大小sorting符号

您可以使用nm命令根据其大小列出符号:

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

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

方法2 – 根据长度sorting符号

您可以运行常规的nm命令,并将其传递给您最喜欢的脚本( AWK , Python等),以根据其长度对符号进行sorting。 根据我们的经验,这种方法确定了最大的麻烦使得候选人比方法1更好。

方法3 – 使用Templight

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

您可以通过检查LLVM和Clang( 说明 )并在其上应用Templight补丁来安装Templight。 LLVM和Clang的默认设置是debugging和断言,这些可以显着影响编译时间。 它看起来像Templight需要两个,所以你必须使用默认设置。 安装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打开,在其中您可以跟踪最耗时的内存消耗实例。

减less模板实例的数量

虽然没有确切的解决scheme来减less模板实例的数量,但有一些指导可以帮助:

用多个模板参数重构类

例如,如果你有一个class级,

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

并且TU都可以有10个不同的选项,所以你已经把这个类的可能的模板实例增加到了100个。解决这个问题的一个方法就是把代码的公共部分抽象成一个不同的类。 另一种方法是使用inheritance反转(反转类层次结构),但要确保在使用此技术之前,您的devise目标不会受到影响。

将非模板化代码重构为单个翻译单元

使用这种技术,您可以编译一次公共部分,稍后将其与其他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>; 

如果您使用一组通用实例编译不同的testing,这种技术可以节省您的时间。

注意:MPICH2在这一点忽略显式实例,并且总是编译所有编译单元中的实例化类。

使用统一构build

统一构build背后的全部想法是包含所有在一个文件中使用的.cc文件,并只编译一次该文件。 使用这种方法,可以避免重新实例化不同文件的公共部分,如果项目包含大量的公用文件,那么也可能会节省磁盘访问。

作为一个例子,假设你有三个文件foo1.ccfoo2.ccfoo3.cc ,它们都包含来自STL的 tuple 。 您可以创build一个foo-all.cc ,如下所示:

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

您只能编译一次该文件,并有可能减less这三个文件之间的常见实例。 一般预测改善是否显着是很难的。 但是一个显而易见的事实是,在你的构build中你将失去并行性(你不能再同时编译这三个文件)。

此外,如果这些文件中的任何一个碰巧占用大量内存,那么在编译结束之前,实际上可能会耗尽内存。 在一些编译器,如GCC ,这可能是ICE(内部编译器错误)你的编译器缺乏内存。 所以不要使用这种技术,除非你知道所有的优点和缺点。

预编译头文件

预编译头文件(PCHs)通过编译头文件到编译器可以识别的中间表示forms,可以节省大量编译时间。 要生成预编译头文件,只需要使用常规编译命令编译头文件。 例如,在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 )。 但是这意味着你必须在所有地方包含新文件。 幸运的是,海湾合作委员会有这个问题的解决scheme。 使用-include并为其提供新的头文件。 你可以使用这种技术逗号分隔不同的文件。

例如:

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

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

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

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

而你碰巧把这个文件包含在两个TU中(两个.cc文件并单独编译)。 两个foo模板实例将不会相同。 这违反了一个定义规则 (ODR)。 出于同样的原因,在头文件中不鼓励使用未命名的名称空间。 随意在.cc文件中使用它们以避免在二进制文件中出现符号。 在某些情况下,更改.cc文件的所有内部详细信息显示生成的二进制大小减less了10%。

更改可见性选项

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

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

如果您想指定每个案例的可见性,则必须将以下属性添加到您的函数,variables和类中:

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

GCC中默认的可见性是default(public),这意味着如果将上面的代码编译为共享库( -shared )方法, foo2foo3类在其他TU中将不可见( foo1foo4将可见)。 如果使用-visibility=hidden编译,则只有foo1可见。 即使foo4将被隐藏。

您可以阅读更多关于GCC维基上的知名度。

有一本关于这个题目的书,标题是“ 大规模C ++软件devise” (John Lakos编写)。

本书预先模板,所以对该书的内容添加“使用模板也可以使编译器变慢”。

过去一种非常好的方法是:不要单独编译多个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" 

当然,这意味着你必须重新编译所有包含的源代码,以防止任何源代码发生变化,所以依赖关系树变得更糟。 但是,将多个源文件编译为一个翻译单元会更快(至less在我的MSVC和GCC实验中)并生成较小的二进制文件。 我也怀疑编译器有更多的优化潜力(因为它可以同时看到更多的代码)。

这种技术在各种情况下都会中断 例如,如果两个或两个以上的源文件使用相同的名称声明一个全局函数,编译器将会解救出来。 我找不到任何其他答案中描述的这种技术,这就是为什么我在这里提到它。

KDE项目从1999年开始使用这个完全相同的技术来构build优化的二进制文件(可能用于发布)。 切换到构buildconfiguration脚本被称为--enable-final 。 出于考古的兴趣,我挖掘了这个宣布这个特征的post: http : //lists.kde.org/? l= kde-devel&m=92722836009368&w=2

我只是链接到我的其他答案: 你如何减less编译时间,并为Visual C ++项目(本机C + +)链接时间? 。 我想补充的另一点,但经常遇到的问题是使用预编译头。 但是,请仅用于几乎不会改变的部分(如GUI工具包标头)。 否则,他们将花费你更多的时间,比他们最终拯救你。

另一个select是,当您使用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就是一个很好的例子)。
  • closures或降低优化(例如,使用-O1 GCC比使用-O2-O3快得多)。
  • 使用预编译头 。

一旦你已经应用了上面的所有代码技巧(向前声明,在公共头文件中将头文件包含减到最小,用Pimpl推送实现文件中的大部分细节…),并且没有其他任何东西可以在语言方面获得,考虑你的构build系统。 如果您使用Linux,请考虑使用distcc (分布式编译器)和ccache (caching编译器)。

第一个是distcc,在本地执行预处理器步骤,然后将输出发送到networking中第一个可用的编译器。 它需要在networking中所有configuration的节点上使用相同的编译器和库版本。

后者ccache是​​一个编译器caching。 它再次执行预处理器,然后检查一个内部数据库(保存在本地目录中),如果该预处理器文件已经用相同的编译器参数编译的话。 如果是的话,它会popup二进制文件,并从编译器的第一次运行中输出。

两者都可以同时使用,这样,如果ccache没有本地副本,它可以通过networking将其发送到另一个具有distcc的节点,否则可以直接注入解决scheme而无需进一步处理。

当我大学gradle时,我看到的第一个真正有生产价值的C ++代码在它们之间定义了头文件之间有着那些神秘的#ifndef … #endif指令。 我以非常天真的方式问那些正在编写关于这些总体内容的代码,并被介绍给大型编程的世界。

回想一下,使用指令来防止重复的头文件定义是我在减less编译时间方面学到的第一个东西。

更多的RAM。

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

使用

 #pragma once 

在头文件的顶部,所以如果它们在翻译单元中被多次包含,头文本将只被包含和parsing一次。

你可以使用Unity Builds 。

  • 升级你的电脑

    1. 获得一个四核(或一个双四核系统)
    2. 获取大量的RAM。
    3. 使用RAM驱动器大幅减less文件I / O延迟。 (有些公司使IDE和SATA RAM驱动器像硬盘驱动器一样)。
  • 那么你有你所有的其他典型的build议

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

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

阅读recursion在Unix环境下讨论这个主题时,被认为是有害的 (PDF)。

尽可能使用前向声明。 如果一个类声明只使用一个指针或一个types的引用,那么你可以直接声明它,并在实现文件中包含这个types的头文件。

例如:

 // Th 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 } 

如果你足够的话,包括更less的工作对于预处理器来说意味着更less的工作。

你在哪里花时间? 你是否受CPU限制? 内存绑定? 磁盘绑定? 你可以使用更多的核心? 更多的内存? 你需要RAID吗? 你只是想提高你当前系统的效率吗?

在gcc / g ++下,你看过ccache吗? 如果你正在做make_clean _; _会做很多事情会很有帮助。

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

我有一个关于使用RAM驱动器的想法。 事实certificate,对于我的项目来说,这毕竟没有太大的区别。 但是,他们仍然很小。 尝试一下! 我有兴趣听到它有多大的帮助。

networking共享将大大减慢你的构build,因为寻求延迟很高。 对于像Boost这样的东西,即使我们的networking共享驱动器速度很快,对我来说也有很​​大的不同。 当我从networking共享切换到本地SSD时,编译玩具升级程序的时间从大约1分钟到1秒。

如果您有多核处理器,则Visual Studio(2005及更高版本)以及GCC都支持多处理器编译。 如果你有硬件,肯定是有用的。

不是编译时间,而是编译时间:

  • 使用ccache如果您正在处理您的构build文件时必须重build相同的文件

  • 使用忍者构build而不是make。 我目前正在编译一个约100个源文件的项目,所有内容都被ccachecaching。 需要5分钟,忍者不到1。

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

虽然不是一个“技术”,但我无法弄清楚如何Win32项目与许多源文件编译比我的“Hello World”空项目更快。 因此,我希望这可以帮助像我这样的人。

在Visual Studio中,增加编译时间的一个选项是增量链接( / INCREMENTAL )。 它与链接时代码生成( / LTCG )不兼容,所以请记住在发布版本时禁用增量链接。

更快的硬盘。

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

在Linux上(也许还有一些其他的* NIX),你可以通过NOT STARING来加快编译的速度,并改变为另一个 TTY。

这是实验: printf减慢我的程序