您使用了哪些策略来缩短大型项目的构建时间?

Posted

技术标签:

【中文标题】您使用了哪些策略来缩短大型项目的构建时间?【英文标题】:What strategies have you used to improve build times on large projects? 【发布时间】:2021-11-22 09:21:20 【问题描述】:

我曾经参与过一个 C++ 项目,需要大约一个半小时才能完全重建。小型编辑、构建、测试周期大约需要 5 到 10 分钟。这是一场徒劳的噩梦。

您曾经经历过的最糟糕的构建时间是什么?

更新:

您认为所使用的语言在多大程度上是造成问题的原因?我认为 C++ 容易对大型项目产生大量依赖,这通常意味着即使是对源代码的简单更改也可能导致大规模重建。您认为哪种语言最能解决大型项目的依赖问题?

【问题讨论】:

已重新标记以添加 c++,因为我猜测构建过程将是合理的特定于语言的......不过我可能错了。 ***.com/questions/373142/… 【参考方案1】:
    转发声明 pimpl 成语 预编译头文件 并行编译(例如 Visual Studio 的 MPCL 插件)。 分布式编译(例如 Incredibuild for Visual Studio)。 增量构建 在几个“项目”中拆分构建,因此如果不需要,不要编译所有代码。

[稍后编辑] 8. 购买速度更快的机器。

【讨论】:

很棒的清单。我还要添加“购买更快的机器” 我会第二个 Incredibuld。对于我们的项目,构建时间从 2 小时缩短到大约 10 分钟,其中 6-7 台开发人员机器每台提供 1 个 CPU。 我不会说更快的机器和更快的驱动器一样 - 在我的一个非常大的 C++ 项目中,RAID 加速了非常大的数量。不相关的是,如果一个人在一个未拆分为 DLL 的大型 Visual C++ 项目上进行增量构建,如果关闭增量链接,链接阶段实际上会更快。 @不确定:我发现 VC8+ 的磁盘绑定比 VC6 少得多。现在,并行构建似乎是“最大的收益”。 驱动器可能不如并行性重要,但我听说在构建方面使用 SSD 简直太棒了。【参考方案2】:

我的策略很简单——我不做大型项目。现代计算的整个推动力是从巨大的和单一的转向小型的和组件化的。因此,当我处理项目时,我会将事物分解为可以独立构建和测试的库和其他组件,并且它们之间的依赖关系最小。在这种环境中“完整构建”实际上从未发生过,因此没有问题。

【讨论】:

如何编写操作系统?你如何编写编译器?并非一切都很简单。 好吧,我写了几个编译器。它们也是模块化的。 一个操作系统内核主要是驱动程序,它们是单独构建的。内核只是操作系统的一小部分,在 unix 中是数百个用户级应用程序。工具链由汇编器、编译器、链接器、各种工具(如 nm 和 objdump 等)组成。 很高兴听到 80 年代的 Unix 哲学现在开始被称为“现代计算的推动力”。 @slacker 好吧,这些东西需要时间才能流行起来。顺便说一句,我是 80 年代的 UNIX 程序员【参考方案3】:

有时会有帮助的一个技巧是将所有内容都包含在一个 .cpp 文件中。由于每个文件处理一次包含,因此可以节省大量时间。 (这样做的缺点是编译器无法并行编译)

您应该能够指定应该并行编译多个 .cpp 文件(Linux 上使用 make 的 -j,MSVC 上使用 /MP - MSVC 还具有并行编译多个项目的选项。这些是单独的选项,并且没有理由你不应该同时使用两者)

同样,分布式构建(例如 Incredibuild)可能有助于减轻单个系统的负载。

SSD 磁盘应该是一个巨大的胜利,虽然我自己还没有测试过(但是 C++ 构建涉及大量文件,这很快就会成为瓶颈)。

谨慎使用时,预编译的头文件也有帮助。 (如果它们必须经常重新编译,它们也会伤害你)。

最后,尽量减少代码本身的依赖关系很重要。使用 pImpl 习惯用法,使用前向声明,尽可能保持代码模块化。在某些情况下,使用模板可以帮助您解耦类并最小化依赖关系。 (当然,在其他情况下,模板会显着减慢编译速度)

但是,是的,你是对的,这在很大程度上是一个语言问题。我不知道另一种语言会受到这种程度的影响。大多数语言都有一个模块系统,允许它们消除头文件,这是一个巨大的因素。 C 有头文件,但它是一种如此简单的语言,编译时间仍然可以管理。 C++ 是两全其美的。一种庞大的复杂语言,以及一种可怕的原始构建机制,需要一次又一次地解析大量代码。

【讨论】:

