LLVM是许多令人惊叹的工业和学术项目所采用的最先进的编译器优化和代码生成框架,例如JavaScript引擎中的即时编译(JIT)编译器和机器学习(ML)框架。它是构建编程语言和二进制文件工具的有用工具箱。然而,尽管该项目具有强大的稳健性,但其学习资源分散,文档也不是最佳的。因此,即使是对LLVM有一定经验的开发者,其学习曲线也相当陡峭。本书旨在通过以实用的方式为您提供LLVM中常见和重要领域的知识来解决这些问题——向您展示一些有用的工程技巧,指出鲜为人知但方便的功能,并举例说明。
作为一名LLVM开发者,从源代码构建LLVM一直是您应该做的第一件事。鉴于LLVM如今的规模,这项任务可能需要几个小时才能完成。更糟糕的是,为了反映更改而重新构建项目可能也需要很长时间,并妨碍您的生产力。因此,了解如何使用正确的工具以及如何为您的项目找到最佳构建配置以节省各种资源(尤其是您宝贵的时间)至关重要。
在本章中,我们将涵盖以下主题:
- 通过更好的工具减少构建资源
- 通过调整CMake参数节约构建资源
- 学习如何使用GN(LLVM的另一种构建系统)及其优缺点
技术要求
在写这本书的时候,LLVM只有几个软件要求:
- 支持C++14的C/C++编译器
- CMake
- CMake支持的构建系统之一,例如GNU Make或Ninja
- Python(2.7也可以,但我强烈建议使用3.x)
- zlib
这些项目的确切版本会不时变化。有关更多详细信息,请访问https://llvm.org/docs/GettingStarted.html#software。
本章假设您以前构建过LLVM。如果不是这种情况,请执行以下步骤:
-
从GitHub获取LLVM源代码树的副本:
$ git clone https://github.com/llvm/llvm-project
-
通常,默认分支应该可以无错误构建。如果您想使用更稳定的发布版本,例如发布版本10.x,请使用以下命令:
$ git clone -b release/10.x https://github.com/llvm/llvm-project
-
最后,您应该创建一个构建文件夹,在其中调用CMake命令。所有构建工件也将放置在此文件夹中。可以使用以下命令完成此操作:
$ mkdir .my_build $ cd .my_build
通过更好的工具减少构建资源
正如本章开头所提到的,如果您使用默认(CMake)配置构建LLVM,通过调用CMake并以以下方式构建项目,整个过程很有可能需要几个小时才能完成:
$ cmake ../llvm
$ make all
通过简单地使用更好的工具和更改一些环境可以避免这种情况。在本节中,我们将介绍一些指南,以帮助您选择正确的工具和配置,这些工具和配置既可以缩短您的构建时间,又可以改善内存占用。
用Ninja替换GNU Make
我们可以做的第一个改进是使用Ninja构建工具(https://ninja-build.org),而不是GNU Make,后者是CMake在主要Linux/Unix平台上生成的默认构建系统。
以下是在您的系统上设置Ninja的步骤:
-
例如,在Ubuntu上,您可以使用以下命令安装Ninja:
$ sudo apt install ninja-build
Ninja也在大多数Linux发行版中可用。
-
然后,在为您的LLVM构建调用CMake时,添加一个额外的参数:
$ cmake -G "Ninja" ../llvm
-
最后,使用以下构建命令:
$ ninja all
Ninja的运行速度比GNU Make在像LLVM这样的大型代码库上显著更快。Ninja超快运行速度的背后秘密之一是,虽然大多数构建脚本(例如Makefile
)设计为手动编写,但Ninja的构建脚本build.ninja
的语法更像汇编代码,开发人员不应编辑它,而应由其他高级构建系统(如CMake)生成。Ninja使用类似汇编的构建脚本这一事实使其能够在底层进行许多优化,并摆脱了许多冗余,例如调用构建时较慢的解析速度。Ninja还因在构建目标之间生成更好的依赖关系而享有盛誉。
Ninja在并行化程度方面做出了明智的决策;也就是说,您希望并行执行多少个作业。因此,通常您不需要担心这个问题。如果您想明确分配工作线程的数量,GNU Make中使用的相同命令行选项在这里仍然有效:
$ ninja -j8 all
现在让我们看看如何避免使用BFD链接器。
避免使用BFD链接器
我们可以做的第二个改进是使用BFD链接器以外的链接器,BFD链接器是大多数Linux系统中默认使用的链接器。尽管BFD链接器是Unix/Linux系统上最成熟的链接器,但它没有针对速度或内存消耗进行优化。这将在像LLVM这样的大型项目中造成性能瓶颈。这是因为与编译阶段不同,链接阶段很难进行文件级别的并行化。更不用说构建LLVM时BFD链接器的峰值内存消耗通常需要约20GB,对内存较小的计算机造成负担。幸运的是,野外至少有两个链接器提供了良好的单线程性能和低内存消耗:GNU gold链接器和LLVM自己的链接器LLD。
gold链接器最初由Google开发并捐赠给GNU的binutils
。在现代Linux发行版中,您应该默认在binutils
包中拥有它。LLD是LLVM的子项目之一,具有更快的链接速度和实验性的并行链接技术。一些Linux发行版(例如较新的Ubuntu版本)已经在其软件包存储库中包含了LLD。您也可以从LLVM的官方网站下载预构建版本。
要使用gold链接器或LLD构建您的LLVM源代码树,请使用您想使用的链接器的名称添加一个额外的CMake参数。
对于gold链接器,请使用以下命令:
$ cmake -G "Ninja" -DLLVM_USE_LINKER=gold ../llvm
同样,对于LLD,请使用以下命令:
$ cmake -G "Ninja" -DLLVM_USE_LINKER=lld ../llvm
限制链接时的并行线程数量
限制链接时的并行线程数量是另一种减少(峰值)内存消耗的方法。您可以通过分配LLVM_PARALLEL_LINK_JOBS=<N>
CMake变量来实现,其中N
是所需的工作线程数量。
通过这样,我们已经了解到,仅仅通过使用不同的工具,构建时间就可以显著缩短。在下一节中,我们将通过调整LLVM的CMake参数来提高这种构建速度。
调整CMake参数
本节将向您展示LLVM构建系统中一些最常见的CMake参数,这些参数可以帮助您自定义构建并实现最大效率。
在开始之前,您应该有一个已配置CMake的构建文件夹。以下小节中的大多数内容将修改构建文件夹中的文件;也就是说,CMakeCache.txt
。
选择正确的构建类型
LLVM使用CMake提供的几种预定义构建类型。其中最常见的类型如下:
Release
:如果您没有指定任何类型,这是默认的构建类型。它将采用最高优化级别(通常为-O3)并消除大部分调试信息。通常,这种构建类型会使构建速度略微变慢。Debug
:此构建类型将在没有应用任何优化的情况下编译(即-O0)。它保留所有调试信息。请注意,这将生成大量工件,并通常占用约20GB的空间,因此在使用此构建类型时,请确保您有足够的存储空间。由于没有执行优化,这通常会使构建速度略微加快。RelWithDebInfo
:此构建类型应用尽可能多的编译器优化(通常为-O2)并保留所有调试信息。这是在空间消耗、运行速度和可调试性之间的平衡选择。
您可以使用CMAKE_BUILD_TYPE
CMake变量选择其中之一。例如,要使用RelWithDebInfo
类型,可以使用以下命令:
$ cmake -DCMAKE_BUILD_TYPE=RelWithDebInfo …
建议首先使用RelWithDebInfo
(如果您稍后要调试LLVM)。现代编译器已经走了很长的路来提高优化程序二进制文件中的调试信息质量。因此,总是先尝试一下以避免不必要的存储浪费;如果事情不顺利,您始终可以回到Debug
类型。
除了配置构建类型外,LLVM_ENABLE_ASSERTIONS
是另一个CMake(布尔)参数,用于控制是否启用断言(即assert(bool predicate)
函数,如果predicate参数不为真,则会终止程序)。默认情况下,只有当构建类型为Debug
时,此标志才会为真,但您始终可以手动打开它,即使在其他构建类型中也可以强制执行更严格的检查。
避免构建所有目标
过去几年中,LLVM支持的目标(硬件)数量迅速增长。在撰写本书时,大约有近20个官方支持的目标。每个目标都涉及非平凡的任务,如本地代码生成,因此构建所需时间相当长。然而,您同时处理所有这些目标的可能性很低。因此,您可以使用LLVM_TARGETS_TO_BUILD
CMake参数选择要构建的目标子集。例如,要仅构建X86目标,我们可以使用以下命令:
$ cmake -DLLVM_TARGETS_TO_BUILD="X86" …
您还可以使用分号分隔的列表指定多个目标,如下所示:
$ cmake -DLLVM_TARGETS_TO_BUILD="X86;AArch64;AMDGPU" …
请用双引号括起目标列表!
在某些shell中,例如BASH
,分号是命令的结束符号。因此,如果您不用双引号括起目标列表,CMake命令的其余部分将被截断。
让我们看看如何通过构建共享库来调整CMake参数。
作为共享库构建
LLVM最具标志性的特征之一是其模块化设计。每个组件、优化算法、代码生成和实用程序库等都放在自己的库中,开发人员可以根据使用情况链接单个库。默认情况下,每个组件都构建为静态库(Unix/Linux中的*.a
和Windows中的*.lib
)。然而,在这种情况下,静态库有以下缺点:
- 与静态库链接通常需要比与动态库(Unix/Linux中的
*.so
和Windows中的*.dll
)链接更多的时间。 - 如果多个可执行文件链接到同一组库,就像许多LLVM工具那样,当您采用静态库方法时,这些可执行文件的总大小将显著增加。这是因为每个可执行文件都有这些库的副本。
- 当您使用调试器(例如GDB)调试LLVM程序时,它们通常会在一开始花费相当多的时间加载静态链接的可执行文件,从而妨碍调试体验。
因此,在开发阶段建议使用BUILD_SHARED_LIBS
CMake参数将每个LLVM组件构建为动态库:
$ cmake -DBUILD_SHARED_LIBS=ON …
这将为您节省大量的存储空间并加快构建过程。
分割调试信息
当您以调试模式构建程序时 - 例如在使用GCC和Clang时添加-g
标志 - 默认情况下,生成的二进制文件包含存储调试信息的部分。在使用调试器(例如GDB)调试该程序时,这些信息是必不可少的。LLVM是一个大型且复杂的项目,因此当您使用cmAKE_BUILD_TYPE=Debug
变量以调试模式构建它时,编译的库和可执行文件会附带大量占用大量磁盘空间的调试信息。这导致以下问题:
- 由于C/C++的设计,相同的调试信息可能会嵌入到不同的对象文件中(例如,头文件的调试信息可能会嵌入到包含它的每个库中),从而浪费大量磁盘空间。
- 链接器在链接阶段需要将对象文件及其关联的调试信息加载到内存中,这意味着如果对象文件包含大量非平凡的调试信息,则内存压力会增加。
为解决这些问题,LLVM的构建系统允许我们将调试信息分割到与原始对象文件分开的文件中。通过将调试信息从对象文件中分离,同一源文件的调试信息被压缩到一个地方,从而避免不必要的重复创建并节省大量磁盘空间。此外,由于调试信息不再是对象文件的一部分,链接器不再需要将它们加载到内存中,从而节省了大量内存资源。最后但同样重要的是,此功能还可以提高我们的增量构建速度 - 也就是在(小)代码更改后重建项目 - 因为我们只需要在一个地方更新修改后的调试信息。
要使用此功能,请使用LLVM_USE_SPLIT_DWARF
cmake变量:
$ cmake -DcmAKE_BUILD_TYPE=Debug -DLLVM_USE_SPLIT_DWARF=ON …
请注意,这个CMake变量只适用于使用DWARF调试格式的编译器,包括GCC和Clang。
构建llvm-tblgen的优化版本
TableGen是特定领域语言(DSL),用于描述将作为LLVM构建过程的一部分转换为相应C/C++代码的结构化数据(我们将在后面的章节中了解更多关于此的信息)。转换工具称为llvm-tblgen
。换句话说,llvm-tblgen
的运行时间将影响LLVM本身的构建时间。因此,如果您不是在开发TableGen部分,无论全局构建类型(即CMAKE_BUILD_TYPE
)如何,构建llvm-tblgen
的优化版本总是一个好主意,这将使llvm-tblgen
运行更快,缩短整体构建时间。
例如,以下CMake命令将创建构建配置,除了llvm-tblgen
可执行文件外,其他所有内容都将构建为调试版本,而llvm-tblgen
将构建为优化版本:
$ cmake -DLLVM_OPTIMIZED_TABLEGEN=ON -DCMAKE_BUILD_TYPE=Debug …
最后,您将看到如何使用Clang和新的PassManager。
使用新的PassManager和Clang
Clang是LLVM的官方C族前端(包括C、C++和Objective-C)。它使用LLVM的库生成机器代码,由LLVM中最重要的子系统之一 - PassManager组织。PassManager将所有优化和代码生成所需的任务(即Passes)整合在一起。
在第9章中,使用PassManager和AnalysisManager,将介绍LLVM的新 PassManager,该PassManager从头构建,未来某个时刻将替代现有的PassManager。与传统PassManager相比,新的PassManager具有更快的运行速度。这一优势间接地为Clang带来了更好的运行性能。因此,这里的想法很简单:如果我们使用Clang构建LLVM的源代码树,并启用新的PassManager,编译速度将更快。大多数主流Linux发行版的软件包仓库已经包含了Clang。如果您希望获得更稳定的PassManager实现,建议使用Clang 6.0或更高版本。使用LLVM_USE_NEWPM
CMake变量构建启用新的PassManager的LLVM,如下所示:
$ env CC=`which clang` CXX=`which clang++` \
cmake -DLLVM_USE_NEWPM=ON …
LLVM是一个庞大的项目,需要很长时间来构建。前两节介绍了一些提高其构建速度的有用技巧和提示。在下一节中,我们将介绍一种替代构建系统来构建LLVM。它相对于默认的CMake构建系统具有一些优势,这意味着在某些情况下它会更适用。
总结
LLVM是一个在构建代码优化和代码生成工具方面非常有用的框架。然而,其代码库的规模和复杂性导致了相当多的构建时间。本章提供了一些加速LLVM源代码树构建时间的提示,包括使用不同的构建工具,选择正确的CMake参数,甚至采用除CMake之外的构建系统。这些技能减少了不必要的资源浪费,提高了您在使用LLVM开发时的生产力。
在下一章中,我们将深入探讨LLVM基于CMake的构建基础设施,并向您展示在许多不同开发环境中至关重要的构建系统特性和指南。
延伸阅读
您可以查看LLVM使用的所有CMake变量的完整列表,网址为:
https://llvm.org/docs/CMake.html#frequently-used-cmake-variables