【问题标题】:Identify slow-to-compile function识别编译缓慢的函数
【发布时间】:2015-05-09 06:26:01
【问题描述】:

我有一些需要大量编译的 cpp 文件。它们包含一些基本的类/代码,以及一些模板,但没有任何东西可以证明编译时间大约为几十秒。

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

这就是 gcc 关于编译时间的说法。如何找到导致编译时间过长的库/包含/函数调用?

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
...

Profiling the C++ compilation process 处理识别慢速文件,但我需要更细粒度的信息才能找到罪魁祸首

(其他文件/项目编译都是毫秒/秒,所以不是电脑资源的问题,我用的是gcc 4.9.1)

【问题讨论】:

  • 使用#if 0对文件进行二进制搜索?即,不要编译后半部分。如果现在很快,请不要编译后半部分。如果相反仍然很慢,请不要编译后面的 3/4。如果您的代码有 1024 个函数(!),则问题在 10 次迭代中解决(如果您的文件有 1024 个函数,则可能文件大小有问题)。
  • @Yakk:可能没那么容易。没有字体一半,后半部分可能无法编译。此外,如果代码确实大量使用模板,那么长时间可能是由于两部分之间的交互。
  • @rodrigo 是的,就是这么简单:看来您将不同的算法投射到我的描述中。我只建议编译文件的前缀——你可能会得到链接器错误,但不会出现编译器错误。最先引起交互而导致编译器时间变长的代码段将是找到的代码段(“边缘”代码段,包含它会使时间变长,而删除它会保持合理的时间)。当然,它并不能解决问题来自于大和小,或小和大的问题,但无论如何这种情况往往比较少。
  • 导致爆炸的边缘代码可能不是“应归咎于”的代码,而是触发问题的代码。可能需要对其进行自省。
  • 如果我是你,我会开始#if 0 Boost 相关的部分。当然,它们可能是“无辜的”,但根据我的经验,Boost 库通常非常依赖 TMP,这可能会导致编译时间变慢。

标签: c++ performance gcc boost compilation


【解决方案1】:

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

当你包含了太多的头文件并且这些头文件包含了太多自己的头文件时,这仅仅意味着编译器需要做很多工作来加载所有这些文件,并且会花费过多的时间在它必须对所有代码执行的处理过程中,无论它是否实际使用,例如预处理、词法分析、AST 构建等。当代码分布在大量小标题,因为性能非常受 I/O 限制(仅从硬盘获取和读取文件就浪费了大量时间)。不幸的是,Boost 库往往采用这种结构。

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

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

第二个问题是模板实例化的问题。在您来自 GCC 的时间报告中,应该在某个地方报告模板实例化阶段的时间值。如果这个数字很高,只要代码中涉及大量模板元编程,就会出现这个数字,那么您将需要解决这个问题。一些模板繁重的代码编译起来非常缓慢的原因有很多,包括深度递归的实例化模式、过于花哨的 Sfinae 技巧、滥用类型特征和概念检查,以及老式的过度设计的通用代码。但是也有一些简单的技巧可以解决很多问题,比如使用未命名的命名空间(以避免浪费所有时间为在翻译单元之外不需要可见的实例化生成符号)和专门化类型特征或概念检查模板(基本上“短路”进入它们的大部分花哨的元编程)。模板实例化的另一个潜在解决方案是使用“extern templates”(来自 C++11)来控制应在何处实例化特定模板实例化(例如,在单独的 cpp 文件中),并避免在使用它的任何地方重新实例化它。

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

  • 您可以使用“Templight”分析工具(及其辅助“Templight-tools”来处理跟踪)。这又是一个基于 Clang 的工具,可以用作 Clang 编译器的替代品(该工具实际上是一个经过检测的成熟编译器),它将生成编译期间发生的所有模板实例化的完整配置文件,包括每个花费的时间(以及可选的内存消耗估计,尽管这会影响计时值)。跟踪可以稍后转换为 Callgrind 格式并在 KCacheGrind 中可视化,只需阅读templight-tools page 上的描述即可。这基本上可以像典型的运行时分析器一样使用,但用于分析编译大量模板代码时的时间和内存消耗。
  • 找出最严重的违规者的一种更基本的方法是创建测试源文件,以实例化您怀疑导致编译时间过长的特定模板。然后,您编译这些文件,对其计时,并尝试以您的方式(可能以“二分搜索”方式)找出最严重的违规者。

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

【讨论】:

    【解决方案2】:

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

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

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

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

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

    一旦您分离出哪个源文件(如果有)是违规者,然后有选择地从中删除一些部分,以找出其中的哪些代码是问题所在。正如 Yakk 在评论中所说,使用“二进制搜索”来消除文件中的函数。我建议先删除整个函数(缩小到有问题的函数),然后在有问题的函数中使用相同的技术。

    它确实有助于结构化您的代码,因此每个文件的函数数量相当少。这减少了为一个功能的微小更改而重新构建大文件的需要,并有助于在将来更轻松地隔离此类问题。

    【讨论】:

      猜你喜欢
      • 2012-05-14
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-09-27
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-02-21
      相关资源
      最近更新 更多