如何加速使用大量模板时的g++编译时间

76

这个问题可能有点奇怪,但是我该如何加速g++编译时间?我的C ++代码大量使用boost和模板。我已经尽可能将头文件中的内容移到其他地方,并使用-j选项,但编译(和链接)仍然需要相当长的时间。

是否有任何工具可以分析我的代码并指出编译器的瓶颈?或者可以以某种方式对运行在我的代码上的编译器进行性能分析吗?这将非常好,因为有时我有这样的印象,花费太多时间盯着编译器控制台日志看......


2
可能是https://dev59.com/3XRC5IYBdhLWcg3wOOP1的重复问题。 - strager
1
@Neil:太长了。特别是如果一个广泛使用的模板头文件发生变化,几乎每个代码都需要重新编译。 - Danvil
10个回答

57

对我而言最有用的是:

  • 在RAM文件系统上构建。这在Linux上很简单。您可能也想在RAM文件系统上保留常用头文件的副本(已编译或实际的.h文件)。
  • 预编译头文件。我为每个(主要的)库(例如Boost、Qt、stdlib)都有一个。
  • 尽可能使用声明而不是包含类。这减少了依赖性,从而减少了更改头文件时需要重新编译的文件数量。
  • 并行化make。这通常在每种情况下都有所帮助,但我在全局范围内为make使用了-j3。但请确保在Makefile中正确设置依赖图,否则可能会出现问题。
  • 如果您不测试执行速度或代码大小(且计算机足够快,您不太关心(可能很小的)性能损失),则使用-O0
  • 每次保存时进行编译。有些人不喜欢这样做,但它可以让您尽早看到错误,并且可以在后台完成,减少完成编写并准备测试时需要等待的时间。

1
FYI,我刚试着用一堆boost和stl库制作了一个预编译头文件。我把它放在ram fs上,但没有加速。 - kirill_igum
我认为这将我的编译(不完整)时间降低了约十倍。 - Evan Carslake
2
我怀疑在RAM文件系统中构建是否会改善任何东西。如果你有足够的RAM,Linux内核只是缓存写入和读取,因此从/写入文件应已经使用了RAM。 - Hi-Angel
1
今天,我们不应该再谈论RAM文件系统了,而是应该说:不要将网络文件系统用于任何东西,包括源代码、目标文件、头文件和工具。 - Frank Puck

17

以下是我在一个非常类似的场景下加速构建所做的内容(包括boost、模板和gcc):

  • 使用本地磁盘而不是像NFS这样的网络文件系统进行构建。
  • 升级到较新版本的gcc。
  • 调查distcc
  • 更快的构建系统,尤其是更多的RAM。

4
Icecream/icecc比distcc更好:“与distcc不同,Icecream使用一个中央服务器动态地将编译作业调度到最快的可用服务器。” - Zitrax
1
ccache 只有在重新编译相同的源代码时才有帮助。在开发过程中通常不会出现这种情况,因为 Make 已经能够识别到这一点,所以编译器不会再次启动。ccache 对于不同目录中的相同文件非常有用,例如 Linux 系统上的所有“configure”测试,或者如果您执行了 make clean 或在另一个目录中编译了相同(或类似)的源代码。这在像 Gentoo 这样的源 Linux 发行版中经常发生,在编译失败并重新启动后,编译的工作目录会被清除。在这种情况下,ccache 可以大大加快编译速度。 - IanH
5
ccache 在使用分布式版本控制系统或者在团队中时非常有用,因为在这两种情况下,文件很可能已经被编译过了,即使它来自另一个位置。 - Matthieu M.

17

我假设我们所讨论的是编译一个文件需要的分钟数,即预编译头文件或本地磁盘问题不是问题所在。

深度模板代码(boost等)的长时间编译通常根源于gcc在模板实例化方面的不友好渐近行为,特别是当模拟可变参数模板使用模板默认参数时。

这里有一份文件,将缩短编译时间作为可变参数模板的动机:

cpptruths有一篇文章介绍了gcc-4.5在这方面的表现更好,以及如何使用它的可变参数模板:

如果我没记错的话,BOOST有一种方法来限制伪可变参数的模板默认参数的生成,我认为“g++-DBOOST_MPL_LIMIT_LIST_SIZE=10”应该可以工作(默认值为20)

更新:这里有一个关于如何加速编译的通用技巧的很好的帖子,可能会有用:

更新:这个问题是关于编译模板时的性能问题,采纳的答案也推荐使用gcc-4.5,同时提到了clang作为一个良好的例子:


3
请参考 http://gcc.gnu.org/gcc-4.5/changes.html:使用模板的代码编译时间现在应该与实例化数量成线性比例增长,而不是像原来那样成二次方比例增长,因为模板实例现在是通过哈希表查找的。 - Sebastian Mach

12

