识别编译缓慢的函数



我有一些cpp文件,编译起来需要很多时间。它们包含一些基本类/代码,以及一些模板,但没有任何东西可以证明编译时间在几十秒左右。

我确实使用了几个外部库(boost/occv)

这就是gcc关于编译时间的说法。我如何才能找到导致可怕编译时间的库/include/函数调用?

Execution times (seconds)
phase setup             :   0.00 ( 0%) usr   0.00 ( 0%) sys   0.01 ( 0%) wall    1445 kB ( 0%) ggc
phase parsing           :   6.69 (46%) usr   1.61 (60%) sys  12.14 (47%) wall  488430 kB (66%) ggc
phase lang. deferred    :   1.59 (11%) usr   0.36 (13%) sys   3.83 (15%) wall   92964 kB (13%) ggc
phase opt and generate  :   6.25 (43%) usr   0.72 (27%) sys  10.09 (39%) wall  152799 kB (21%) ggc
|name lookup            :   1.05 ( 7%) usr   0.28 (10%) sys   2.01 ( 8%) wall   52063 kB ( 7%) ggc
|overload resolution    :   0.83 ( 6%) usr   0.18 ( 7%) sys   1.48 ( 6%) wall   42377 kB ( 6%) ggc
...

评测C++编译过程涉及识别慢文件,但我需要更细粒度的信息来找到罪魁祸首

(其他文件/项目以毫秒/秒的速度编译,所以这不是计算机资源的问题。我使用gcc 4.9.1)

基本上有两件事会导致编译时间过长:包含太多和模板太多。

当你包含了太多的头,而这些头又包含了自己的太多头时,这只意味着编译器要加载所有这些文件还有很多工作要做,它将在所有代码上花费过多的时间进行处理,而不管它是否实际使用,比如预处理、词法分析、AST构建,当代码分布在大量的小标题上时,这可能会特别有问题,因为性能在很大程度上受I/O限制(仅从硬盘获取和读取文件就浪费了大量时间)。不幸的是,Boost库的结构往往是这样的。

以下是解决此问题的几种方法或工具:

  • 您可以使用"包含您使用的内容"工具。这是一个基于Clang的分析工具,它主要查看您在代码中实际使用的内容,以及这些内容来自哪些标头,然后报告您可以通过删除某些不必要的包含、使用前向声明或用更细粒度的标头替换更广泛的"一体式"标头来进行的任何潜在优化
  • 大多数编译器都有转储预处理源的选项(在GCC/Clang上,它是-E-E -P选项,或者直接使用GCC的C预处理器程序cpp)。您可以获取源文件,注释出不同的include语句或include语句组,并转储经过预处理的源代码,以查看这些不同标头所引入的代码总量(还可以使用行计数命令,如$ g++ -E -P my_source.cpp | wc -l)。这可以帮助您从要处理的代码行数中识别哪些标头是最严重的违规者。然后,你可以看看你能做些什么来避免它们或以某种方式缓解问题
  • 您也可以使用预编译的标头。这是大多数编译器支持的一项功能,使用该功能,您可以指定要预编译的某些头文件(尤其是经常包含的"一体式"头文件),以避免为包含它们的每个源文件重新解析它们
  • 如果您的操作系统支持它,您可以使用ram磁盘来存储代码和外部库的头。这基本上占用了RAM内存的一部分,使其看起来像一个普通的硬盘/文件系统。这可以通过减少I/O延迟来显著减少编译时间,因为所有的头文件和源文件都是从RAM内存读取的,而不是从实际的硬盘读取的

