当前位置:  开发笔记 > 编程语言 > 正文

可以使用哪些技术来加速C++编译时间?

如何解决《可以使用哪些技术来加速C++编译时间?》经验,为你挑选了14个好方法。

可以使用哪些技术来加速C++编译时间?

这个问题出现在Stack Overflow问题C++编程风格的一些评论中,我很想知道它有什么想法.

我见过一个相关的问题,为什么C++编译需要这么长时间?,但这并没有提供很多解决方案.


这里投票有Visual Studio支持共享项目之间的预编译头



1> Eclipse..:

语言技巧

Pimpl成语

看看这里的Pimpl习语 ,这里也称为不透明指针或句柄类.它不仅加快了编译速度,还与非投掷交换功能相结合,增加了异常安全性.Pimpl习惯用法可以减少标头之间的依赖关系,并减少需要完成的重新编译量.

前瞻性声明

尽可能使用前向声明.如果编译器只需要知道它SomeIdentifier是结构或指针或其他什么,就不要包含整个定义,从而迫使编译器完成比它需要的更多的工作.这可能会产生级联效应,使这种方式比它们需要的慢.

在I/O流,特别是著名的减缓构建.如果在头文件中需要它们,请尝试#including 而不是#include 实现文件中的头.该头仅持有向前声明.不幸的是,其他标准头文件没有相应的声明头.

首选通过引用传递函数签名中的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选项的选项,而不仅仅是项目级别的并列式.

其他并行工具:

Incredibuild

Unity Build

distcc的

使用较低的优化级别

编译器尝试优化的越多,它就越难以工作.

共享库

将不常修改的代码移动到库中可以减少编译时间.通过使用共享库(.so.dll),您也可以减少链接时间.

获得更快的计算机

更多的RAM,更快的硬盘驱动器(包括SSD)以及更多的CPU /核心都会对编译速度产生影响.


*首选将函数签名传递给pass-by-value.这将消除#include头文件中相应类型定义的需要*这是**错误**,您不需要具有完整类型来声明通过值传递的函数,您只需要完整类型即可*实现*或*使用*该功能,但在大多数情况下(除非您只转发呼叫),无论如何您都需要该定义.
但是,预编译的头文件并不完美.使用它们的一个副作用是你获得的文件比必要的多(因为每个编译单元使用相同的预编译头文件),这可能会强制完全重新编译,而不是必要的.请记住一些事情.
在现代编译器中,#ifnf和#pragma一样快(只要包含保护位于文件的顶部).因此,在编译速度方面,#pragma没有任何好处
即使您只有VS 2005而不是2008,您也可以在编译选项中添加/ MP切换以启用.cpp级别的并行构建.
在编写这个答案时,SSD的价格非常昂贵,但今天它们是编译C++时的最佳选择.编译时可以访问很多小文件.这需要大量的IOPS,这是SSD提供的.
此外,在VS2008中,您可以并行构建多个.cpp文件,而不仅仅是项目.
我倾向于把STL标题和库标题(如windows.h)之类的内容放在预编译头文件中.但是,是的,放入任何即使是半频繁变化的东西也是一个坏主意.
如果是现代编译器,你只包含gcc,那么是的.据我所知,visual c ++不会这样做.

2> Mani Zandifa..:

我在STAPL项目上工作,这是一个经过严格模板化的C++库.偶尔,我们必须重新审视所有技术以减少编译时间.在这里,我总结了我们使用的技术.其中一些技术已在上面列出:

寻找最耗时的部分

虽然在符号长度和编译时间之间没有经证实的相关性,但我们观察到较小的平均符号大小可以改善所有编译器的编译时间.所以你的第一个目标是找到代码中最大的符号.

方法1 - 根据大小对符号进行排序

您可以使用该nm命令根据大小列出符号:

nm --print-size --size-sort --radix=d YOUR_BINARY

在此命令中--radix=d,您可以看到十进制数字的大小(默认为十六进制).现在,通过查看最大的符号,确定是否可以打破相应的类并尝试通过将非模板化部分分解为基类来重新设计它,或者将类拆分为多个类.

方法2 - 根据长度对符号排序

您可以运行常规nm命令并将其传送到您喜欢的脚本(AWK,Python等),以根据符号的长度对符号进行排序.根据我们的经验,这种方法确定了比方法1更好的候选人.

方法3 - 使用Templight

" Templight是一个基于Clang的工具,用于分析模板实例化的时间和内存消耗,并执行交互式调试会话以获得对模板实例化过程的反省".

您可以通过签出LLVM和Clang(说明)并在其上应用Templight补丁来安装Templight .LLVM和Clang的默认设置是调试和断言,这些可能会显着影响编译时间.看起来Templight需要两者,所以你必须使用默认设置.安装LLVM和Clang的过程大约需要一个小时左右.

应用补丁后,您可以使用templight++位于安装时指定的构建文件夹中来编译代码.

确保它templight++在你的PATH中.现在编译将以下开关添加到CXXFLAGSMakefile或命令行选项中:

