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

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

我看到过一个相关的问题,为什么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
}

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

其他回答

Use

#pragma once

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

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

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

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

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

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

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

我从事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上阅读更多关于可见性的信息。