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 共用物件原生物件檔案需要哪些符號。在上面的範例中,連結器使用 lto_codegen_add_must_preserve_symbol()
報告只有 foo1()
被原生物件檔案使用。接下來,連結器使用 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*)
它會傳回一個指向包含所產生原生物件檔案之緩衝區的指標。然後,連結器會解析該檔案並將其與其餘原生物件檔案連結。