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,这些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*)
它返回指向包含生成的原生目标文件的缓冲区的指针。然后,链接器解析该文件并将其与其余的原生目标文件链接。