CXXFLAGS+=-Xtemplight -profiler -Xtemplight -memory -Xtemplight -ignore-system

要么

templight++ -Xtemplight -profiler -Xtemplight -memory -Xtemplight -ignore-system

编译完成后,您将在同一文件夹中生成.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

生成的callgrind文件可以使用kcachegrind打开,您可以在其中跟踪最耗时的内存实例.

减少模板实例化的数量

虽然没有确切的解决方案来减少模板实例化的数量,但有一些指导原则可以帮助:

重构具有多个模板参数的类

例如,如果你有一个班级,

template 
struct foo { };

两者的TU可以有10个不同的选项,你已经增加了这类可能的模板实例,以100的一种方式来解决,这是抽象的代码的公共部分,以不同的类.另一种方法是使用继承反转(反转类层次结构),但在使用此技术之前,请确保您的设计目标不会受到影响.

将非模板化代码重构为单个翻译单元

使用此技术,您可以编译公共部分一次,然后将其与其他TU(翻译单元)链接.

使用extern模板实例化(从C++ 11开始)

如果您知道类的所有可能的实例化,则可以使用此技术在不同的转换单元中编译所有情况.

例如,在:

enum class PossibleChoices = {Option1, Option2, Option3}

template 
struct foo { };

我们知道这个类可以有三个可能的实例化:

template class foo;
template class foo;
template class foo;

将上述内容放在翻译单元中,并在头文件中使用extern关键字,在类定义下面:

extern template class foo;
extern template class foo;
extern template class foo;

如果使用一组通用实例编译不同的测试,这种技术可以节省您的时间.

注意:此时MPICH2忽略显式实例化,并始终在所有编译单元中编译实例化的类.

使用统一构建

统一构建背后的整个思想是包含您在一个文件中使用的所有.cc文件,并仅编译该文件一次.使用此方法,您可以避免重新实现不同文件的公共部分,如果您的项目包含许多常见文件,您可能也会节省磁盘访问.

举个例子,假设你有三个文件foo1.cc,foo2.cc,foo3.cc和它们都包括tuple从STL.您可以创建一个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之前的同一文件夹.

这种技术有两个问题:

    您必须确保预编译的头文件是稳定的并且不会更改(您可以随时更改您的makefile)

    每个编译单元只能包含一个PCH(在大多数编译器上).这意味着如果要预编译多个头文件,则必须将它们包含在一个文件中(例如,all-my-headers.hpp).但这意味着您必须在所有位置包含新文件.幸运的是,GCC有一个解决这个问题的方法.使用-include并为其提供新的头文件.您可以使用此技术以逗号分隔不同的文件.

例如:

g++ foo.cc -include all-my-headers.hpp

使用未命名或匿名的命名空间

未命名的命名空间(也称为匿名命名空间)可以显着减少生成的二进制文件大小.未命名的命名空间使用内部链接,这意味着在这些命名空间中生成的符号对其他TU(转换或编译单元)不可见.编译器通常为未命名的命名空间生成唯一的名称.这意味着如果你有一个文件foo.hpp:

namespace {

template 
struct foo { };
} // Anonymous namespace
using A = foo;

并且您碰巧将此文件包含在两个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() { }

GCC中的默认可见性是默认的(公共),这意味着如果您将上面的内容编译为共享库(-shared)方法,foo2并且类foo3将不会在其他TU 中可见(foo1并且foo4将是可见的).如果你使用编译,-visibility=hidden那么只有foo1可见.甚至foo4会被隐藏起来.

您可以在GCC维基上阅读有关可见性的更多信息.



3> Paulius..:

我推荐这些来自"内部游戏,独立游戏设计和编程"的文章:

物理结构和C++ - 第1部分:初看

物理结构和C++ - 第2部分:构建时间

包括更多实验

Incredibuild有多难以置信?

预编码标题的保养和喂养

寻求完美的构建系统

探索完美构建系统(第2部分)

当然,它们已经很老了 - 您必须使用最新版本(或可用的版本)重新测试所有内容,以获得真实的结果.无论哪种方式,它都是思想的良好来源.



4> Frerich Raab..:

过去对我来说效果很好的一种技术:不要独立编译多个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的实验中)并生成较小的二进制文件.我还怀疑编译器有更多的优化潜力(因为它可以同时看到更多的代码).

这种技术在各种情况下都会中断 例如,如果两个或多个源文件声明具有相同名称的全局函数,编译器将会挽救.我找不到任何其他答案中描述的这种技术,这就是为什么我在这里提到它.

值得一提的是,自1999年以来,KDE项目使用了这种完全相同的技术来构建优化的二进制文件(可能用于发布).调用了构建配置脚本的切换--enable-final.出于考古学的兴趣,我挖出了宣布这个功能的帖子:http://lists.kde.org/? l = kde-devel&m = 92722836009368&w = 2


