跨模块优化(CMO)是提高运行时性能的有效方法,它扩展了跨源模块边界的优化范围。
CMO方法有链接时间优化(LTO)和轻量级过程间优化(LIPO)。
ThinLTO的目标是与常规的非lto构建一样具有可伸缩性,在没有大内存配置的大型应用程序和机器上支持CMO,同时与分布式和增量构建系统很好地集成。
这是通过快速的基于摘要的全程序分析(WPA)来实现的,这是唯一的串行步骤,不需要读取或写入程序的中间表示(IR)。
2.ThinLTO Design
1. Compile : 生成与LTO模式相同的IR,但扩展了模块摘要。
2. Thin Link : 瘦链接器插件层,用于合并摘要和执行全局分析。
3.ThinLTO后端 : 使用基于摘要的导入和优化的并行后端
2.1 ThinLTO Backends
Thin Lto在前端编译成IR的时候就会生成一个包含程序信息的summary部分.但是这部分依然是在中端生成的,所以Thin lto还是支持跨语言的.
这个summary是ThinLTO设计的基石。每个模块将其发送到包含其IR的目标文件中。这些摘要部分的设计使它们可以单独加载,而不需要任何昂贵的构造。每个全局变量和函数在模块摘要中都有一个条目。条目包含驱动thin lto全局分析的元数据。目前,一个函数条目包含链接类型、指令数量、可选的配置文件引导优化(PGO)信息和一些标志。该格式将根据需要进行扩展。此外,每个引用(地址获取、加载/存储)和调用(直接调用目标,或在值配置文件信息中发现的间接调用目标)都被记录下来。调用可选地使用PGO热特性进行修饰,稍后将用于全局分析.
2.2 Thin Link Phase
ThinLTO通常被实现为一个链接插件,以便在现有的构建系统中透明地插入。当这个阶段从链接器获得额外的信息时,一些分析更加准确。此阶段从仅从IR文件中读取摘要部分开始,然后简单地将它们聚合为单个组合的摘要索引。每个模块摘要中记录的引用边和调用边共同为程序建立了一个精确的引用和调用图。
A key aspect of the ThinLTO model is that CMO must besplit into two parts:
1.Analysis:该部分在thin link串行阶段执行,并在参考图上操作。为了使得thin link尽可能快地执行,只有摘要可用于分析。这避免了在内存中解析和加载任何IR。
2.Transformation: 这是在后端阶段并行执行的。它将使用WPA的结果来对IR应用转换。
2.3 ThinLTO Backends
第三阶段(ThinLTO后端)为每个模块并行地执行从IR到本机代码的后端编译,使用的是在thin link阶段计算出的早期基于摘要的分析结果。这些结果用于独立地为每个模块执行实际的转换。链接器被设置为通过线程池从链接器进程并行地启动ThinLTO线程。
3.Incremental Builds
目前对于程序编译,例如使用make,即使修改单个源文件,也总是会重新触发链接步骤以生成最终的二进制文件。
ThinLTO被设计成集成的增量构建系统。这一过程包括以下内容:
1.在第一阶段的末尾,在发出IR文件时,对内容进行hash处理,并将结果附加到文件中。
2.在第三阶段(ThinLTO后端),输入的IR文件hash与第二阶段(thin link)的分析结果相结合,生成一个新的hash。这个散列用作执行缓存查找的键。缓存可以在磁盘上的一个目录中实现,其中的文件是用key命名的。在缓存命中时,后端从缓存加载条目并返回它。对于缓存,优化和代码生成必须进行,结果对象文件在返回给链接器之前提交给缓存。
这个方案是比较粗糙的,因为它在模块级运行,即使修改了一个函数。然而,它很自然地适合LLVM,并保证无论是否涉及增量构建,最终的二进制文件都是相同的。注意,为每个模块的IR计算的散列反映了对命令行选项或配置文件数据的任何更改对其生成的代码的影响。
4.Function Importing
ThinLTO全局分析支持的关键转换是函数导入,其中只有那些被认为可能内联的外部函数被导入到每个模块中。这最小化了每个ThinLTO后端的内存开销,同时最大化了最有影响力的CMO机会:内联。因此,IPO转换在每个使用导入功能扩展的模块上执行。
在第二阶段(thin link)中,将遍历合并的调用图,以确定可能从导入每个模块中获益的函数。这些是典型的小型热函数,在它们的callsite内联是有利可图的。每个调用链都被跟踪,直到没有发现有利可图的导入。导入函数的盈利能力由阈值控制(当前函数大小)。当遍历每个调用链时,阈值会降低,因为与原始导入模块距离较远的调用不太可能内联到导入模块中的函数中。
此外,在进行函数导入决策时,被导入函数引用的符号将被标记为导出。这对局部符号和全局优化(如5.1节后面所述的内部化)都有影响。
在第三阶段(ThinLTO后端),被编译的模块使用组合索引的模块路径符号表中的路径从定义模块中导入已标识的函数。为了减少I/O,从每个模块导入的函数是成批处理的,对于从其中导入的每个并行后端,每个源模块只打开一次。在我们的实现中还使用了其他减少I/O开销的策略,比如在每个IR文件中编码一个偏移量表,从何处加载每个组成函数体的IR。
函数导入转换在每个后端很早就发生,以便内联和其他IPO可以利用扩展模块。导入的函数符号被标记,以便在IPO之后可以删除它们的定义,从而防止进一步的编译时影响。
Promotion of Symbols with Local Linkage
当导入的函数在原始模块中包含对本地符号的引用时,必须将该符号提升到全局范围,以便从导入模块中引用它。
此外,为了消除提升局部变量与其他提升局部变量或具有相同名称的全局变量之间的歧义,必须重新命名提升符号。
但是,符号需要在多个独立的ThinLTO后端过程中一致地重命名:原始的定义(导出)模块,以及导入其引用的所有模块。
因此,重命名方案应该应用与合并的汇总索引中的源模块相关联的标识符。
我们只是附加源模块的IR文件的SHA-1散列,它记录在合并索引中,如7.1节所述。
5. ThinLTO Cross-Module Optimizations
除了函数导入/内联之外,还可以使用ThinLTO执行其他全局分析和优化(包括WPO)。如第2.2节所述,这些优化分为两部分。第一部分是基于索引的全局分析,在瘦链接阶段执行,其结果记录在索引中。第二部分是转换,使用索引中记录的信息,独立地应用于ThinLTO后端的IR。本节描述其中一些全局分析和优化。
5.1 Internalization
在常规的LTO中,当所有的IR合并成一个单独的单片模块后,编译器就可以看到IR中所有的符号定义和使用。任何不需要在LTO合并模块外部可见的符号都可以被内部化,这意味着它可以从全局符号转换为局部符号。在LLVM中,这是通过改变链接类型来实现的
内部化有几个优点。首先,如果没有引用,那么编译器可以丢弃符号定义.例如内联之后,就可以得到一个更小的目标文件和二进制文件。例如LLVM很可能内联一个只有一个callsite的本地函数,因为这个函数可以在调用后立即丢弃。另一个优点是,所有符号的使用都是已知的,这可以导致更精确的分析和更积极的优化。本地函数不受ABI约束:调用约定可以随意调整,未使用的函数参数可以删除。对于本地变量,编译器可能会得出未使用地址的结论,从而支持更准确的别名分析。
在ThinLTO中,我们没有一个单独的单片模块来执行内部化。此外,由于函数导入,我们将为每个模块创建新的外部引用,如前面第4节所述。然而,在第二阶段(thin link),使用参考图和函数导入分析的结果,我们标记任何只从其定义模块中引用的全局符号。
与常规的LTO相比,ThinLTO可以应用的内部化的数量本来就比较少,因为保持模块分离会导致跨模块引用。然而,在最终的本机对象链接期间,由于链接器剥离,导致的ThinLTO文本大小几乎与常规LTO相同,如第8.5节所示。
5.2 Weak Symbol Resolution
对于弱链接的符号,链接器将保留一个主要副本,丢弃剩余的抢占副本。虽然将ThinLTO后端中所有弱符号的副本都发出并让最终的本机对象链接选择一个是合法的,但是我们可以通过后端将抢占的副本标记为内联后删除,从而减少编译时间。此外,在LLVMthere两类弱链接[25]:弱链接这意味着可能会有使用之外的模块和符号定义总是排放到本机对象,和linkonce链接这意味着符号定义可能下降在编译过程中如果没有该模块中引用(例如在内联)。因此,如果某个linkonce符号有任何导出的引用,则必须将当前副本提升为弱链接,以确保在链接时保留它以满足任何导出的引用。
5.3 Indirect Call Promotion
正如将在7.2.2节中描述的,来自间接callsite的已分析引用被记录在函数摘要中。在瘦连接期间,可以提升和进一步内联的主要间接呼叫目标被标记为导入到呼叫方模块。实际的间接调用提升转换发生在后端编译阶段。
6. Distributed Build Implications
为了支持分布式构建,全局分析的结果必须序列化到磁盘。但是,与其将应用程序的整个组合索引传输到每个远程构建机器,不如为每个模块发出包含该模块后端编译结果的组合索引子集。这个子集包括模块自己定义的符号和应该导入的函数的摘要,以及每个导入模块的模块路径符号表条目。这将导入决策与索引中记录的全局分析结果(如第5节所述)一起传递到后端。此外,对于没有网络文件系统的分布式构建系统,包含要导入的函数的中间目标文件必须暂存到远程机器的本地存储中。为了帮助这个过程,thin link阶段被配置为发出中间对象文件的纯文本列表,这些文件是每个模块后端调用的额外输入。
7. Implementation
% clang -flto=thin -O2 file1.c file2.c -c
% clang -flto=thin -O2 file1.o file2.o
目前,gold、ld64和lld链接器支持LLVM IR的ThinLTO链接。默认情况下,链接器将在并行线程中启动第三阶段(ThinLTO后端),将生成的本机对象文件传递回链接器以获得最终的本机链接。因此,使用模型与非lto相同,不需要对现有构建系统进行任何更改。所有LLVM实用程序都支持ThinLTO中间对象文件。本节描述了实现的llvm特定方面。
7.1 LLVM Bitcode Representation
当为clang/LLVM编译步骤调用常规LTO或ThinLTO时,得到的目标文件包含用位码文件格式[24]编码的LLVM IR。位码格式本质上是结构化数据的二进制编码,被组织成包含数据记录的嵌套块。
7.1.1 Module Bitcode Files
对于ThinLTO,第一阶段(编译)生成的模块位码文件需要增加两个。首先,第2.1节中描述的模块摘要被编码到一个新的块中,可以很容易地读取它,而不需要解析IR的其余部分。其次,为了支持程序的增量重新链接,需要动态地计算序列化位码的散列,并在文件中记录产生的SHA1标识符。第3节详细介绍了增量构建支持。
7.1.2组合索引
在第二阶段创建的组合索引(瘦链接)也可以序列化为独立的位码文件。当后端线程从链接器启动时,通常使用内存中的索引,但将它序列化出来对于调试很有用,对于第6节中介绍的分布式构建方案也很有用。第2.2节中描述的组合索引包含从各个模块摘要块聚合而来的摘要块和模块路径表。它还包含一个全局惟一标识符(GUID)的符号表,用于汇总中的值。GUID是由原始符号名形成的标识符的MD5散列,并为本地值附加源文件路径。这不仅比包含完整的符号名称更紧凑,而且有助于与间接调用配置文件的集成,如第7.2.2节所述。
7.2 Integration with Profile Guided Optimization(PGO)
虽然ThinLTO不需要配置文件反馈,但它是互补的。当为PGO[4]提供时,该索引被设计成与概要数据集成。
7.2.1 Direct Call Edge Profiles
如2.1节所述,概要中对概要数据进行了编码:调用边缘可以标记为“热”或“冷”。此信息将与其他汇总信息一起传播到组合的汇总索引中。它用于帮助指导函数导入决策,如第8节所示。
7.2.2 Indirect Call Profiles
基于配置文件的间接调用提升[1]是一种有效的技术,它不仅可以减少间接分支的频率,而且更重要的是可以使无法静态解析的间接调用目标内联。由于虚函数的存在,间接调用在c++中特别普遍。
LLVM编译器支持间接调用目标的值分析。在随后的PGO编译中,最多包含两个频繁目标的热间接调用将被转换为对这些目标的一系列直接调用,由目标地址检查和一个通过的间接调用来保护。
在LLVMimplementation中,配置文件使用GUID对已分析的目标进行编码,GUID是原始目标callee名称的MD5散列,并为本地函数附加源文件。间接调用值配置文件数据作为元数据[25]附加到IR中的间接调用指令。间接调用提升传递将使用此元数据为热间接调用创建保护的直接调用。
由于可能的目标是通过其名称的散列来标识的,因此只能对模块中可用的目标函数进行转换(需要从散列映射到函数名)。此外,间接调用提升的大部分好处来自能够内联新的直接调用函数的主体。对于单片LTO,所有的功能都是可用的。对于ThinLTO,间接调用的目标函数需要通过第4节中介绍的函数导入转换来实现。当在第一阶段(编译)构建模块摘要时,附加到IR中的间接调用的间接调用值概要元数据被转换为每个关联GUID的额外直接调用边缘。注意,这个GUID是用于组合索引中的符号的相同全局标识符,如7.1.2节所述。合并索引将包括在应用程序IR中定义的所有概要目标的指南索引摘要。这些基于概要文件的间接调用边缘与直接调用边缘看起来完全相同,因此以相同的方式导入。第三阶段(ThinLTO后端)中随后的间接调用提升通道将能够提升导入模块的任何经过分析的间接调用目标定义,然后将其内联到新的直接调用中。
8. Performance Evaluation
除非另有说明,否则将ThinLTO配置为仅导入被调用的外部函数,这些函数在该调用的摘要中记录的指令少于100条。此外,如第4节所述,当遍历调用链时,指令阈值会降低。默认的衰减系数是0.7(因此导入函数的第一级的调用有70的指令限制,依此类推)。当PGO数据可用时,如第7.2.1节所述,标记为热的边缘被给予300条指令的较高限制,衰减系数为1.0.