我同意。我有一个与问题中提到的 C++ 项目类似大小的 C 项目。完全重建只需 20 秒,但由于我之前的糟糕经历,我付出了一些努力来保持低依赖性。 不要把这个建议走得太远。我见过程序员喝'把所有东西放在一个.cpp文件'koolaid。现在我们有超过 17000 行的 .cpp 文件。这使得任何人都无法理解该文件中的内容。 @CJohnson 当然,就像几乎所有东西一样,它可能会被过度使用。我只是提到它是要考虑的因素之一。不过,我相信你误会了。执行此操作的常用方法是将 #include 所有内容放入一个通用 cpp 文件中,然后构建它。这不会给你 17000 行长的文件。它仍然有它的缺点(主要是它阻止了增量重建),但这是一个值得一提的技巧。 是的,我明白你的意思。就像任何东西一样,它有优点也有缺点。 除了不能并行编译的巨大缺点之外,将所有内容包含在单个翻译单元中可能会大量消耗资源,从而导致交换死亡。【参考方案4】:
    多核编译。在 I7 上编译 8 个内核,速度非常快。 增量链接 外部常量 删除了 C++ 类的内联方法。

最后两个使我们的链接时间从大约 12 分钟减少到 1-2 分钟。请注意,仅当事物具有巨大的可见性时才需要这样做,即“无处不在”并且存在许多不同的常量和类。

干杯

【讨论】:

【参考方案5】:

IncrediBuild

【讨论】:

【参考方案6】:

统一构建

Incredibuild

指向实现的指针

前向声明

将项目的“已完成”部分编译到 dll 中

【讨论】:

【参考方案7】:

ccache 和 distcc(用于 C/C++ 项目)-

ccache 缓存编译后的输出,使用预处理文件作为查找输出的“键”。这很好,因为预处理非常快,而且强制重新编译的更改通常不会真正更改许多文件的源。此外,它确实加快了完全重新编译的速度。同样不错的是您可以在团队成员之间共享缓存的实例。这意味着只有第一个获取最新代码的人才能真正编译任何东西。

distcc 在机器网络上进行分布式编译。仅当您有用于编译的机器网络时,这才有用。它与 ccache 配合得很好,并且只会移动预处理的源代码,因此您在编译器引擎系统上唯一需要担心的是它们具有正确的编译器(不需要标题或整个源代码树可见)。

【讨论】:

【参考方案8】:

最好的建议是构建真正理解依赖关系的 makefile,并且不会为了一个小的变化而自动重建世界。但是,如果完全重建需要 90 分钟,而小型重建需要 5-10 分钟,那么很有可能您的构建系统已经这样做了。

构建可以并行完成吗?多核还是多服务器?

为真正静态且不需要每次都重新构建的片段检查预编译位。使用但未更改的第 3 方工具/库是此处理的良好候选者。

如果适用,将构建限制为单个“流”。 “完整产品”可能包括调试版本或 32 位和 64 位版本,或者可能包括每次派生/构建的帮助文件或手册页。删除开发不需要的组件可以显着减少构建时间。

构建是否还打包产品?这真的需要开发和测试吗?该构建是否包含一些可以跳过的基本健全性测试?

最后,您可以重构代码库,使其更加模块化并减少依赖项。 Large Scale C++ Software Design 是学习将大型软件产品解耦为更易于维护和更快构建的东西的绝佳参考。

编辑:在本地文件系统上构建而不是在 NFS 挂载的文件系统上也可以显着加快构建时间。

【讨论】:

【参考方案9】:
    摆弄编译器优化标志, 为 gmake 使用选项 -j4 进行并行编译(多核或单核) 如果您使用 clearmake ,请使用 winking 我们可以去掉调试标志..在极端情况下。 使用一些功能强大的服务器。

【讨论】:

【参考方案10】:

这本书Large-Scale C++ Software Design 有非常好的建议,我在过去的项目中使用过。

【讨论】:

+1 除了建议使用 Pimpls 之外,这是唯一真正解决问题的书籍之一【参考方案11】:
    最小化您的公共 API 尽量减少 API 中的内联函数。 (不幸的是,这也增加了链接器要求)。 最大化前向声明。 减少代码之间的耦合。例如,将两个整数传递给函数,用于坐标,而不是具有自己的头文件的自定义 Point 类。 使用 Incredibuild。但它有时会出现一些问题。 不要将从两个不同模块导出的代码放在同一个头文件中。 使用 Pimple 成语。之前提到过,但需要重复。 使用预编译的头文件。 避免使用 C++/CLI(即托管 c++)。链接器时间也会受到影响。 避免在 API 中使用包含“其他所有内容”的全局头文件。 如果您的代码并不真正需要它,请不要依赖 lib 文件。 了解包含带引号和尖括号的文件之间的区别。

【讨论】:

还有一个:获得固态驱动器。这是改善我们的构建时间的最重要的事情。【参考方案12】:

