我从事 STAPL 项目,这是一个高度模板化的 C++ 库。有时,我们必须重新审视所有技术以减少编译时间。在这里,我总结了我们使用的技术。其中一些技术已在上面列出:
查找最耗时的部分
虽然符号长度和编译时间之间没有经过证实的相关性,但我们观察到较小的平均符号大小可以提高所有编译器的编译时间。因此,您的首要目标是找到代码中最大的符号。
方法 1 - 根据大小对符号进行排序
您可以使用nm 命令根据符号的大小列出符号:
nm --print-size --size-sort --radix=d YOUR_BINARY
在此命令中,--radix=d 可让您查看十进制数字的大小(默认为十六进制)。现在通过查看最大的符号,确定您是否可以破坏相应的类并尝试通过将非模板部分分解为基类或将类拆分为多个类来重新设计它。
方法 2 - 根据长度对符号进行排序
您可以运行常规的nm 命令并将其传送到您最喜欢的脚本(AWK、Python 等),以根据符号的长度对符号进行排序。根据我们的经验,与方法 1 相比,此方法确定了使候选人更好的最大问题。
方法 3 - 使用 Templight
“Templight 是一个基于Clang 的工具,用于分析模板实例化的时间和内存消耗,并执行交互式调试会话以了解模板实例化过程”。
您可以通过查看 LLVM 和 Clang (instructions) 并在其上应用 Templight 补丁来安装 Templight。 LLVM 和 Clang 的默认设置是调试和断言,这些会显着影响您的编译时间。似乎 Templight 两者都需要,所以你必须使用默认设置。安装 LLVM 和 Clang 的过程大约需要一个小时左右。
应用补丁后,您可以使用位于您在安装时指定的构建文件夹中的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 Tools,它将这些跟踪转换为其他格式。按照这些instructions 安装 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 { };
T 和 U 都可以有 10 个不同的选项,您已将此类的可能模板实例化增加到 100 个。解决此问题的一种方法是将代码的公共部分抽象为不同的类.另一种方法是使用继承反转(反转类层次结构),但在使用此技术之前请确保您的设计目标没有受到影响。
将非模板代码重构为单个翻译单元
使用这种技术,您可以编译一次公共部分,然后将其与您的其他 TU(翻译单元)链接。
使用外部模板实例化(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 忽略显式实例化,并始终在所有编译单元中编译实例化的类。
使用统一构建
统一构建背后的整个想法是将您使用的所有 .cc 文件包含在一个文件中,并且只编译该文件一次。使用这种方法,您可以避免重新实例化不同文件的公共部分,如果您的项目包含大量公共文件,您可能还会节省磁盘访问。
例如,假设您有三个文件foo1.cc、foo2.cc、foo3.cc,它们都包括来自STL 的tuple。您可以创建一个 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。
这种技术有两个问题:
- 您必须确保被预编译的头文件是稳定的并且不会改变 (you can always change your makefile)
- 每个编译单元只能包含一个 PCH(在大多数编译器上)。这意味着如果要预编译多个头文件,则必须将它们包含在一个文件中(例如,
all-my-headers.hpp)。但这意味着您必须在所有地方都包含新文件。幸运的是,GCC 有解决这个问题的方法。使用-include 并为其提供新的头文件。您可以使用此技术用逗号分隔不同的文件。
例如:
g++ foo.cc -include all-my-headers.hpp
使用未命名或匿名的命名空间
Unnamed namespaces(又名匿名命名空间)可以显着减少生成的二进制文件大小。未命名的命名空间使用内部链接,这意味着在这些命名空间中生成的符号对其他 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%。
更改可见性选项
在较新的编译器中,您可以选择符号在动态共享对象 (DSO) 中可见或不可见。理想情况下,更改可见性可以提高编译器性能、链接时间优化 (LTO) 和生成的二进制大小。如果您查看 GCC 中的 STL 头文件,您会发现它被广泛使用。要启用可见性选择,您需要更改每个函数、每个类、每个变量的代码,更重要的是每个编译器。
借助可见性,您可以从生成的共享对象中隐藏您认为它们是私有的符号。在 GCC 上,您可以通过将 default 或 hidden 传递给编译器的 -visibility 选项来控制符号的可见性。这在某种意义上类似于未命名的命名空间,但以一种更复杂和侵入性的方式。
如果您想指定每个案例的可见性,您必须将以下属性添加到您的函数、变量和类中:
__attribute__((visibility("default"))) void foo1() { }
__attribute__((visibility("hidden"))) void foo2() { }
__attribute__((visibility("hidden"))) class foo3 { };
void foo4() { }
GCC 中的默认可见性是 default (public),这意味着如果将上面的内容编译为共享库 (-shared) 方法,foo2 和类 foo3 在其他 TU 中将不可见 (@987654375 @ 和 foo4 将可见)。如果您使用-visibility=hidden 编译,则只有foo1 可见。甚至foo4 也会被隐藏。
您可以在GCC wiki 上阅读有关可见性的更多信息。