第二个问题是模板实例化的问题。在GCC的时间报告中,应该有一个为模板实例化阶段报告的时间值。如果这个数字很高,那么一旦代码中涉及到大量的模板元编程,你就需要解决这个问题。一些重模板的代码编译速度慢得令人痛苦,原因有很多,包括深度递归的实例化模式、过于花哨的Sfinae技巧、滥用类型特征和概念检查,以及过时的通用代码。但也有一些简单的技巧可以解决很多问题,比如使用未命名的名称空间(以避免为翻译单元外不需要显示的实例化生成符号而浪费所有时间)和专门化类型特征或概念检查模板(基本上"缩短"了其中的许多高级元编程)。模板实例化的另一个潜在解决方案是使用"外部模板"(来自C++11)来控制特定模板实例化应该在哪里实例化(例如,在单独的cpp文件中),并避免在使用的任何地方重新实例化。

这里有几种方法或工具可以帮助您识别瓶颈:

  • 您可以使用"Templight"分析工具(及其用于处理轨迹的辅助"Templight工具")。这也是一个基于Clang的工具,可以用作Clang编译器的替代工具(该工具实际上是一个插入指令的全面编译器),它将生成编译过程中发生的所有模板实例化的完整概要文件,包括在每个实例化上花费的时间(以及可选的内存消耗估计,尽管这会影响定时值)。这些痕迹稍后可以转换为Callgrind格式,并在KCacheGrind中可视化,只需阅读templight工具页面上的描述即可。这基本上可以像典型的运行时探查器一样使用,但用于在编译模板繁重的代码时分析时间和内存消耗
  • 找到最严重的违规者的一种更基本的方法是创建测试源文件,实例化您怀疑导致编译时间过长的特定模板。然后,你编译这些文件,计时,并尝试以你的方式(可能是以"二进制搜索"的方式)找出最严重的罪犯

但即使有这些技巧,识别模板实例化瓶颈也比实际解决它们更容易。所以,祝你好运。

如果没有关于如何组织和构建源文件的信息,这是无法完全回答的,所以只需要一些一般性的观察。

  1. 模板实例化会大大增加编译时间,尤其是在为多个源文件中的每个文件中的几个不同类型/参数实例化复杂模板的情况下。显式模板实例化的方案(即确保模板只在少数源文件中实例化,而不是在所有源文件中)可以减少这种情况下的编译时间(以及链接时间和可执行文件大小)。您需要阅读编译器文档以了解如何做到这一点——默认情况下不一定会发生这种情况,这可能意味着重新构建代码以支持它
  2. 在许多源文件中,无论是否需要,#included的头文件都会增加编译时间。我看到一个案例,一个团队成员写了一个"globals.h"#include表示所有内容,#include表示所有内容——(在一个大型项目中)构建时间增加了一个数量级。这是双重打击——每个源文件的编译时间都会增加,并且会乘以直接或间接#include该标头的源文件的数量。如果启用"预编译头"之类的功能会加快第二个和后续构建的构建时间,那么这可能是一个原因。(您可能会将预编译的头文件视为一种解决方案,但请记住,使用它们还有其他权衡)
  3. 如果您正在使用外部库,请检查以确保它们是本地安装和配置。一个编译过程默默地在互联网上查找某些组件(例如某个远程服务器上的硬编码头文件名)会变慢事情相当严重。第三方库经常发生这种情况,你会感到惊讶

除此之外,发现问题的技术取决于构建过程的结构。

如果您使用的是单独编译源文件的makefile(或其他方式),那么请使用某种方式为单个编译和链接命令计时。请记住,这可能是链接时间占主导地位。

如果使用单个编译命令(例如,在一个命令中对多个文件调用gcc),则将其分解为每个源文件的单独命令。

一旦您隔离了哪个源文件(如果有的话)是罪魁祸首,那么就有选择地从中删除一些部分,以找出其中的哪些代码是问题所在。正如Yakk在评论中所说,使用"二进制搜索"来消除文件中的函数。我建议先删除整个函数(将范围缩小到有问题的函数),然后在有问题的功能中使用相同的技术。

它确实有助于构建代码,使每个文件的函数数量相对较少。这减少了为一个函数的微小更改重新构建大文件的需要,并有助于在未来更容易地隔离此类问题。

最新更新