强大的编译器和并行编译器。我们还确保尽可能少地需要完整构建。我们不会更改代码以使其编译速度更快。

效率和正确性比编译速度更重要。

【讨论】:

【参考方案13】:

在 Visual Studio 中,您可以设置一次编译的项目数量。它的默认值为 2,增加它会减少一些时间。

如果您不想弄乱代码,这将有所帮助。

【讨论】:

【参考方案14】:

这是我们在 Linux 下为开发所做的事情清单:

正如 Warrior 所说,使用并行构建 (make -jN) 我们使用分布式构建(目前icecream 非常容易设置),这样我们可以在给定的时间拥有数十个或处理器。这还具有将构建提供给功能最强大且负载较少的机器的优势。 我们使用 ccache,因此当您执行 make clean 时,您不必真正重新编译未更改的源,它是从缓存中复制的。 另请注意,调试版本通常编译速度更快,因为编译器不必进行优化。

【讨论】:

清理后缓存?只有当我想检查构建系统或者我真的想从头开始重建时,我才会运行 clean。 有时您会因为依赖项发生更改而进行清理(例如,您重命名了标题并且计算的依赖项文件中的依赖项存储会丢失)。 ccache 的另一个有用用途是当对文件所做的唯一更改包括记录代码(将 doxygen 文档添加到函数中)时,这不会更改预处理文件,因此不需要重新编译。【参考方案15】:

我们曾经尝试过创建代理类。

这些确实是一个类的简化版本,只包含公共接口,减少了需要在头文件中公开的内部依赖项的数量。然而,他们付出了沉重的代价,将每个类分散到多个文件中,当对类接口进行更改时,所有这些文件都需要更新。

【讨论】:

【参考方案16】:

一般来说,我从事的大型 C++ 项目的构建时间很慢,非常混乱,代码中散布着许多相互依赖关系(大多数 cpps 中使用的相同包含文件,胖接口而不是瘦接口)。在这些情况下,缓慢的构建时间只是较大问题的症状,也是次要症状。重构以使接口更清晰,并将代码分解为库,从而改善了架构和构建时间。当您创建一个库时,它会迫使您考虑什么是接口,什么不是,这实际上(根据我的经验)最终会改进代码库。如果没有技术原因需要划分代码,一些程序员在维护过程中只会将任何内容放入任何头文件中。

【讨论】:

【参考方案17】:

Cătălin Pitiş 介绍了很多好东西。我们做的其他事情:

拥有一个工具,可为在大型整体项目的特定子区域工作的人员生成精简的 Visual Studio .sln 文件 缓存 DLL 和 pdb 从它们在 CI 上构建时开始,以便在开发人员机器上分发 对于 CI,请确保链接机器特别具有大量内存和高端驱动器 将一些重新生成的文件存储在源代码管理中,即使它们可以作为构建的一部分创建 将 Visual Studio 对需要重新链接的内容的检查替换为我们自己根据情况量身定制的脚本

【讨论】:

【参考方案18】:

这是我的一个小烦恼,所以即使你已经接受了一个很好的答案,我也会插话:

在 C++ 中,与其说是语言本身,不如说是 70 年代的语言强制构建模型,以及头文件繁重的库。

Cătălin Pitiş 的回复唯一有问题的地方是:“购买更快的机器”应该放在首位。这是最简单、影响最小的方法。

在 W2K Professional 上运行 VC6 的老化构建机器上,我最糟糕的是大约 80 分钟。现在,在具有 4 个超线程内核、8G RAM Win 7 x64 和不错的磁盘的机器上,相同的项目(包含大量新代码)只需不到 6 分钟。 (类似的机器,处理器功率减少约 10..20%,配备 4G RAM 和 Vista x86 需要两倍的时间)

奇怪的是,增量构建现在大部分时间都比完全重建慢。

【讨论】:

【参考方案19】:

完整构建大约需要 2 小时。我尽量避免对基类进行修改,因为我的工作主要是实现这些基类,所以我只需要构建小组件(几分钟)。

【讨论】:

【参考方案20】:

创建一些单元测试项目来测试各个库,这样如果您需要编辑会导致大量重建的低级类,您可以在重建整个应用程序之前使用 TDD 来了解您的新代码是否有效。 Themis 提到的 John Lakos 书​​中提供了一些非常实用的建议,可帮助您重组图书馆以实现这一目标。

【讨论】:

以上是关于您使用了哪些策略来缩短大型项目的构建时间?的主要内容,如果未能解决你的问题,请参考以下文章

你的安卓项目编译要花 10 分钟,如何缩短到 1 分钟?

用Vue构建大型单页面应用需要注意哪些?

使用 Flash flex 策略和问题

在大型的 Angularjs 项目中,如何组织您的代码

加快大型项目的自动配置/配置

拆分大型 Angular 应用四种策略