识别慢速编译函数
Identify slow-to-compile function
我有一些需要大量编译的 cpp 文件。它们包含一些基本的 classes/code,带有一些模板,但没有任何东西可以证明编译时间在几十秒的数量级上是合理的。
我确实使用了几个外部库 (boost/opencv)
这就是 gcc 关于编译时间的说法。我怎样才能找到导致可怕的编译时间的 library/include/function 调用?
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 处理识别慢文件,但我需要更细粒度的信息来找到罪魁祸首
(其他files/projects在milliseconds/seconds编译,所以不是电脑资源的问题,我用的是gcc 4.9.1)
基本上有两件事会导致编译时间过长:太多的包含和太多的模板。
当你包含太多 headers 而这些 headers 包含太多它们自己的 headers 时,这只意味着编译器有很多工作要做加载所有这些文件,它会花费过多的时间在它必须对所有代码进行的处理过程中,无论它是否实际使用过,比如 pre-processing、词法分析、AST 构建等.. 当代码分布在大量小的 headers 上时,这尤其有问题,因为性能非常受 I/O 限制(大量时间浪费在从 hard-disk).不幸的是,Boost 库往往以这种方式构建。
这里有几种方法或工具可以解决这个问题:
- 您可以使用“include-what-you-use”工具。这是一个 Clang-based 分析工具,主要查看您在代码中实际使用的内容,以及 headers 这些内容来自哪些内容,然后报告您可以通过删除某些不必要的包含进行的任何潜在优化,改用 forward-declarations,或者用更广泛的 fine-grained headers.
替换更广泛的 "all-in-one" headers
- 大多数编译器都有转储预处理源的选项(在 GCC / Clang 上,它是
-E
或 -E -P
选项,或者直接使用 GCC 的 C 预处理器程序 cpp
)。您可以获取源文件并注释掉不同的 include 语句或 include 语句组,并转储预处理的源代码以查看这些不同 headers 引入的代码总量(并且可能使用行计数命令,例如$ g++ -E -P my_source.cpp | wc -l
)。这可以帮助您从要处理的大量代码行中识别出哪些 headers 是最严重的问题。然后,您可以了解如何避免它们或以某种方式缓解问题。
- 您也可以使用 pre-compiled headers。这是大多数编译器都支持的功能,您可以使用它指定某些 headers(尤其是 oft-included "all-in-one" headers)为 pre-compiled 以避免 re-parsing 它们适用于包含它们的每个源文件。
- 如果您的 OS 支持它,您可以为您的代码使用 ram-disk 并为您的外部库使用 headers。这实际上占用了您的 RAM 内存的一部分,并使其看起来像普通的 hard-disk / file-system。这可以通过减少 I/O 延迟来显着减少编译时间,因为所有 headers 和源文件都是从 RAM 内存中读取的,而不是实际的 hard-disk.
第二个问题是模板实例化。在 GCC 的时间报告中,模板实例化阶段的某处应该报告一个时间值。如果该数字很高,只要代码中涉及大量模板 meta-programming,它就会很高,那么您将需要解决该问题。一些 template-heavy 代码的编译速度非常慢的原因有很多,包括深度递归实例化模式、过于花哨的 Sfinae 技巧、滥用 type-traits 和概念检查,以及良好的旧时尚 over-engineered 通用代码。但是也有一些简单的技巧可以解决很多问题,比如使用未命名的命名空间(以避免浪费所有时间为实际上不需要在翻译单元外可见的实例化生成符号)和专门化 type-traits或概念检查模板(基本上 "short-circuit" 进入其中的许多奇特的 meta-programming)。模板实例化的另一个潜在解决方案是使用“extern templates”(来自 C++11)来控制特定模板实例化应在何处实例化(例如,在单独的 cpp 文件中)并避免在任何地方使用 re-instantiating已经用过了。
这里有一些方法或工具可以帮助您识别瓶颈:
- 您可以使用“Templight" profiling tool (and its auxiliary "Templight-tools" for dealing with the traces). This is again a Clang-based tool that can be used as a drop-in replacement for the Clang compiler (the tool is actually an instrumented full-blown compiler) and it will generate a complete profile of all the template instantiations that occur during compilation, including the time spent on each (and optionally, memory consumption estimates, although this will affect the timing values). The traces can later be converted to a Callgrind format and be visualized in KCacheGrind, just read the description on that on the templight-tools page。这基本上可以像典型的 run-time 分析器一样使用,但用于分析编译 template-heavy 代码时的时间和内存消耗。
- 寻找最严重违规者的一种更基本的方法是创建测试源文件,这些文件实例化您怀疑导致编译时间长的特定模板。然后,你编译这些文件,计时,并尝试以你的方式(可能以 "binary search" 方式)对付最严重的违规者。
但即使使用这些技巧,识别模板实例化瓶颈也比实际解决它们更容易。那么,祝你好运。
如果没有关于您的源文件是如何组织和构建的信息,就无法完全回答这个问题,所以只是一些一般性的观察。
- 模板实例化会大大增加编译时间,特别是如果在多个源文件中的每一个文件中为几个不同的 types/parameters 实例化了复杂的模板。显式模板实例化的方案(即确保模板仅在几个源文件而不是所有源文件中实例化)可以减少这种情况下的编译时间(以及 link 时间和可执行文件大小)。您需要阅读编译器文档以了解如何执行此操作 - 它不一定会默认发生,并且可能意味着重构您的代码以支持它。
- Header 文件在许多源文件中
#include
d,无论是否需要,往往会增加编译时间。我看到一个案例,其中一个团队成员写了一个 "globals.h"
,#include
d 一切,#include
d 无处不在 - 构建时间(在一个大项目中)增加了一个数量级震级。这是一个双重打击 - 每个源文件的编译时间都增加了,并且乘以直接或间接 #include
即 header 的源文件的数量。如果打开 "precompiled headers" 之类的功能导致第二次和后续构建的构建时间 speed-up,这可能是一个贡献者。 (您可能会将预编译的 header 视为解决此问题的方法,但请记住还有其他 trade-offs 可以使用它们)。
- 如果您使用的是外部库,请检查以确保它们是
在本地安装和配置。一个编译过程
默默地在互联网上寻找一些组件(例如
hard-coded header 某些远程服务器上的文件名)会变慢
事情相当大。您会惊讶于 third-party 图书馆发生这种情况的频率。
除此之外,查找问题的技术取决于构建过程的结构。
如果您使用的是单独编译源文件的 makefile(或其他方式),则使用某种方式来为各个编译和 linking 命令计时。请记住,可能是 link 时间占主导地位。
如果您使用单个编译命令(例如,在一个命令中对多个文件调用 gcc),则将其分解为每个源文件的单独命令。
一旦您确定了哪个源文件(如果有)是违规者,然后有选择地从中删除一些部分以找出其中的哪些代码有问题。正如 Yakk 在评论中所说,为此使用 "binary search" 来消除文件中的函数。我建议先删除整个函数(以缩小到有问题的函数),然后在有问题的函数中使用相同的技术。
它确实有助于构建您的代码,因此每个文件的函数数量相当少。这减少了为一个功能的微小变化重建大文件的需要,并有助于将来更容易地隔离此类问题。
我有一些需要大量编译的 cpp 文件。它们包含一些基本的 classes/code,带有一些模板,但没有任何东西可以证明编译时间在几十秒的数量级上是合理的。
我确实使用了几个外部库 (boost/opencv)
这就是 gcc 关于编译时间的说法。我怎样才能找到导致可怕的编译时间的 library/include/function 调用?
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 处理识别慢文件,但我需要更细粒度的信息来找到罪魁祸首
(其他files/projects在milliseconds/seconds编译,所以不是电脑资源的问题,我用的是gcc 4.9.1)
基本上有两件事会导致编译时间过长:太多的包含和太多的模板。
当你包含太多 headers 而这些 headers 包含太多它们自己的 headers 时,这只意味着编译器有很多工作要做加载所有这些文件,它会花费过多的时间在它必须对所有代码进行的处理过程中,无论它是否实际使用过,比如 pre-processing、词法分析、AST 构建等.. 当代码分布在大量小的 headers 上时,这尤其有问题,因为性能非常受 I/O 限制(大量时间浪费在从 hard-disk).不幸的是,Boost 库往往以这种方式构建。
这里有几种方法或工具可以解决这个问题:
- 您可以使用“include-what-you-use”工具。这是一个 Clang-based 分析工具,主要查看您在代码中实际使用的内容,以及 headers 这些内容来自哪些内容,然后报告您可以通过删除某些不必要的包含进行的任何潜在优化,改用 forward-declarations,或者用更广泛的 fine-grained headers. 替换更广泛的 "all-in-one" headers
- 大多数编译器都有转储预处理源的选项(在 GCC / Clang 上,它是
-E
或-E -P
选项,或者直接使用 GCC 的 C 预处理器程序cpp
)。您可以获取源文件并注释掉不同的 include 语句或 include 语句组,并转储预处理的源代码以查看这些不同 headers 引入的代码总量(并且可能使用行计数命令,例如$ g++ -E -P my_source.cpp | wc -l
)。这可以帮助您从要处理的大量代码行中识别出哪些 headers 是最严重的问题。然后,您可以了解如何避免它们或以某种方式缓解问题。 - 您也可以使用 pre-compiled headers。这是大多数编译器都支持的功能,您可以使用它指定某些 headers(尤其是 oft-included "all-in-one" headers)为 pre-compiled 以避免 re-parsing 它们适用于包含它们的每个源文件。
- 如果您的 OS 支持它,您可以为您的代码使用 ram-disk 并为您的外部库使用 headers。这实际上占用了您的 RAM 内存的一部分,并使其看起来像普通的 hard-disk / file-system。这可以通过减少 I/O 延迟来显着减少编译时间,因为所有 headers 和源文件都是从 RAM 内存中读取的,而不是实际的 hard-disk.
第二个问题是模板实例化。在 GCC 的时间报告中,模板实例化阶段的某处应该报告一个时间值。如果该数字很高,只要代码中涉及大量模板 meta-programming,它就会很高,那么您将需要解决该问题。一些 template-heavy 代码的编译速度非常慢的原因有很多,包括深度递归实例化模式、过于花哨的 Sfinae 技巧、滥用 type-traits 和概念检查,以及良好的旧时尚 over-engineered 通用代码。但是也有一些简单的技巧可以解决很多问题,比如使用未命名的命名空间(以避免浪费所有时间为实际上不需要在翻译单元外可见的实例化生成符号)和专门化 type-traits或概念检查模板(基本上 "short-circuit" 进入其中的许多奇特的 meta-programming)。模板实例化的另一个潜在解决方案是使用“extern templates”(来自 C++11)来控制特定模板实例化应在何处实例化(例如,在单独的 cpp 文件中)并避免在任何地方使用 re-instantiating已经用过了。
这里有一些方法或工具可以帮助您识别瓶颈:
- 您可以使用“Templight" profiling tool (and its auxiliary "Templight-tools" for dealing with the traces). This is again a Clang-based tool that can be used as a drop-in replacement for the Clang compiler (the tool is actually an instrumented full-blown compiler) and it will generate a complete profile of all the template instantiations that occur during compilation, including the time spent on each (and optionally, memory consumption estimates, although this will affect the timing values). The traces can later be converted to a Callgrind format and be visualized in KCacheGrind, just read the description on that on the templight-tools page。这基本上可以像典型的 run-time 分析器一样使用,但用于分析编译 template-heavy 代码时的时间和内存消耗。
- 寻找最严重违规者的一种更基本的方法是创建测试源文件,这些文件实例化您怀疑导致编译时间长的特定模板。然后,你编译这些文件,计时,并尝试以你的方式(可能以 "binary search" 方式)对付最严重的违规者。
但即使使用这些技巧,识别模板实例化瓶颈也比实际解决它们更容易。那么,祝你好运。
如果没有关于您的源文件是如何组织和构建的信息,就无法完全回答这个问题,所以只是一些一般性的观察。
- 模板实例化会大大增加编译时间,特别是如果在多个源文件中的每一个文件中为几个不同的 types/parameters 实例化了复杂的模板。显式模板实例化的方案(即确保模板仅在几个源文件而不是所有源文件中实例化)可以减少这种情况下的编译时间(以及 link 时间和可执行文件大小)。您需要阅读编译器文档以了解如何执行此操作 - 它不一定会默认发生,并且可能意味着重构您的代码以支持它。
- Header 文件在许多源文件中
#include
d,无论是否需要,往往会增加编译时间。我看到一个案例,其中一个团队成员写了一个"globals.h"
,#include
d 一切,#include
d 无处不在 - 构建时间(在一个大项目中)增加了一个数量级震级。这是一个双重打击 - 每个源文件的编译时间都增加了,并且乘以直接或间接#include
即 header 的源文件的数量。如果打开 "precompiled headers" 之类的功能导致第二次和后续构建的构建时间 speed-up,这可能是一个贡献者。 (您可能会将预编译的 header 视为解决此问题的方法,但请记住还有其他 trade-offs 可以使用它们)。 - 如果您使用的是外部库,请检查以确保它们是 在本地安装和配置。一个编译过程 默默地在互联网上寻找一些组件(例如 hard-coded header 某些远程服务器上的文件名)会变慢 事情相当大。您会惊讶于 third-party 图书馆发生这种情况的频率。
除此之外,查找问题的技术取决于构建过程的结构。
如果您使用的是单独编译源文件的 makefile(或其他方式),则使用某种方式来为各个编译和 linking 命令计时。请记住,可能是 link 时间占主导地位。
如果您使用单个编译命令(例如,在一个命令中对多个文件调用 gcc),则将其分解为每个源文件的单独命令。
一旦您确定了哪个源文件(如果有)是违规者,然后有选择地从中删除一些部分以找出其中的哪些代码有问题。正如 Yakk 在评论中所说,为此使用 "binary search" 来消除文件中的函数。我建议先删除整个函数(以缩小到有问题的函数),然后在有问题的函数中使用相同的技术。
它确实有助于构建您的代码,因此每个文件的函数数量相当少。这减少了为一个功能的微小变化重建大文件的需要,并有助于将来更容易地隔离此类问题。