如果你需要经常重新编译,ccache 可能会对你有所帮助。它并不会加速编译过程,但是如果你因某些原因进行了无用的重新编译,它会提供一个缓存结果。这可能会给人一种解决错误问题的印象,但有时候重建规则太复杂了,在新的构建过程中确实会得到相同的编译步骤。

另一个想法:如果你的代码可以使用clang编译器编译,则可以使用它。通常比gcc更快。


3

除了其他人所做的和你已经做的(并行构建,编译器选项等),请考虑将模板隐藏在通过接口访问的实现类中。这意味着,不要像这样拥有一个类:

// ClsWithNoTemplates.h file, included everywhere

class ClsWithTemplates
{
    ComplicatedTemplate<abc> member;
    // ...

public:
    void FunctionUsingYourMember();
};

你应该具备以下技能:

// ClsWithNoTemplates.h file:

class ClsWithTemplatesImplementation; // forward declaration
  // definition included in the ClsWithNoTemplates.cpp file
  // this class will have a ComplicatedTemplate<abc> member, but it is only 
  // included in your ClsWithNoTemplates definition file (that is only included once)


class ClsWithNoTemplates
{
     ClsWithTemplatesImplementation * impl; // no templates mentioned anywhere here
public:
    void FunctionUsingYourMember(); // call impl->FunctionUsingYourMember() internally
};

这会稍微改变你的面向对象设计,但是这是有好处的:现在包括“ClsWithNoTemplates”的定义是快速的,并且你只需要(预)编译一次“ClsWithNoTemplates”的定义。

此外,如果你更改了实现代码,任何包含ClsWithNoTemplates.h的代码可能不需要重新定义。

这个改变应该会显著增加你的部分编译时间,并且它还有助于当你的ClsWithNoTemplates是从库文件导出的公共接口时:由于文件在你仅更改实现时不会被更改,你的依赖客户端代码根本不需要重新编译。


3

2

如果有很多文件,您可以通过只使用一个包含所有其他.cpp文件的.cpp文件来加快编译速度。当然,这需要您更加小心处理已经定义为每个文件的宏等,因为它们现在将被其他cpp文件看到。

如果有许多文件,这可以大大减少编译时间。


也许。直到你的编译器耗尽内存并开始交换。 - KeithB
只需合理地组合,我并不是说你应该将300万行代码放在一个文件中。我在一个庞大的项目中使用这种技术,取得了非常好的效果,在某些编译器上甚至可以缩短原始时间的五分之一。 - Zitrax

1

实例化更少的模板和内联函数。尽可能预编译并将其链接,而不是从头开始编译所有内容。确保使用最新版本的GCC。

然而,事实是C++是一种非常复杂的语言,编译它需要相当长的时间。


1

本文描述了一种编译模板代码的方法,类似于“传统”的非模板对象文件。每个模板实例化只需要一个代码开销,可以节省编译和链接时间。


我似乎无法在不登录的情况下下载这篇文章的PDF。您必须实际付款才能获取此论文的访问权限吗? - greatwolf

0
通常,编译过程中最耗费资源的部分是(a)读取源文件(所有文件),和(b)为每个源文件加载编译器到内存中。
假如你有52个源文件(.cc),每个文件包含了47个#include(.h)文件,那么你将要加载编译器52次,并且需要处理2496个文件。根据文件中注释的密度,你可能会花费相当一段时间处理无用字符。(我曾经在一个组织中看到过,头文件的注释占据了66%到90%,只有10%到33%的内容是“有意义”的。增强这些文件可读性的最佳方法就是删除每一个注释,只留下代码。)
仔细审视一下你的程序的物理结构。看看是否可以合并源文件,并简化你的#include文件的层次结构。
几十年前,像IBM这样的公司就已经认识到这一点,并且他们会编写他们的编译器,使得编译器能够接收一个需要编译的文件列表,而不仅仅是一个文件,这样编译器只需要加载一次。

2
“编译”注释需要多长时间?(由预处理器完成,与理解模板元编程相比不应该很复杂吧?) - UncleBens
7
编译器的加载很可能只会在第一次文件加载时发生,之后它几乎肯定会与常见的头文件一起留在磁盘缓存中。计算机结构和速度在过去数十年里发生了很大变化,IBM全盛时期的共识今天可能已经不太有用了。 - Mike Seymour
4
代码注释具有意义,删除它们可能是危险的。如你所说,删除注释并不会使源代码更加“可读”。 - Malfist
@Danvil,关键在于编译器必须读取注释,以获取注释后的可执行行。即使它被预处理器“读取”,它仍然必须被读取,并且您仍然需要支付读取它的成本。 - John R. Strohm
程序代码注释并不总是件好事,但也不总是坏事。需要根据实际情况进行评估和去除。一如既往的,如果有异味,就将其清除。但并不是所有的注释都有异味(尽管像我们在这篇文章中的所有注释都有)。 - osirisgothra
显示剩余4条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接