可以使用哪些技术来加快c++编译时间?
这个问题出现在一些关于Stack Overflow问题c++编程风格的评论中,我很有兴趣听听有什么想法。
我看到过一个相关的问题,为什么c++编译要花这么长时间?,但这并没有提供很多解决方案。
可以使用哪些技术来加快c++编译时间?
这个问题出现在一些关于Stack Overflow问题c++编程风格的评论中,我很有兴趣听听有什么想法。
我看到过一个相关的问题,为什么c++编译要花这么长时间?,但这并没有提供很多解决方案。
当前回答
我从事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(2005及以后版本)和GCC都支持多处理器编译。当然,如果你有硬件,这是可以实现的。
我有个关于使用内存驱动器的主意。事实证明,对于我的项目来说,这并没有太大的区别。但它们仍然很小。试一试!我很想知道这有多大帮助。
有一本书是关于这个主题的,书名是《大规模c++软件设计》(由John Lakos撰写)。
这本书的年代早于模板,所以在书的内容中加上“使用模板也会使编译器变慢”。
在Linux(也许还有其他一些* nix)上,您可以通过不盯着输出并更改到另一个TTY来加速编译。
下面是实验:printf使我的程序变慢
我将链接到我的另一个答案:如何减少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中)。