我不确定它是否真的相同,但我想在VC++中启用"整个程序优化"(http://msdn.microsoft.com/en-us/library/0zza0de8%28VS.71%29.aspx )应该对运行时性能产生与您建议的相同的影响.但是,编译时间肯定会更好!

5> Johannes Sch..:

我将链接到我的另一个答案:你如何减少编译时间,并链接Visual C++项目的时间(本机C++)?.我想添加的另一点,但经常出现问题的是使用预编译的头文件.但请注意,只能将它们用于几乎不会改变的部分(如GUI工具包标题).否则,他们将花费你比他们最后拯救你更多的时间.

另一个选择是,当你使用GNU make时,打开-j选项:

  -j [N], --jobs[=N]          Allow N jobs at once; infinite jobs with no arg.

我通常拥有它,3因为我在这里有双核心.然后,它将为不同的翻译单元并行运行编译器,前提是它们之间没有依赖关系.链接不能并行完成,因为只有一个链接器进程将所有目标文件链接在一起.

但是链接器本身可以是线程化的,这就是ELF链接器所做的事情.它是优化的线程C++代码,据说可以比旧版本更快地链接ELF对象文件(实际上包含在binutils中).GNU gold ld



6> ChrisW..:

这本主题有一本书,名为大型C++软件设计(由John Lakos编写).

本书预先定义了模板,因此对于该书的内容添加"使用模板,也可以使编译器变慢".



7> Milan Babušk..:

这里有一些:

通过启动多编译作业来使用所有处理器核心(这make -j2是一个很好的例子).

关闭或降低优化(例如,GCC -O1-O2or 更快-O3).

使用预编译的标头.


仅供参考,我发现启动更多流程通常比核心更快.例如,在四核系统上,我通常使用-j8,而不是-j4.原因是当一个进程在I/O上被阻塞时,另一个进程可以编译.

8> David Rodríg..:

一旦你应用了上面的所有代码技巧(前向声明,在公共头文件中将头部包含减少到最小,用Pimpl推送实现文件中的大部分细节......)并且没有其他任何东西可以通过语言获得,请考虑你的构建系统.如果您使用Linux,请考虑使用distcc(分布式编译器)和ccache(缓存编译器).

第一个是distcc,它在本地执行预处理器步骤,然后将输出发送到网络中的第一个可用编译器.它需要网络中所有已配置节点中的相同编译器和库版本.

后者ccache是​​一个编译器缓存.它再次执行预处理器,然后检查内部数据库(保存在本地目录中)是否已使用相同的编译器参数编译该预处理器文件.如果是这样,它只会弹出二进制文件并从第一次运行编译器输出.

两者都可以同时使用,因此如果ccache没有本地副本,它可以通过网络将其发送到另一个带有distcc的节点,否则它只需注入解决方案而无需进一步处理.


我不认为distcc在所有配置的节点上都需要相同的*library*版本.distcc只进行远程编译,而不是链接.它还通过线路发送*preprocessed*代码,因此远程系统上可用的标头无关紧要.

9> questzen..:

当我从大学毕业时,我看到的第一个真正具有生产价值的C++代码在它们之间有了这些神秘的#ifndef ... #endif指令,其中定义了标题.我问那个以非常幼稚的方式编写关于这些总体事情的代码的人,并介绍了大规模编程的世界.

回到这一点,使用指令来防止重复的头定义是我在减少编译时间时学到的第一件事.



10> mr calendar..:

更多内存.

有人在另一个答案中谈到了RAM驱动器.我用80286和Turbo C++(显示年龄)做到了这一点,结果是惊人的.当机器崩溃时数据丢失也是如此.



11> Evan Teran..:

尽可能使用前向声明.如果类声明仅使用指针或类型的引用,则只需转发声明它并在实现文件中包含该类型的标头.

例如:

// 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
}

如果你做得足够多,那么对于预处理器来说,更少的工作意味着更少的工作.



12> OJ...:

您可以使用Unity Build.

​​


@idbrii,链接已经死了.[这是archive.org上的快照](http://web.archive.org/web/20120419191807/http://www.altdevblogaday.com/2011/08/14/the-evils-of-unity-builds /)

13> Scott Langha..:

使用

#pragma once

在头文件的顶部,因此如果它们在翻译单元中被包含多次,则标题的文本将仅被包含并解析一次.


而这些天,常规包括警卫具有相同的效果.只要它们位于文件的顶部,编译器就完全能够将它们视为#pragma一次
尽管广泛支持,但是#pragma曾经是非标准的。参见http://en.wikipedia.org/wiki/Pragma_once

14> dmckee..:

只是为了完整性:构建可能会很慢,因为构建系统是愚蠢的,因为编译器需要很长时间才能完成它的工作.

阅读递归使得被认为有害(PDF)在Unix环境中讨论该主题.

推荐阅读
惬听风吟jyy_802
这个屌丝很懒,什么也没留下!
DevBox开发工具箱 | 专业的在线开发工具网站    京公网安备 11010802040832号  |  京ICP备19059560号-6
Copyright © 1998 - 2020 DevBox.CN. All Rights Reserved devBox.cn 开发工具箱 版权所有