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
始終為 false,這表示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()
。接下來,連結器調用 LLVM 最佳化器和程式碼產生器,使用 lto_codegen_compile()
,它返回一個通過合併 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*)
它返回一個指向緩衝區的指針,該緩衝區包含產生的原生物件檔案。然後,連結器解析它並將其與其餘原生物件檔案連結。