如何减少单个 .cpp 文件的大型 C++ 库的编译时间?

Posted

技术标签:

【中文标题】如何减少单个 .cpp 文件的大型 C++ 库的编译时间?【英文标题】:How to reduce compile time for large C++ library of individual .cpp files? 【发布时间】:2015-10-22 09:18:58 【问题描述】:

我们正在开发一个C++ library,目前包含超过 50000 个单独的 .cpp 文件。这些都被编译并归档到一个静态库中。即使使用并行构建,这也需要几分钟。我想减少这个编译时间。

每个文件平均有 110 行,其中包含一个或两个函数。但是,对于每个 .cpp 文件,都有一个对应的 .h 标头,并且这些标头通常包含在许多 .cpp 文件中。例如,A.h 可能包含在 A.cppB.cppC.cpp 等中。

我想先介绍编译过程。有没有办法找出在做什么上花费了多少时间?我担心打开头文件只是为了检查包含保护并忽略文件而浪费了很多时间。

如果这类事情是罪魁祸首,那么减少编译时间的最佳做法是什么?

我愿意添加新的分组标题,但可能不愿意更改这种多文件布局,因为这使我们的库也可以作为按需仅标题库。

【问题讨论】:

看到这个帖子***.com/questions/13559818/… 我不确定打开文件是否真的需要时间。通常,可以通过包含较少无用的重头文件(包含许多内联函数/模板元编程结构)和减少头文件之间的耦合来减少编译时间。但是,如果您的标头仅包含前向声明,那么可能只是 500x100x110 行代码(根据您的数字)。毕竟 C++ 编译速度很慢,只要确保每次重新编译都不会重新编译不依赖于更新文件的东西。好吧,我猜你什么时候会看到它。 您可能会发现tup 构建系统很有趣,它非常快并且避免了冗余。一些测试:gittup.org/tup/make_vs_tup.html 您是每次都进行完全重建,还是只重建需要重建的文件?您能否以这样一种方式重新组织代码,以减少为响应大多数代码更改而重新构建的文件? @JeremyFriesner,在开发时使用 cmake,所以我只重建必须的。在进行全新构建时,例如在进行夜间编译检查时,我会更加恼火。 【参考方案1】:

真的很难说。

我在工作中致力于改善我们项目的编译时间,发现一个文件花了 15 分钟(在 -O2 中编译时,但在 -O0 中大约 15 秒)并被编译了两次,所以总共编译时间大约 60-70 分钟,这大约是一半时间。关闭一个优化功能使一个文件缩短到大约 20 秒而不是 15 分钟......这个文件正在生成一个由机器生成的函数,并且有几万行长,这导致编译器做一些魔法长的东西(大概是一些 O(N^2) 算法)。

如果您有一个小函数然后依次调用许多小函数,最终通过内联层变成一个大文件,也会发生这种情况。

在其他时候,我发现减少文件数量并将更多代码放在一个文件中效果更好。

总的来说,我的经验(包括我自己的编译器项目,以及其他人/公司的编译器)是,花时间的不是文件的解析和读取,而是各种优化和代码生成过程。您可以通过使用-fsyntax-only 或编译器调用的任何文件来编译所有文件来尝试一下。那将只是阅读源代码并检查它在语法上是否正确。如果您还没有,请尝试使用-O0 进行编译。通常一个特定的优化通道是问题,有些通道比其他通道更差,因此检查特定的-O 选项中有哪些单独的优化通道很有用 - 在 gcc 中可以用-Q -O2 --help=optimizers 列出[在这种情况下对于-O2]。

您确实需要弄清楚编译器花时间在什么上面。如果问题在于您花费大部分时间优化代码,那么更改代码是没有意义的。如果时间花在解析上,那么减少优化器是没有意义的,而优化不会增加额外的时间。如果没有实际构建您的项目,很难确定。

另一个提示是检查top 以查看您的编译进程是否每个都使用 100% cpu - 如果不是,您的编译机中可能没有足够的内存。我的工作项目有一个构建选项,它通过运行如此多的内存“杀死”我的台式机,整个系统只是停止运行——即使在网络浏览器中从一个选项卡切换到另一个选项卡也需要 15-30 秒。唯一的解决方案是少跑-j [当然,我通常会忘记,在那个时候-所以如果我不想打断它,我会去吃午饭,喝咖啡休息一下,直到它结束,因为机器无法使用]。这仅适用于调试版本,因为将大型代码库的调试信息放在一起会占用大量内存 [显然!]

【讨论】:

【参考方案2】:

如果这种事情是罪魁祸首,那么减少编译时间的最佳做法是什么?

如果您的预处理器支持#pragma once 指令,请使用它。这将确保 .h 文件不会被多次读取。

如果没有,请在 .cpp 文件中使用 #include 保护。

说你有

啊哈:

#ifndef A_H
#define A_H

...

#endif

可以在A.cpp中使用如下方法:

#ifndef A_H
#include "A.h"
#endif

您需要为每个 .h 文件重复该模式。例如

#ifndef B_H
#include "B.h"
#endif

#ifndef C_H
#include "C.h"
#endif

您可以在 What is the function of include guard in .cpp (not in .h)? 的 .cpp 文件中阅读有关使用 #include 守卫的更多信息。

【讨论】:

感谢 .cpp 文件中的守卫背后的推理链接。乍一看确实是多余的。 @Alain,这是非常多余的。只有当你确信它们物有所值时,你才会经历这些痛苦。 现代编译器已经优化了#include,因此它们甚至不会多次打开同一个标头:gcc.gnu.org/onlinedocs/cppinternals/Guard-Macros.html 这些技巧只会使代码库更难维护。【参考方案3】:

我不知道你是否已经这样做了,但是使用前向声明而不是头文件中的包含应该会提高编译速度。有关更多信息,请参阅此问题:

Should one use forward declarations instead of includes wherever possible?

另一种减少编译时间的方法是使用ccache。它缓存以前编译的结果。

https://ccache.samba.org

【讨论】:

【参考方案4】:

将您的代码结构化为使用 PIMPL 范例。两个主要好处是:

您可以对用户隐藏所有实现(成员变量等) 如果您更改了实现文件,那么“通常”只有这个区域需要重新编译而不是完全重建。

如需全面了解,请参阅here

【讨论】:

以上是关于如何减少单个 .cpp 文件的大型 C++ 库的编译时间?的主要内容,如果未能解决你的问题,请参考以下文章

在 2 个 cpp 文件中定义一个 C++ 类?

如何减少用本机 Visual C++ 编写的大型项目的链接时间?

通过 <...> 访问大型 C++ 项目中的头文件

如何在 C++ 项目中运行多 cpp?

用于具有单个 .h 文件的库的 Cmake

Linux下C++静态库动态库的制作与使用