LLVM 链接时优化:设计与实现¶
描述¶
LLVM 具有强大的模块间优化功能,可在链接时使用。链接时优化 (LTO) 是在链接阶段执行模块间优化的另一种名称。本文档描述了 LTO 优化器和链接器之间的接口和设计。
设计理念¶
LLVM 链接时优化器在编译器工具链中进行模块间优化时,提供了完全的透明性。其主要目标是让开发人员利用模块间优化,而无需对开发人员的 makefile 或构建系统进行任何重大更改。这是通过与链接器的紧密集成实现的。在此模型中,链接器将 LLVM 位代码文件视为本机目标文件,并允许在它们之间进行混合和匹配。链接器使用 libLTO(一个共享对象)来处理 LLVM 位代码文件。链接器和 LLVM 优化器之间的这种紧密集成有助于进行在其他模型中不可能实现的优化。链接器输入允许优化器避免依赖保守的逃逸分析。
链接时优化示例¶
以下示例说明了 LTO 集成方法和简洁接口的优势。此示例需要一个系统链接器,该链接器通过本文档中描述的接口支持 LTO。在这里,clang 透明地调用系统链接器。
输入源文件
a.c
被编译成 LLVM 位代码形式。输入源文件
main.c
被编译成本机目标代码。
--- a.h ---
extern int foo1(void);
extern void foo2(void);
extern void foo4(void);
--- a.c ---
#include "a.h"
static signed int i = 0;
void foo2(void) {
i = -1;
}
static int foo3() {
foo4();
return 10;
}
int foo1(void) {
int data = 0;
if (i < 0)
data = foo3();
data = data + 42;
return data;
}
--- main.c ---
#include <stdio.h>
#include "a.h"
void foo4(void) {
printf("Hi\n");
}
int main() {
return foo1();
}
要编译,运行
% clang -flto -c a.c -o a.o # <-- a.o is LLVM bitcode file
% clang -c main.c -o main.o # <-- main.o is native object file
% clang -flto a.o main.o -o main # <-- standard link command with -flto
在此示例中,链接器识别出
foo2()
是在 LLVM 位代码文件中定义的外部可见符号。链接器完成其通常的符号解析过程,并发现foo2()
在任何地方都未使用。此信息被 LLVM 优化器使用,它移除了foo2()
。一旦
foo2()
被移除,优化器就会识别出条件i < 0
始终为假,这意味着foo3()
永远不会被使用。因此,优化器也移除了foo3()
。而这反过来,使链接器能够移除
foo4()
。
此示例说明了与链接器紧密集成的优势。在这里,没有链接器的输入,优化器无法移除 foo3()
。
替代方法¶
- 编译器驱动程序单独调用链接时优化器。
在此模型中,链接时优化器无法利用在链接器的正常符号解析阶段收集的信息。在上面的示例中,没有链接器的输入,优化器无法移除
foo2()
,因为它在外部可见。这反过来又阻止了优化器移除foo3()
。- 使用单独的工具从所有目标文件收集符号信息。
在此模型中,一个新的、单独的工具或库复制了链接器收集链接时优化信息的能力。这种代码重复不仅难以证明其合理性,而且还有其他几个缺点。例如,在各种平台上,链接语义和链接器提供的功能不是唯一的。这意味着,这个新工具需要在一个超级工具中支持所有这些功能和平台,或者每个平台都需要一个单独的工具。这大大增加了链接时优化器的维护成本,这是不必要的。这种方法还需要与各种平台上的链接器开发保持同步,这不是链接时优化器的主要关注点。最后,由于此单独工具和链接器本身完成的工作重复,这种方法会增加最终用户的构建时间。
libLTO 和链接器之间的多阶段通信¶
链接器收集有关各种链接对象中符号定义和使用的信息,这比其他工具在典型构建周期中收集的任何信息都更准确。链接器通过查看本机 .o 文件中符号的定义和使用情况以及使用符号可见性信息来收集此信息。链接器还使用用户提供的信息,例如导出的符号列表。LLVM 优化器从优化器的角度收集控制流信息、数据流信息,并了解更多关于程序结构的信息。我们的目标是通过在各种链接阶段共享此信息,来利用链接器和优化器之间的紧密集成。
阶段 1:读取 LLVM 位代码文件¶
链接器首先按自然顺序读取所有目标文件并收集符号信息。这包括本机目标文件以及 LLVM 位代码文件。为了在所有 .o 文件都是本机目标文件的情况下最大限度地降低链接器的成本,当发现提供的目标文件不是本机目标文件时,链接器才调用 lto_module_create()
。如果 lto_module_create()
返回文件是 LLVM 位代码文件,则链接器随后使用 lto_module_get_symbol_name()
和 lto_module_get_symbol_attribute()
迭代模块,以获取所有已定义和引用的符号。此信息被添加到链接器的全局符号表。
lto* 函数都在共享对象 libLTO 中实现。这允许 LLVM LTO 代码独立于链接器工具进行更新。在支持它的平台上,共享对象是延迟加载的。
阶段 2:符号解析¶
在此阶段,链接器使用全局符号表解析符号。它可能会报告未定义的符号错误、读取存档成员、替换弱符号等。即使链接器不知道输入 LLVM 位代码文件的确切内容,它也能够无缝地完成此操作。如果启用了死代码删除,则链接器会收集活动符号的列表。
阶段 3:优化位代码文件¶
在符号解析之后,链接器告诉 LTO 共享对象本机目标文件需要哪些符号。在上面的示例中,链接器报告仅 foo1()
被本机目标文件使用,通过使用 lto_codegen_add_must_preserve_symbol()
。接下来,链接器使用 lto_codegen_compile()
调用 LLVM 优化器和代码生成器,该函数返回一个本机目标文件,该文件是通过合并 LLVM 位代码文件并应用各种优化过程创建的。
阶段 4:优化后的符号解析¶
在此阶段,链接器读取优化的本机目标文件,并更新内部全局符号表以反映任何更改。链接器还收集有关 LLVM 位代码文件对外部符号的使用情况的任何更改的信息。在上面的示例中,链接器注意到不再使用 foo4()
。如果启用了死代码删除,则链接器会适当地刷新活动符号信息并执行死代码删除。
在此阶段之后,链接器继续链接,就好像它从未见过 LLVM 位代码文件一样。
libLTO
¶
libLTO
是 LLVM 工具的一部分,是一个共享对象,旨在供链接器使用。libLTO
提供了一个抽象的 C 接口,用于使用 LLVM 过程间优化器,而无需公开 LLVM 内部结构的详细信息。目的是保持接口尽可能稳定,即使在 LLVM 优化器不断发展的情况下也是如此。甚至完全不同的编译技术也应该有可能提供不同的 libLTO,使其与他们的目标文件和标准链接器工具一起工作。
lto_module_t
¶
非本机目标文件通过 lto_module_t
处理。以下函数允许链接器检查文件(在磁盘上或在内存缓冲区中)是否是 libLTO 可以处理的文件
lto_module_is_object_file(const char*)
lto_module_is_object_file_for_target(const char*, const char*)
lto_module_is_object_file_in_memory(const void*, size_t)
lto_module_is_object_file_in_memory_for_target(const void*, size_t, const char*)
如果目标文件可以被 libLTO
处理,链接器将通过使用以下方法之一创建 lto_module_t
lto_module_create(const char*)
lto_module_create_from_memory(const void*, size_t)
完成后,句柄通过以下方式释放
lto_module_dispose(lto_module_t)
链接器可以通过获取符号的数量以及通过以下方式获取每个符号的名称和属性来内省非本机目标文件
lto_module_get_num_symbols(lto_module_t)
lto_module_get_symbol_name(lto_module_t, unsigned int)
lto_module_get_symbol_attribute(lto_module_t, unsigned int)
符号的属性包括对齐方式、可见性和类型。
在 Darwin 上处理目标文件的工具(例如 lipo)可能需要知道诸如 CPU 类型之类的属性
lto_module_get_macho_cputype(lto_module_t mod, unsigned int *out_cputype, unsigned int *out_cpusubtype)
lto_code_gen_t
¶
一旦链接器将每个非本机目标文件加载到 lto_module_t
中,它就可以请求 libLTO
处理所有这些文件并生成本机目标文件。这是分几个步骤完成的。首先,使用以下方法创建一个代码生成器
lto_codegen_create()
然后,使用以下方法将每个非本机目标文件添加到代码生成器
lto_codegen_add_module(lto_code_gen_t, lto_module_t)
然后,链接器可以选择设置一些代码生成选项。是否生成 DWARF 调试信息是通过以下方式设置的
lto_codegen_set_debug_model(lto_code_gen_t)
位置无关代码的类型是通过以下方式设置的
lto_codegen_set_pic_model(lto_code_gen_t)
并且每个被本机目标文件引用或以其他方式引用且不能被优化掉的符号是通过以下方式设置的
lto_codegen_add_must_preserve_symbol(lto_code_gen_t, const char*)
完成所有这些设置后,链接器请求使用这些设置从模块中创建本机目标文件,使用以下方法
lto_codegen_compile(lto_code_gen_t, size*)
这会返回一个指向包含生成的本机目标文件的缓冲区的指针。然后,链接器解析该文件并将其与其余本机目标文件链接。