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

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

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

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


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


语言技巧

皮姆普成语

看看这里的Pimpl成语,这里也称为不透明指针或句柄类。 它不仅加快了编译速度,而且与非抛出交换功能结合在一起时也增加了异常安全性。 Pimpl成语可以减少标题之间的依赖关系,并减少需要完成的重新编译的数量。

转发声明

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

I / O流特别以降低构建而闻名。 如果你在一个头文件中需要它们,请仅在实现文件中尝试#including <iosfwd>而不是<iostream>和#include <iostream>头文件。 <iosfwd>标题仅保存前向声明。 不幸的是,其他标准头文件没有相应的声明头文件。

优先传递参考以传递函数签名中的值。 这将消除在头文件中#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
  • 团结建设
  • distcc的
  • 使用较低的优化级别

    编译器试图优化的越多,就越难以工作。

    共享库

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

    获得更快的计算机

    更多的内存,更快的硬盘驱动器(包括固态硬盘)以及更多的CPU /内核都会影响编译速度。


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

  • 物理结构和C ++ - 第1部分:初看
  • 物理结构和C ++ - 第2部分:构建时间
  • 包含更多的实验
  • Incredibuild有多难以置信?
  • 关心和喂食预先编译的标题
  • 追求完美的构建系统
  • 寻求完美的构建系统(第二部分)
  • 当然,它们很老 - 你必须用最新版本(或可用的版本)重新测试一切,以获得切合实际的结果。 无论哪种方式,它都是创意的好来源。


    我在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的过程大概需要一个小时左右。

    应用修补程序后,可以使用位于安装时指定的build文件夹中的templight++来编译代码。

    确保templight++在您的PATH中。 现在编译将以下开关添加到您的Makefile中的CXXFLAGS或命令行选项中:

    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 <typename T, typename U>
    struct foo { };
    

    并且TU都可以有10个不同的选项,所以您已经将此类的可能模板实例增加到100.解决此问题的一种方法是将代码的公共部分抽象为不同的类。 另一种方法是使用继承反转(颠倒类层次结构),但确保在使用此技术之前,您的设计目标不会受到影响。

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

    使用这种技术,您可以编译一次公共部分,稍后将其与其他TU(翻译单位)链接。

    使用extern模板实例化(自C ++ 11以来)

    如果你知道一个类的所有可能的实例,你可以使用这种技术来编译不同翻译单元中的所有案例。

    例如,在:

    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.ccfoo2.ccfoo3.cc ,它们都包含STL的tuple 。 您可以创建一个foo-all.cc

    #include "foo1.cc"
    #include "foo2.cc"
    #include "foo3.cc"
    

    您只能编译一次该文件,并有可能减少这三个文件中的常见实例。 通常难以预测改善是否显着。 但一个显而易见的事实是,你的构建中会失去并行性(你不能再同时编译这三个文件)。

    此外,如果这些文件中的任何一个碰巧占用大量内存,那么在编译结束之前,实际上可能会耗尽内存。 在一些编译器上,比如GCC,这可能是ICE(内部编译器错误)编译器缺少内存的原因。 所以,除非你知道所有的优点和缺点,否则不要使用这种技术。

    预编译头文件

    预编译头文件(PCHs)通过将头文件编译为可由编译器识别的中间表示形式,可以节省大量编译时间。 要生成预编译头文件,只需要使用常规编译命令编译头文件。 例如,在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 <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上,您可以通过将默认或隐藏传递给编译器的-visibility选项来控制符号的可见性。 这在某种意义上类似于未命名的命名空间,但是以更加精细和侵入性的方式。

    如果您想指定每个案例的可见性,则必须将以下属性添加到您的函数,变量和类中:

    __attribute__((visibility("default"))) void  foo1() { }
    __attribute__((visibility("hidden")))  void  foo2() { }
    __attribute__((visibility("hidden")))  class foo3   { };
    void foo4() { }
    

    GCC中的默认可见性是默认(public),这意味着如果您将上述内容编译为共享库( -shared )方法, foo2foo3类在其他TU中将不可见( foo1foo4将可见)。 如果使用-visibility=hidden编译,则只有foo1可见。 即使foo4也会隐藏起来。

    您可以阅读更多关于GCC维基上的可见性。

    链接地址: http://www.djcxy.com/p/14969.html

    上一篇: What techniques can be used to speed up C++ compilation times?

    下一篇: Why Compilation Speed Differs a lot in C++ and C#?