LLVM具有強大的模塊間優化功能,可以在鏈接時使用。鏈接時優化(LTO)就是指在鏈接時進行模塊間的優化。本文介紹了LTO優化器與鏈接器在接口上的設計。
鏈接器使用libLTO
(共享對象)來處理LLVM位代碼文件(bitcode)。鏈接器和LLVM優化器之間的緊密集成有助于進行其他模型不可能實現的優化。
LTO示例
以下示例說明了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 是一個bitcode文件
% clang -c main.c -o main.o # <-- main.o 是原生目標文件
% clang -flto a.o main.o -o main # <-- 使用 -flto 將兩個.o文件鏈接
- 在這個例子中,鏈接器會發現函數
foo2()
在LLVM bitcode文件中被定義為一個外部可見的符號,當鏈接器使用pass對此符號進行處理時會發現函數foo2()
從來沒被調用過,此時LLVM優化器會將foo2()
移除。 - 正如
foo2()
被移除時一樣,優化器同時也能觀察到條件i<0
永遠為false
,這意味著函數foo3
也永遠不會被調用。因此,優化器也會移除foo3()
。 - 同樣的,由于
foo3()
被移除,鏈接器也會移除foo4()
。
這個例子演示了LTO的優點,而在沒有鏈接的參與下,優化器是沒辦法確定函數foo3()
是否應該被移除的。
libLTO與linker鏈接器的多階段通信
鏈接器收集有關符號定義的信息,并在各種鏈接對象中使用。鏈接器通過查看本地.o文件中符號的定義和使用以及使用符號可見性來收集這些信息。鏈接器還使用用戶提供的信息,例如導出的符號列表。
LLVM優化器收集控制流信息,數據流信息,并從優化器的角度更多地了解程序結構。我們的目標是在各個鏈接階段共享這些信息,使鏈接器和優化器之間緊密集成。
第一階段:讀取LLVM bitcode文件
鏈接器首先讀取所有的目標文件并收集符號信息,包括原生的目標文件以及LLVM的bitcode文件。為了在所有.o
文件都是原生目標文件的情況下降低鏈接器的開銷,當提供的目標文件不是原生的目標文件時,鏈接器僅調用lto_module_create()
。如果lto_module_create()
返回指明該文件是LLVM 的bitcode文件,那么鏈接器使用lto_module_get_symbol_name()
和lto_module_get_symbol_attribute()
重新載入該模塊,得到所有的符號信息。這些信息被添加到鏈接器的全局符號表中。
所有的lto*
函數都是在共享庫libLTO
中實現的。它可以獨立與鏈接器之外進行更新。它采用懶加載方式。
第二階段:符號處理
在此階段,鏈接器使用全局符號表對符號進行處理??赡軙蠓栁炊x的錯誤,讀取歸檔成員,替換弱符號等。鏈接器即使不知道LLVM bitcode文件的具體內容,也可以執行此操作。如果啟用了死代碼刪除,那么鏈接器將收集活動符號列表。
第三階段:優化bitcode文件
在符號解析后,鏈接器告訴LTO共享庫哪些符號在原生的目標文件中是要用到的。在上面的例子中,鏈接器會通過函數lto_codegen_add_must_preserve_symbol()
向LTO共享庫提供信息:只有foo1()
是原生目標文件將要用到的。接下來鏈接器使用函數lto_codegen_compile()
調用LLVM優化器和代碼生成器,通過合并bitcode文件并應用各種優化pass來返回原生目標文件。
第四階段:優化后的符號處理
此階段,鏈接器讀取優化后生成的原生目標文件并更新內部全局符號表響應所有的更改。對于LLVMbitcode文件使用到的外部符號的更改信息,鏈接器也會將其收集起來。在上面的例子中,由于foo3()
的移除,鏈接器注意到foo4()
不會再被調用,因此也會將foo4()
移除。更新全局符號表。
在這之后,鏈接器繼續進行正常的鏈接操作,與bitcode文件的結合并不會影響到后續的鏈接。
libLTO
libLTO
作為LLVM工具的一部分,它是一個共享對象,旨在供鏈接器使用。libLTO
提供了一個抽象的C接口來使用LLVM過程優化器,并不會暴露LLVM內部的細節實現。
具體libLTO
的一些相關函數見這里