可以使用哪些技术来加快C ++的编译时间?
这个问题出现在Stack Overflow问题C ++编程风格的一些注释中,我很想听听那里有什么想法。
我看到一个相关的问题,为什么C ++编译需要这么长时间?,但这并没有提供很多解决方案。
可以使用哪些技术来加快C ++的编译时间?
这个问题出现在Stack Overflow问题C ++编程风格的一些注释中,我很想听听那里有什么想法。
我看到一个相关的问题,为什么C ++编译需要这么长时间?,但这并没有提供很多解决方案。
Answers:
在此处和此处(也称为不透明指针或句柄类)查看Pimpl习惯用法 。与非抛出交换结合使用时,它不仅可以加快编译速度,而且还可以提高异常安全性函数性。Pimpl习惯用法使您可以减少头文件之间的依赖性,并减少需要完成的重新编译量。
尽可能使用前向声明。如果编译器只需要知道它SomeIdentifier
是结构,指针还是其他东西,就不要包含整个定义,从而迫使编译器执行比其所需更多的工作。这会产生级联效应,使这种方式的速度慢于所需的速度。
在I / O流,特别是著名的减缓构建。如果您需要在头文件中使用它们,请尝试使用#include <iosfwd>
代替,<iostream>
而<iostream>
仅在实现文件中将头包含在#include 中。该<iosfwd>
头只拥有前置声明。不幸的是,其他标准头文件没有相应的声明头文件。
在函数签名中,首选传递引用比传递值。这样就无需在头文件中#include各自的类型定义,并且只需要向前声明该类型。当然,最好使用const引用而不是非const引用,以避免模糊的bug,但这是另一个问题。
使用保护条件以防止头文件在一个翻译单元中被多次包含。
#pragma once
#ifndef filename_h
#define filename_h
// Header declarations / definitions
#endif
通过同时使用编译指示和ifndef,您可以获得纯宏解决方案的可移植性,以及某些编译器在存在pragma once
指令的情况下可以进行的编译速度优化。
通常,代码设计的模块化程度越高,相互依赖性越小,则重新编译所有内容的频率就越少。您还可以减少编译器必须同时跟踪任何单个块的工作量,从而减少了工作量。
这些用于一次编译许多翻译单元的包含标头的公共部分。编译器编译一次,然后保存其内部状态。然后可以快速加载该状态,以抢先编译具有相同头文件集的另一个文件。
请注意,您只能在预编译的头文件中包含很少更改的内容,否则最终可能会比必要的时候进行完整的重建。这是STL标头和其他库包含文件的好地方。
ccache是另一个利用缓存技术来加快速度的实用程序。
许多编译器/ IDE支持使用多个内核/ CPU同时进行编译。在GNU Make(通常与GCC一起使用)中,使用-j [N]
选项。在Visual Studio中,首选项下有一个选项允许它并行构建多个项目。您还可以将/MP
选项用于文件级并行处理,而不仅仅是项目级并行处理。
其他并行实用程序:
编译器尝试优化的次数越多,其工作就越困难。
将修改频率较低的代码移到库中可以减少编译时间。通过使用共享库(.so
或.dll
),您还可以减少链接时间。
更多的RAM,更快的硬盘驱动器(包括SSD)和更多的CPU /内核都将影响编译速度。
我从事的是STAPL项目,这是一个包含大量模板的C ++库。有时,我们必须重新审视所有技术以减少编译时间。在这里,我总结了我们使用的技术。其中一些技术已在上面列出:
尽管没有证明符号长度和编译时间之间的相关性,但是我们已经观察到,较小的平均符号大小可以改善所有编译器的编译时间。因此,您的第一个目标就是在代码中找到最大的符号。
您可以使用nm
命令根据符号的大小列出符号:
nm --print-size --size-sort --radix=d YOUR_BINARY
在此命令中 --radix=d
,您可以以十进制数字查看大小(默认为十六进制)。现在,通过查看最大的符号,确定是否可以打破相应的类,并尝试通过将基类中未模板化的部分分解为因子,或将类拆分为多个类来重新设计它。
您可以运行常规nm
命令并将其通过管道发送到您喜欢的脚本(AWK,Python等),以根据符号的长度对符号进行排序。根据我们的经验,此方法确定了使候选人比方法1更好的最大麻烦。
“ Templight是锵基于的工具,用于分析模板实例化的时间和内存消耗,并执行交互式调试会话以对模板实例化过程进行自省。”
您可以通过签出LLVM和Clang(说明)并在其上应用Templight补丁来安装Templight 。LLVM和Clang的默认设置是在调试和断言上,它们会极大地影响您的编译时间。看起来Templight两者都需要,因此您必须使用默认设置。安装LLVM和Clang的过程大约需要一个小时。
应用补丁程序后,您可以使用templight++
安装时指定的build文件夹中的代码来编译代码。
确保这templight++
在您的PATH中。现在进行编译,将以下开关添加到CXXFLAGS
Makefile中或命令行选项中:
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
可以使用kcachegrind打开生成的callgrind文件,您可以在其中跟踪耗时/内存最多的实例。
尽管没有减少模板实例化数量的确切解决方案,但是有一些准则可以帮助您:
例如,如果您有一堂课,
template <typename T, typename U>
struct foo { };
并且,T
和U
都可以有10个不同的选项,您已经将该类的可能的模板实例化增加到100。解决此问题的一种方法是将代码的公共部分抽象到另一个类。另一种方法是使用继承反转(反转类层次结构),但是在使用此技术之前,请确保不损害您的设计目标。
使用此技术,您可以一次编译公共部分,然后将其与其他TU(转换单元)链接。
如果您知道一个类的所有可能实例化,则可以使用此技术在不同的翻译单元中编译所有个案。
例如,在:
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此时将忽略显式实例化,并始终在所有编译单元中编译实例化的类。
统一构建的整个思想是将您使用的所有.cc文件包含在一个文件中,并且仅对该文件进行一次编译。使用这种方法,可以避免重新实例化不同文件的公共部分,并且如果您的项目包含许多公共文件,则可能还可以节省磁盘访问。
举个例子,假设你有三个文件foo1.cc
,foo2.cc
,foo3.cc
和它们都包括tuple
从STL。您可以创建foo-all.cc
如下所示的:
#include "foo1.cc"
#include "foo2.cc"
#include "foo3.cc"
您只编译一次该文件,并可能减少三个文件之间的通用实例。通常很难预测这种改善是否显着。但是一个明显的事实是,您将在构建中失去并行性(您不能再同时编译三个文件)。
此外,如果这些文件中的任何一个恰好占用大量内存,则在编译结束之前您实际上可能会用完内存。在某些编译器(例如GCC)上,由于内存不足,这可能会导致ICE(内部编译器错误)编译器。因此,除非您了解所有优点和缺点,否则不要使用此技术。
通过将头文件编译为编译器可识别的中间表示形式,预编译头(PCH)可以节省大量时间。要生成预编译的头文件,只需要使用常规编译命令来编译头文件。例如,在GCC上:
$ g++ YOUR_HEADER.hpp
这将在同一文件夹中生成一个YOUR_HEADER.hpp.gch file
(.gch
是GCC中PCH文件的扩展名)。这意味着,如果您将其包含YOUR_HEADER.hpp
在其他文件中,则编译器将使用您的YOUR_HEADER.hpp.gch
而不是YOUR_HEADER.hpp
以前在同一文件夹中。
此技术有两个问题:
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%。
在较新的编译器中,您可以选择在动态共享对象(DSO)中可见或不可见的符号。理想情况下,更改可见性可以提高编译器性能,链接时间优化(LTO)和生成的二进制大小。如果在GCC中查看STL头文件,您会发现它已被广泛使用。要启用可见性选择,您需要针对每个函数,每个类,每个变量(更重要的是针对每个编译器)更改代码。
借助可见性,您可以从生成的共享对象中隐藏您认为私有的符号。在GCC上,您可以通过将默认值或隐藏值传递给-visibility
编译器的选项来控制符号的可见性。从某种意义上说,这与未命名的命名空间类似,但是更加复杂和侵入性。
如果要指定每种情况的可视性,则必须在函数,变量和类中添加以下属性:
__attribute__((visibility("default"))) void foo1() { }
__attribute__((visibility("hidden"))) void foo2() { }
__attribute__((visibility("hidden"))) class foo3 { };
void foo4() { }
GCC中的默认可见性为默认(公共),这意味着如果将上述内容编译为共享库(-shared
)方法,则foo2
该类foo3
在其他TU中将不可见(foo1
并且foo4
将可见)。如果您使用进行编译,-visibility=hidden
则只会foo1
显示。甚至foo4
会被隐藏。
您可以在GCC Wiki上阅读有关可见性的更多信息。
我建议从“内部游戏,独立游戏设计和编程”中推荐以下文章:
当然,它们已经很老了-您必须使用最新版本(或可用的版本)重新测试所有内容,以获得真实的结果。无论哪种方式,它都是思想的良好来源。
过去对我来说效果很好的一种技术是:不要独立地编译多个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
<core-count> + N
并行编译的子列表,其中子列表N
是一些合适的整数(取决于系统内存和机器的使用方式)。
关于此主题有一整本书,标题为《大规模C ++软件设计》(由John Lakos撰写)。
这本书比模板早,所以在那本书的内容上加上“也使用模板,会使编译器变慢”。
我将链接到另一个答案:如何减少编译时间,并减少Visual C ++项目(本机C ++)的链接时间?。我想补充一点,但经常引起问题的是使用预编译的头文件。但是,请仅将它们用于几乎不变的部分(例如GUI工具包标题)。否则,与最终节省您的时间相比,它们将花费您更多的时间。
另一个选择是,当您使用GNU make时,请打开以下-j<N>
选项:
-j [N], --jobs[=N] Allow N jobs at once; infinite jobs with no arg.
我通常在这里,3
因为我在这里有一个双核。如果它们之间没有依赖性,它将为不同的翻译单元并行运行编译器。链接不能并行完成,因为只有一个链接程序进程将所有目标文件链接在一起。
但是链接器本身可以被线程化,这就是ELF链接器的作用。它是经过优化的线程化C ++代码,据说它链接ELF目标文件的速度比旧版本快(实际上已包含在binutils中)。GNU gold
ld
这里有一些:
make -j2
是一个很好的例子)。-O1
比GCC快得多)。-O2
-O3
-j12
各地-j18
均大大高于-j8
,就像你的建议。我想知道在内存带宽成为限制因素之前可以拥有多少个内核...
-j
实际核心数的2倍。
一旦应用了上述所有代码技巧(前向声明,将头包含在公共头中的数量减少到最小,使用Pimpl将大多数细节推送到实现文件中...),并且在语言上没有其他好处,请考虑您的构建系统。如果使用Linux,请考虑使用distcc(分布式编译器)和ccache(缓存编译器)。
第一个是distcc,它在本地执行预处理器步骤,然后将输出发送到网络中的第一个可用的编译器。它在网络中所有已配置的节点中需要相同的编译器和库版本。
后者ccache是编译器缓存。它再次执行预处理器,然后检查内部数据库(保存在本地目录中)是否已使用相同的编译器参数编译了该预处理器文件。如果是这样,它将仅弹出二进制文件并从编译器的第一次运行中输出。
两者可以同时使用,因此,如果ccache没有本地副本,则可以通过distcc通过网络将其通过网络发送到另一个节点,否则它可以直接注入解决方案而无需进一步处理。
当我大学毕业时,我看到的第一个真正有价值的C ++代码在它们之间的标头定义位置包含了这些奥秘的#ifndef ... #endif指令。我问那个以非常幼稚的方式编写代码的有关这些总体问题的家伙,并被介绍给大规模编程领域。
回到这一点,使用指令来防止重复的标头定义是我减少编译时间的第一件事。
更多RAM。
有人在另一个答案中谈到了RAM驱动器。我使用80286和Turbo C ++(显示年龄)进行了此操作,结果非常出色。机器崩溃时数据丢失也是如此。
尽可能使用前向声明。如果类声明仅使用对类型的指针或引用,则可以向前声明它,并将类型的标头包括在实现文件中。
例如:
// 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
}
如果包含足够多的内容,则意味着预处理器的工作量将大大减少。
用
#pragma once
头文件的顶部,因此,如果它们在翻译单元中被多次包含,则头文件的文本将仅被包含并解析一次。
仅出于完整性考虑:构建可能会很慢,因为构建系统很笨拙,而且编译器需要很长时间才能完成工作。
阅读递归使被视为有害(PDF),以在Unix环境中讨论此主题。
我有一个使用RAM驱动器的想法。事实证明,对于我的项目而言,毕竟没有什么太大的不同。但是那时它们还很小。试试吧!我想听听它有多大帮助。
动态链接(.so)比静态链接(.a)快得多。特别是当您的网络驱动器速度较慢时。这是因为您在.a文件中拥有所有需要处理和写出的代码。另外,需要将更大的可执行文件写出到磁盘。
在Linux(可能还有其他一些* NIXes)上,您可以通过在输出端不加星标并更改为另一个 TTY 来真正加快编译速度。
这是实验:printf会使我的程序变慢
尽管不是“技术”,但我无法弄清楚带有许多源文件的Win32项目如何比“ Hello World”空项目更快地编译。因此,我希望这对像我这样的人有所帮助。
在Visual Studio中,增加编译时间的一种方法是增量链接(/ INCREMENTAL)。它与链接时代码生成(/ LTCG)不兼容,因此请记住在发布版本时禁用增量链接。
/INCREMENTAL
在调试模式下启用
从Visual Studio 2017开始,您可以具有一些有关耗时的编译器指标。
将这些参数添加到项目属性窗口中的C / C ++->命令行(其他选项)中:
/Bt+ /d2cgsummary /d1reportTime
您可以在这篇文章中获得更多信息。