可以使用哪些技术来加速C ++编译时间?
Stack Overflow问题 C++ programming style 的一些评论中提出了这个问题,我很想知道它有什么想法。
我见过一个相关的问题, Why does C++ compilation take so long? ,但这并没有提供很多解决方案。
Vote here have Visual Studio support sharing precompiled headers between projects
答案 0 :(得分:237)
查看 Pimpl idiom here和here,也称为opaque pointer或句柄类。它不仅可以加速编译,还可以在与non-throwing swap功能结合使用时提高异常安全性。 Pimpl习惯用法可以减少标题之间的依赖关系,减少需要重新编译的数量。
尽可能使用forward declarations。如果编译器只需要知道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
通过同时使用pragma和ifndef,您可以获得普通宏解决方案的可移植性,以及某些编译器在pragma once
指令存在时可以执行的编译速度优化。
一般来说,代码设计越模块化,越不相互依赖,您重新编译所有内容的频率就越低。您还可以最终减少编译器同时对任何单个块执行的工作量,因为它可以更少地跟踪。
这些用于为许多翻译单元编译一次包含标题的公共部分。编译器将其编译一次,并保存其内部状态。然后可以快速加载该状态,以便在使用同一组头文件编译另一个文件时获得先机。
请注意,您只在预编译的头文件中包含很少更改的内容,否则您最终可能会比必要时更频繁地进行完全重建。这是STL标题和其他库包含文件的好地方。
ccache是另一个利用缓存技术加速的实用程序。
许多编译器/ IDE支持使用多个内核/ CPU同时进行编译。在GNU Make(通常与GCC一起使用)中,使用-j [N]
选项。在Visual Studio中,首选项下有一个选项,允许它并行构建多个项目。您还可以使用/MP
option进行文件级并行游戏,而不仅仅是项目级别的并列游戏。
其他并行工具:
编译器尝试优化越多,就越难以工作。
将不常修改的代码移动到库中可以减少编译时间。通过使用共享库(.so
或.dll
),您也可以减少链接时间。
更多内存,更快的硬盘(包括SSD)以及更多的CPU /内核都会对编译速度产生影响。
答案 1 :(得分:32)
我在STAPL项目上工作,这是一个经过严格模板化的C ++库。偶尔,我们必须重新审视所有技术以减少编译时间。在这里,我总结了我们使用的技术。其中一些技术已在上面列出:
虽然在符号长度和编译时间之间没有经证实的相关性,但我们观察到较小的平均符号大小可以改善所有编译器的编译时间。所以你的第一个目标就是找到你代码中最大的符号。
您可以使用nm
命令根据符号大小列出符号:
nm --print-size --size-sort --radix=d YOUR_BINARY
在此命令中,--radix=d
可让您查看十进制数字的大小(默认为十六进制)。现在通过查看最大的符号,确定是否可以打破相应的类并尝试通过将非模板化部分分解为基类来重新设计它,或者将类拆分为多个类。
您可以运行常规nm
命令并将其发送到您喜欢的脚本(AWK,Python等),以根据长度>对符号进行排序STRONG>。根据我们的经验,这种方法可以确定候选人最大的麻烦,而不是方法1。
&#34; Templight是一个基于Clang的工具,用于分析模板实例化的时间和内存消耗,并执行交互式调试会话以获得对模板实例化过程的反省&#34;。
您可以通过查看LLVM和Clang(instructions)并在其上应用Templight补丁来安装Templight。 LLVM和Clang的默认设置是调试和断言,这些可能会显着影响编译时间。看起来Templight需要两者,所以你必须使用默认设置。安装LLVM和Clang的过程大约需要一个小时左右。
应用补丁后,您可以使用安装时指定的构建文件夹中的templight++
来编译代码。
确保templight++
在您的路径中。现在编译将以下开关添加到Makefile中的CXXFLAGS
或命令行选项:
CXXFLAGS+=-Xtemplight -profiler -Xtemplight -memory -Xtemplight -ignore-system
或者
templight++ -Xtemplight -profiler -Xtemplight -memory -Xtemplight -ignore-system
编译完成后,您将在同一文件夹中生成.trace.memory.pbf和.trace.pbf。要可视化这些跟踪,您可以使用可以将这些跟踪转换为其他格式的Templight Tools。按照这些instructions安装templight-convert。我们通常使用callgrind输出。如果项目很小,您也可以使用GraphViz输出:
$ templight-convert --format callgrind YOUR_BINARY --output YOUR_BINARY.trace
$ templight-convert --format graphviz YOUR_BINARY --output YOUR_BINARY.dot
生成的callgrind文件可以使用kcachegrind打开,您可以在其中跟踪最耗时的内存消耗实例。
虽然没有减少模板实例化数量的确切解决方案,但有一些指导原则可以提供帮助:
例如,如果你有一个班级,
template <typename T, typename U>
struct foo { };
并且T
和U
都可以有10个不同的选项,您已将此类的可能模板实例化增加到100.解决此问题的一种方法是抽象代码的公共部分到另一个班级。另一种方法是使用继承反转(反转类层次结构),但在使用此技术之前,请确保您的设计目标不会受到影响。
使用此技术,您可以编辑公共部分一次,然后将其与其他TU(翻译单元)链接。
如果您知道某个类的所有可能的实例化,则可以使用此技术在不同的翻译单元中编译所有案例。
例如,在:
enum class PossibleChoices = {Option1, Option2, Option3}
template <PossibleChoices pc>
struct foo { };
我们知道这个类可以有三种可能的实例化:
template class foo<PossibleChoices::Option1>;
template class foo<PossibleChoices::Option2>;
template class foo<PossibleChoices::Option3>;
将上述内容放在翻译单元中,并在头文件中使用extern关键字,在类定义下方:
extern template class foo<PossibleChoices::Option1>;
extern template class foo<PossibleChoices::Option2>;
extern template class foo<PossibleChoices::Option3>;
如果您使用一组常见的实例化编译不同的测试,这种技术可以节省您的时间。
注意:此时MPICH2忽略显式实例化,并始终在所有编译单元中编译实例化的类。
统一构建背后的整个想法是包含您在一个文件中使用的所有.cc文件,并仅编译该文件一次。使用此方法,您可以避免重新实例化不同文件的公共部分,如果您的项目包含许多常见文件,您可能也会节省磁盘访问。
例如,假设您有三个文件foo1.cc
,foo2.cc
,foo3.cc
,并且它们都包含来自STL的tuple
。您可以创建一个如下所示的foo-all.cc
:
#include "foo1.cc"
#include "foo2.cc"
#include "foo3.cc"
您只编译此文件一次,并可能减少三个文件之间的常见实例化。很难一般地预测改善是否显着。但有一个明显的事实是你会在你的构建中失去并行性(你不能再同时编译这三个文件)。
此外,如果这些文件中的任何一个碰巧占用了大量内存,那么在编译结束之前,实际上可能会耗尽内存。在某些编译器上,例如GCC,这可能是ICE(内部编译器错误)编译器缺少内存。除非你了解所有的利弊,否则不要使用这种技术。
通过将头文件编译为编译器可识别的中间表示,预编译头(PCH)可以节省大量编译时间。要生成预编译的头文件,只需使用常规编译命令编译头文件。例如,在GCC上:
$ g++ YOUR_HEADER.hpp
这将在同一文件夹中生成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
Unnamed namespaces(a.k.a。匿名命名空间)可以显着减少生成的二进制文件大小。未命名的命名空间使用内部链接,这意味着在这些命名空间中生成的符号对其他TU(转换或编译单元)不可见。编译器通常为未命名的命名空间生成唯一的名称。这意味着如果你有一个文件foo.hpp:
namespace {
template <typename T>
struct foo { };
} // Anonymous namespace
using A = foo<int>;
您碰巧将此文件包含在两个TU中(两个.cc文件并单独编译)。两个foo模板实例不一样。这违反了One Definition Rule(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() { }
GCC中的默认可见性是默认(公共),这意味着如果将上述内容编译为共享库(-shared
)方法,则foo2
和类foo3
将不可见在其他TU中(foo1
和foo4
将可见)。如果使用-visibility=hidden
进行编译,则只会显示foo1
。即使foo4
也会被隐藏。
您可以在GCC wiki上了解有关可见性的更多信息。
答案 2 :(得分:31)
我推荐这些来自“内部游戏,独立游戏设计和编程”的文章:
当然,它们已经很老了 - 您必须使用最新版本(或可用的版本)重新测试所有内容,以获得真实的结果。无论哪种方式,它都是思想的良好来源。
答案 3 :(得分: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"
当然,这意味着您必须重新编译所有包含的源代码,以防任何源更改,因此依赖关系树会变得更糟。但是,将多个源文件编译为一个转换单元的速度更快(至少在我使用MSVC和GCC的实验中)并生成较小的二进制文件。我还怀疑编译器有更多的优化潜力(因为它可以同时看到更多的代码)。
这种技术在各种情况下都会中断;例如,如果两个或多个源文件声明具有相同名称的全局函数,编译器将会挽救。我找不到任何其他答案中描述的这种技术,这就是为什么我在这里提到它。
对于它的价值,KDE Project使用了自1999年以来完全相同的技术来构建优化的二进制文件(可能用于发布)。切换到构建配置脚本称为--enable-final
。出于考古学的兴趣,我挖出了宣布这一特征的帖子:http://lists.kde.org/?l=kde-devel&m=92722836009368&w=2
答案 4 :(得分:15)
我只会链接到我的其他答案: How do YOU reduce compile time, and linking time for Visual C++ projects (native C++)? 。我想添加的另一点,但经常出现问题的是使用预编译的头文件。但请注意,只能将它们用于几乎不会改变的部分(如GUI工具包标题)。否则,他们将花费你比他们最后拯救你更多的时间。
另一个选择是,当你使用GNU make时,打开-j<N>
选项:
-j [N], --jobs[=N] Allow N jobs at once; infinite jobs with no arg.
我通常在3
拥有它,因为我在这里有双核心。然后,它将为不同的翻译单元并行运行编译器,前提是它们之间没有依赖关系。链接不能并行完成,因为只有一个链接器进程将所有目标文件链接在一起。
但链接器本身可以是线程化的,这就是GNU gold
ELF链接器所做的事情。它是优化的线程C ++代码,据说可以比旧的ld
更快地链接ELF目标文件(并且实际上包含在binutils中)。
答案 5 :(得分:15)
有关于这个主题的整本书,标题为 Large-Scale C++ Software Design (由John Lakos编写)。
本书预先设定了模板,因此对于该书的内容添加“使用模板,也可以使编译器变慢”。
答案 6 :(得分:11)
以下是一些:
make -j2
就是一个很好的例子)。-O1
而不是-O2
或-O3
,GCC要快得多。答案 7 :(得分:10)
一旦你应用了上面的所有代码技巧(前向声明,在公共头文件中将头部包含减少到最小,用Pimpl推送实现文件中的大部分细节......)并且没有别的东西可以获得语言 - 请考虑您的构建系统。如果您使用Linux,请考虑使用distcc(分布式编译器)和ccache(缓存编译器)。
第一个,distcc,在本地执行预处理器步骤,然后将输出发送到网络中的第一个可用编译器。它需要在网络中的所有已配置节点中使用相同的编译器和库版本。
后者ccache是一个编译器缓存。它再次执行预处理器,然后检查内部数据库(保存在本地目录中)是否已使用相同的编译器参数编译该预处理器文件。如果是这样,它只会弹出二进制文件并从第一次运行编译器输出。
两者都可以同时使用,因此如果ccache没有本地副本,它可以通过网络将其发送到另一个带有distcc的节点,否则它只需注入解决方案而无需进一步处理。
答案 8 :(得分:9)
当我从大学毕业时,我看到的第一个真正具有生产价值的C ++代码在它们之间具有这些神秘的#ifndef ... #endif指令,其中定义了标题。我问那个以非常幼稚的方式编写关于这些总体事情的代码的人,并介绍了大规模编程的世界。
回到这一点,使用指令来防止重复的头定义是我在减少编译时间时学到的第一件事。
答案 9 :(得分:8)
答案 10 :(得分:6)
尽可能使用前向声明。如果类声明仅使用指针或对类型的引用,则只需转发声明它并在实现文件中包含该类型的标题。
例如:
// 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
}
如果你做得足够多,那么对预处理器的工作就会少得多。
答案 11 :(得分:6)
您可以使用Unity Builds。
答案 12 :(得分:5)
使用
#pragma once
位于头文件的顶部,因此如果它们在翻译单元中被包含多次,则标题的文本将仅包含并解析一次。
答案 13 :(得分:5)
只是为了完整性:构建可能会很慢,因为构建系统是愚蠢的,因为编译器需要很长时间才能完成它的工作。
阅读 Recursive Make Considered Harmful (PDF),以便在Unix环境中讨论此主题。
答案 14 :(得分:4)
我对using a RAM drive有所了解。事实证明,对于我的项目来说,它毕竟没有那么大的差别。但是他们还很小。试试吧!我很想知道它有多大帮助。
答案 15 :(得分:4)
升级您的计算机
然后你有其他所有典型的建议
答案 16 :(得分:3)
动态链接(.so)可以比静态链接(.a)快得多。特别是当你的网络驱动器速度很慢时。这是因为您拥有.a文件中需要处理和写出的所有代码。此外,需要将更大的可执行文件写入磁盘。
答案 17 :(得分:3)
你在哪里度过你的时间?你受CPU限制吗?记忆力?磁盘绑定?你可以使用更多核心吗?更多内存?你需要RAID吗?您只是想提高当前系统的效率吗?
在gcc / g ++下,您看过ccache了吗?如果你正在做make_clean _; _做了很多。
答案 18 :(得分:3)
不是关于编译时间,而是关于构建时间:
如果您在工作时必须重建相同的文件,请使用 ccache 在您的构建文件
使用 ninja-build 代替make。我目前正在编制一个项目 有~100个源文件,所有内容都由ccache缓存。提出需求 5分钟,忍者少于1。
您可以使用-GNinja
从cmake生成忍者文件。
答案 19 :(得分:2)
如果您有多核处理器,Visual Studio(2005及更高版本)以及GCC都支持多处理器编译。如果您有硬件,那么可以启用它。
答案 20 :(得分:2)
随着寻道延迟很高,网络共享将大大减慢您的构建速度。对于像Boost这样的东西,它对我来说有很大的不同,即使我们的网络共享驱动器非常快。当我从网络共享切换到本地SSD时,编译玩具Boost程序的时间从大约1分钟到1秒。
答案 21 :(得分:2)
更快的硬盘。
编译器将许多(可能是巨大的)文件写入磁盘。使用SSD而不是典型的硬盘和编译时间要低得多。
答案 22 :(得分:2)
在Linux(可能还有其他一些* NIXes)上,您可以通过输出中的NOT STARING加速编译,并更改为 另一个 TTY。
以下是实验: printf slows down my program
答案 23 :(得分:1)
虽然不是“技术”,但我无法弄清楚Win32项目如何编译许多源文件比我的“Hello World”空项目更快。因此,我希望这可以帮助像我这样的人。
在Visual Studio中,增加编译时间的一个选项是增量链接( / INCREMENTAL )。它与链接时代码生成( / LTCG )不兼容,因此请记住在执行发布版本时禁用增量链接。
答案 24 :(得分:1)
从Visual Studio 2017开始,您可以掌握一些编译器指标,以了解所需的时间。
在项目属性窗口中将这些参数添加到C / C ++->命令行(其他选项):
/Bt+ /d2cgsummary /d1reportTime
您可以获取更多信息in this post。
答案 25 :(得分:1)
使用动态链接而不是静态链接可以使编译器更快地感觉到。
如果使用t Cmake,请激活该属性:
set(BUILD_SHARED_LIBS ON)
Build Release,使用静态链接可以获得更多优化。
答案 26 :(得分:1)
首先,我们必须了解C ++的与众不同之处,使它与其他语言有所不同。
有人说C ++具有许多功能。但是,嘿,有些语言具有更多的功能,而且远远没有那么慢。
有人说,重要的是文件的大小。不,代码的源代码行与编译时间无关。
但是等等,怎么可能呢?更多的代码行意味着更长的编译时间,这是什么法宝?
诀窍是预处理程序指令中隐藏了许多代码行。是。只有一个#include
会破坏模块的编译性能。
您会看到,C ++没有模块系统。所有*.cpp
文件都是从头开始编译的。因此,拥有1000个* .cpp文件意味着编译您的项目一千次。你还有更多吗?太糟糕了。
这就是C ++开发人员不愿将类拆分为多个文件的原因。所有这些标头都很难维护。
那么除了使用预编译的头文件,将所有cpp文件合并为一个文件并保持头文件的数量最少之外,我们还能做什么?
C ++ 20为我们带来了对模块的初步支持!最终,您将能够忘记#include
和头文件带来的可怕的编译性能。碰到一个文件?仅重新编译该文件!需要编译新的结帐吗?只需几秒钟即可完成编译,而不是几分钟和几小时。
C ++社区应尽快迁移到C ++ 20。 C ++编译器开发人员应更加关注这一点,C ++开发人员应开始测试各种编译器的初步支持,并使用支持模块的那些编译器。这是C ++历史上最重要的时刻!