可以使用哪些技术来加速C++编译时间?
这个问题出现在Stack Overflow问题C++编程风格的一些评论中,我很想知道它有什么想法.
我见过一个相关的问题,为什么C++编译需要这么长时间?,但这并没有提供很多解决方案.
Ecl*_*pse 250
看看这里的Pimpl习语 ,这里也称为不透明指针或句柄类.它不仅加快了编译速度,还与非投掷交换功能相结合,增加了异常安全性.Pimpl习惯用法可以减少标头之间的依赖关系,并减少需要完成的重新编译量.
尽可能使用前向声明.如果编译器只需要知道它SomeIdentifier是结构或指针或其他什么,就不要包含整个定义,从而迫使编译器完成比它需要的更多的工作.这可能会产生级联效应,使这种方式比它们需要的慢.
在I/O流,特别是著名的减缓构建.如果在头文件中需要它们,请尝试#including <iosfwd>而不是<iostream>#include <iostream>实现文件中的头.该<iosfwd>头仅持有向前声明.不幸的是,其他标准头文件没有相应的声明头.
首选通过引用传递函数签名中的pass-by-value.这将消除#include头文件中相应类型定义的需要,您只需要转发声明类型.当然,更喜欢const引用非const引用以避免模糊的错误,但这是另一个问题的问题.
使用保护条件可以防止头文件在单个翻译单元中被多次包含.
#pragma once
#ifndef filename_h
#define filename_h
// Header declarations / definitions
#endif
Run Code Online (Sandbox Code Playgroud)
通过使用pragma和ifndef,您可以获得普通宏解决方案的可移植性,以及某些编译器在pragma once指令存在时可以执行的编译速度优化.
一般来说,代码设计越模块化,越不相互依赖,您重新编译所有内容的频率就越低.您还可以最终减少编译器同时对任何单个块执行的工作量,因为它具有较少的跟踪记录.
这些用于为许多翻译单元编译一次包含的标题的公共部分.编译器将其编译一次,并保存其内部状态.然后可以快速加载该状态,以便在使用同一组头文件编译另一个文件时获得先机.
请注意,您只在预编译的头文件中包含很少更改的内容,否则您最终可能会比必要时更频繁地进行完全重建.这是STL标头和其他库包含文件的好地方.
ccache是另一个利用缓存技术来加快速度的实用程序.
许多编译器/ IDE支持使用多个内核/ CPU同时进行编译.在GNU Make(通常与GCC一起使用)中,使用该-j [N]选项.在Visual Studio中,首选项下有一个选项,允许它并行构建多个项目.您还可以使用文件级并行性/MP选项的选项,而不仅仅是项目级别的并列式.
其他并行工具:
编译器尝试优化的越多,它就越难以工作.
将不常修改的代码移动到库中可以减少编译时间.通过使用共享库(.so或.dll),您也可以减少链接时间.
更多的RAM,更快的硬盘驱动器(包括SSD)以及更多的CPU /核心都会对编译速度产生影响.
Man*_*far 37
我在STAPL项目上工作,这是一个经过严格模板化的C++库.偶尔,我们必须重新审视所有技术以减少编译时间.在这里,我总结了我们使用的技术.其中一些技术已在上面列出:
虽然在符号长度和编译时间之间没有经证实的相关性,但我们观察到较小的平均符号大小可以改善所有编译器的编译时间.所以你的第一个目标是找到代码中最大的符号.
您可以使用该nm命令根据大小列出符号:
nm --print-size --size-sort --radix=d YOUR_BINARY
Run Code Online (Sandbox Code Playgroud)
在此命令中--radix=d,您可以看到十进制数字的大小(默认为十六进制).现在,通过查看最大的符号,确定是否可以打破相应的类并尝试通过将非模板化部分分解为基类来重新设计它,或者将类拆分为多个类.
您可以运行常规nm命令并将其传送到您喜欢的脚本(AWK,Python等),以根据符号的长度对符号进行排序.根据我们的经验,这种方法确定了比方法1更好的候选人.
" Templight是一个基于Clang的工具,用于分析模板实例化的时间和内存消耗,并执行交互式调试会话以获得对模板实例化过程的反省".
您可以通过签出LLVM和Clang(说明)并在其上应用Templight补丁来安装Templight .LLVM和Clang的默认设置是调试和断言,这些可能会显着影响编译时间.看起来Templight需要两者,所以你必须使用默认设置.安装LLVM和Clang的过程大约需要一个小时左右.
应用补丁后,您可以使用templight++位于安装时指定的构建文件夹中来编译代码.
确保它templight++在你的PATH中.现在编译将以下开关添加到CXXFLAGSMakefile或命令行选项中:
CXXFLAGS+=-Xtemplight -profiler -Xtemplight -memory -Xtemplight -ignore-system
Run Code Online (Sandbox Code Playgroud)
要么
templight++ -Xtemplight -profiler -Xtemplight -memory -Xtemplight -ignore-system
Run Code Online (Sandbox Code Playgroud)
编译完成后,您将在同一文件夹中生成.trace.memory.pbf和.trace.pbf.要可视化这些跟踪,您可以使用可以将这些跟踪转换为其他格式的Templight工具.请按照以下说明安装templight-convert.我们通常使用callgrind输出.如果项目很小,您还可以使用GraphViz输出:
$ templight-convert --format callgrind YOUR_BINARY --output YOUR_BINARY.trace
$ templight-convert --format graphviz YOUR_BINARY --output YOUR_BINARY.dot
Run Code Online (Sandbox Code Playgroud)
生成的callgrind文件可以使用kcachegrind打开,您可以在其中跟踪最耗时的内存实例.
虽然没有确切的解决方案来减少模板实例化的数量,但有一些指导原则可以帮助:
例如,如果你有一个班级,
template <typename T, typename U>
struct foo { };
Run Code Online (Sandbox Code Playgroud)
两者的T和U可以有10个不同的选项,你已经增加了这类可能的模板实例,以100的一种方式来解决,这是抽象的代码的公共部分,以不同的类.另一种方法是使用继承反转(反转类层次结构),但在使用此技术之前,请确保您的设计目标不会受到影响.
使用此技术,您可以编译公共部分一次,然后将其与其他TU(翻译单元)链接.
如果您知道类的所有可能的实例化,则可以使用此技术在不同的转换单元中编译所有情况.
例如,在:
enum class PossibleChoices = {Option1, Option2, Option3}
template <PossibleChoices pc>
struct foo { };
Run Code Online (Sandbox Code Playgroud)
我们知道这个类可以有三个可能的实例化:
template class foo<PossibleChoices::Option1>;
template class foo<PossibleChoices::Option2>;
template class foo<PossibleChoices::Option3>;
Run Code Online (Sandbox Code Playgroud)
将上述内容放在翻译单元中,并在头文件中使用extern关键字,在类定义下面:
extern template class foo<PossibleChoices::Option1>;
extern template class foo<PossibleChoices::Option2>;
extern template class foo<PossibleChoices::Option3>;
Run Code Online (Sandbox Code Playgroud)
如果使用一组通用实例编译不同的测试,这种技术可以节省您的时间.
注意:此时MPICH2忽略显式实例化,并始终在所有编译单元中编译实例化的类.
统一构建背后的整个思想是包含您在一个文件中使用的所有.cc文件,并仅编译该文件一次.使用此方法,您可以避免重新实现不同文件的公共部分,如果您的项目包含许多常见文件,您可能也会节省磁盘访问.
举个例子,假设你有三个文件foo1.cc,foo2.cc,foo3.cc和它们都包括tuple从STL.您可以创建一个foo-all.cc如下所示的内容:
#include "foo1.cc"
#include "foo2.cc"
#include "foo3.cc"
Run Code Online (Sandbox Code Playgroud)
您只编译此文件一次,并可能减少三个文件中的常见实例.很难一般地预测改善是否显着.但是一个明显的事实是你会在你的构建中失去并行性(你不能再同时编译这三个文件).
此外,如果这些文件中的任何一个碰巧占用了大量内存,那么在编译结束之前,实际上可能会耗尽内存.在一些编译器上,例如GCC,这可能是ICE(内部编译器错误)你的编译器缺乏内存.所以除非你知道所有的利弊,否则不要使用这种技术.
通过将头文件编译为编译器可识别的中间表示,预编译头(PCH)可以节省大量编译时间.要生成预编译的头文件,只需使用常规编译命令编译头文件.例如,在GCC上:
$ g++ YOUR_HEADER.hpp
Run Code Online (Sandbox Code Playgroud)
这将在同一文件夹中生成YOUR_HEADER.hpp.gch file(.gch是GCC中PCH文件的扩展名).这意味着如果您包含YOUR_HEADER.hpp在其他一些文件中,编译器将使用您的YOUR_HEADER.hpp.gch而不是YOUR_HEADER.hpp之前的同一文件夹.
这种技术有两个问题:
all-my-headers.hpp).但这意味着您必须在所有位置包含新文件.幸运的是,GCC有一个解决这个问题的方法.使用-include并为其提供新的头文件.您可以使用此技术以逗号分隔不同的文件.例如:
g++ foo.cc -include all-my-headers.hpp
Run Code Online (Sandbox Code Playgroud)
未命名的命名空间(也称为匿名命名空间)可以显着减少生成的二进制文件大小.未命名的命名空间使用内部链接,这意味着在这些命名空间中生成的符号对其他TU(转换或编译单元)不可见.编译器通常为未命名的命名空间生成唯一的名称.这意味着如果你有一个文件foo.hpp:
namespace {
template <typename T>
struct foo { };
} // Anonymous namespace
using A = foo<int>;
Run Code Online (Sandbox Code Playgroud)
并且您碰巧将此文件包含在两个TU中(两个.cc文件并单独编译).两个foo模板实例不一样.这违反了单一定义规则(ODR).出于同样的原因,在头文件中不鼓励使用未命名的命名空间.您可以随意在.cc文件中使用它们,以避免在二进制文件中显示符号.在某些情况下,更改文件的所有内部详细信息会.cc显示生成的二进制文件大小减少10%.
在较新的编译器中,您可以选择在动态共享对象(DSO)中可见或不可见的符号.理想情况下,更改可见性可以提高编译器性能,链接时间优化(LTO)和生成的二进制大小.如果你看一下GCC中的STL头文件,你会发现它被广泛使用.要启用可见性选择,您需要更改每个函数,每个类,每个变量的代码,更重要的是每个编译器.
借助于可见性,您可以从生成的共享对象中隐藏您认为是私有的符号.在GCC上,您可以通过将default或hidden传递给-visibility编译器选项来控制符号的可见性.这在某种意义上类似于未命名的命名空间,但是以更精细和侵入性的方式.
如果要指定每个案例的可见性,则必须将以下属性添加到函数,变量和类中:
__attribute__((visibility("default"))) void foo1() { }
__attribute__((visibility("hidden"))) void foo2() { }
__attribute__((visibility("hidden"))) class foo3 { };
void foo4() { }
Run Code Online (Sandbox Code Playgroud)
GCC中的默认可见性是默认的(公共),这意味着如果您将上面的内容编译为共享库(-shared)方法,foo2并且类foo3将不会在其他TU 中可见(foo1并且foo4将是可见的).如果你使用编译,-visibility=hidden那么只有foo1可见.甚至foo4会被隐藏起来.
您可以在GCC维基上阅读有关可见性的更多信息.
Pau*_*ius 32
我推荐这些来自"内部游戏,独立游戏设计和编程"的文章:
当然,它们已经很老了 - 您必须使用最新版本(或可用的版本)重新测试所有内容,以获得真实的结果.无论哪种方式,它都是思想的良好来源.
Fre*_*abe 16
过去对我来说效果很好的一种技术:不要独立编译多个C++源文件,而是生成一个包含所有其他文件的C++文件,如下所示:
// myproject_all.cpp
// Automatically generated file - don't edit this by hand!
#include "main.cpp"
#include "mainwindow.cpp"
#include "filterdialog.cpp"
#include "database.cpp"
Run Code Online (Sandbox Code Playgroud)
当然,这意味着您必须重新编译所有包含的源代码,以防任何源更改,因此依赖关系树会变得更糟.但是,将多个源文件编译为一个转换单元的速度更快(至少在我使用MSVC和GCC的实验中)并生成较小的二进制文件.我还怀疑编译器有更多的优化潜力(因为它可以同时看到更多的代码).
这种技术在各种情况下都会中断 例如,如果两个或多个源文件声明具有相同名称的全局函数,编译器将会挽救.我找不到任何其他答案中描述的这种技术,这就是为什么我在这里提到它.
值得一提的是,自1999年以来,KDE项目使用了这种完全相同的技术来构建优化的二进制文件(可能用于发布).调用了构建配置脚本的切换--enable-final.出于考古学的兴趣,我挖出了宣布这个功能的帖子:http://lists.kde.org/? l = kde-devel&m = 92722836009368&w = 2
Joh*_*itb 15
我将链接到我的另一个答案:你如何减少编译时间,并链接Visual C++项目的时间(本机C++)?.我想添加的另一点,但经常出现问题的是使用预编译的头文件.但请注意,只能将它们用于几乎不会改变的部分(如GUI工具包标题).否则,他们将花费你比他们最后拯救你更多的时间.
另一个选择是,当你使用GNU make时,打开-j<N>选项:
-j [N], --jobs[=N] Allow N jobs at once; infinite jobs with no arg.
Run Code Online (Sandbox Code Playgroud)
我通常拥有它,3因为我在这里有双核心.然后,它将为不同的翻译单元并行运行编译器,前提是它们之间没有依赖关系.链接不能并行完成,因为只有一个链接器进程将所有目标文件链接在一起.
但是链接器本身可以是线程化的,这就是ELF链接器所做的事情.它是优化的线程C++代码,据说可以比旧版本更快地链接ELF对象文件(实际上包含在binutils中).GNU gold ld
Dav*_*eas 11
一旦你应用了上面的所有代码技巧(前向声明,在公共头文件中将头部包含减少到最小,用Pimpl推送实现文件中的大部分细节......)并且没有其他任何东西可以通过语言获得,请考虑你的构建系统.如果您使用Linux,请考虑使用distcc(分布式编译器)和ccache(缓存编译器).
第一个是distcc,它在本地执行预处理器步骤,然后将输出发送到网络中的第一个可用编译器.它需要网络中所有已配置节点中的相同编译器和库版本.
后者ccache是一个编译器缓存.它再次执行预处理器,然后检查内部数据库(保存在本地目录中)是否已使用相同的编译器参数编译该预处理器文件.如果是这样,它只会弹出二进制文件并从第一次运行编译器输出.
两者都可以同时使用,因此如果ccache没有本地副本,它可以通过网络将其发送到另一个带有distcc的节点,否则它只需注入解决方案而无需进一步处理.
当我从大学毕业时,我看到的第一个真正具有生产价值的C++代码在它们之间有了这些神秘的#ifndef ... #endif指令,其中定义了标题.我问那个以非常幼稚的方式编写关于这些总体事情的代码的人,并介绍了大规模编程的世界.
回到这一点,使用指令来防止重复的头定义是我在减少编译时间时学到的第一件事.
尽可能使用前向声明.如果类声明仅使用指针或类型的引用,则只需转发声明它并在实现文件中包含该类型的标头.
例如:
// T.h
class Class2; // Forward declaration
class T {
public:
void doSomething(Class2 &c2);
private:
Class2 *m_Class2Ptr;
};
// T.cpp
#include "Class2.h"
void Class2::doSomething(Class2 &c2) {
// Whatever you want here
}
Run Code Online (Sandbox Code Playgroud)
如果你做得足够多,那么对于预处理器来说,更少的工作意味着更少的工作.
您可以使用Unity Build.
我有一个关于使用 RAM 驱动器的想法。事实证明,对于我的项目来说,这根本没有多大区别。但它们仍然很小。尝试一下!我很想听听它有多大帮助。
升级你的电脑
然后你就有了所有其他典型的建议
使用
#pragma once
Run Code Online (Sandbox Code Playgroud)
在头文件的顶部,因此如果它们在翻译单元中被包含多次,则标题的文本将仅被包含并解析一次.
不是关于编译时间,而是关于构建时间:
如果在处理构建文件时必须重建相同的文件,请使用ccache
使用ninja-build而不是 make。我目前正在编译一个包含约 100 个源文件的项目,所有内容都由 ccache 缓存。制作需要5分钟,忍者不到1分钟。
你可以从 cmake 生成你的忍者文件-GNinja。
| 归档时间: |
|
| 查看次数: |
101091 次 |
| 最近记录: |