Rust和C / C ++的跨语言链接时间优化LTO
Rust和C / C ++的跨語言鏈接時間優化LTO
 鏈接時間優化(LTO)是LLVM實施整個程序優化的方法。跨語言LTO是Rust編譯器中的一項新功能,使LLVM的鏈接時間優化可以在混合的C / C ++ / Rust代碼庫中執行。也是一項功能,完美結合了Rust編程語言和LLVM編譯器平臺的兩個優點:
 ? Rust缺乏語言運行時和底層訪問能力,幾乎具有與現有C / C ++代碼庫無縫集成的獨特能力,并且
 ? 作為語言不可知的基礎,LLVM提供了一個通用基礎,在該基礎上不再需要編寫特定代碼的源語言。
 那么,跨語言LTO會做什么?有兩個答案:
 ? 從技術角度來看,允許在不考慮實現語言邊界的情況下優化代碼庫,即使在編寫了一個編譯單元的情況下,也可以跨單個編譯單元執行重要的優化(如函數內聯)用Rust編寫,另一個用C ++編寫。
 ? 從心理學的角度(可能同樣重要)來看,有助于減輕許多注重性能的開發人員在開發一款在不同源語言實現的功能之間來回跳動的軟件,可能會產生的效率低下的煩惱感。
 由于Firefox是一個大型的,對性能敏感的代碼庫,其中很多部分都用Rust編寫,因此跨語言LTO一直是Firefox開發人員中最喜歡的愿望清單。因此,Mozilla的低級工具團隊的工作人員全力以赴,以在Rust編譯器中實現。
為了解釋跨語言LTO的工作原理,退后一步并回顧LLVM世界中傳統的編譯和“常規”鏈接時間優化是如何工作的,這很有用。
 背景-LLVM編譯管道的鳥瞰圖
 Clang和Rust編譯器都遵循類似的編譯工作流程,在某種程度上由LLVM規定:
-  編譯器前端.bc為每個編譯單元生成一個LLVM位代碼模塊()。在C和C ++中,每個源文件將產生一個編譯單元。在Rust中,每個板條箱至少轉換為一個編譯單元。 .c --clang–> .bc .c --clang–> .bc .rs --+ 
 |
 .rs --±-rustc–> .bc
 |
 .rs --+
-  下一步,LLVM的優化管道將單獨優化每個LLVM模塊: .c --clang–> .bc --LLVM–> .bc (opt) .c --clang–> .bc --LLVM–> .bc (opt) .rs --+ 
 |
 .rs --±-rustc–> .bc --LLVM–> .bc (opt)
 |
 .rs --+
-  LLVM然后將每個模塊降低為機器代碼,以便每個模塊得到一個目標文件: .c --clang–> .bc --LLVM–> .bc (opt) --LLVM–> .o .c --clang–> .bc --LLVM–> .bc (opt) --LLVM–> .o .rs --+ 
 |
 .rs --±-rustc–> .bc --LLVM–> .bc (opt) --LLVM–> .o
 |
 .rs --+
-  最后,鏈接器將獲取目標文件集,并將鏈接在一起成為二進制文件: .c --clang–> .bc --LLVM–> .bc (opt) --LLVM–> .o ------+ 
 |
 .c --clang–> .bc --LLVM–> .bc (opt) --LLVM–> .o ------+
 |
 ±-ld–> bin
 .rs --+ |
 | |
 .rs --±-rustc–> .bc --LLVM–> .bc (opt) --LLVM–> .o --+
 |
 .rs --+
 如果不涉及任何LTO,這就是常規的編譯工作流程。每個編譯單元都是單獨進行優化的。優化器不知道其它編譯單元內部的函數定義,因此無法內聯或根據實際作用做出其它類型的決策。為了使內聯和優化能夠跨越編譯單元邊界進行,LLVM支持鏈接時間優化。
 LLVM中的鏈接時間優化
 LTO的基本原理是,某些LLVM的優化過程被推回到鏈接階段。為什么要鏈接階段?因為那是流水線中的整個程序(即整個編譯單元集)一次可用的點,因此跨編譯單元邊界的優化成為可能。通過鏈接程序的插件,可以在鏈接階段執行LLVM工作。
這是LTO具體實現的方式:
 ? 編譯器將每個編譯單元轉換為LLVM位代碼(即,跳過降級為機器代碼),
? 鏈接器通過LLVM鏈接器插件知道如何讀取LLVM位代碼模塊(例如常規目標文件),以及
? 鏈接器再次通過LLVM鏈接器插件,合并它遇到的所有位碼模塊,然后在執行實際鏈接之前運行LLVM優化過程。
 有了這些功能后,啟用了C ++代碼LTO的新編譯工作流程如下所示:
.c --clang--> .bc --LLVM--> .bc (opt) ------------------+ - - +|     |
.c --clang--> .bc --LLVM--> .bc (opt) ------------------+ - - +|     |+-ld+LLVM--> bin
.rs --+                                                 ||                                                 |
.rs --+--rustc--> .bc --LLVM--> .bc (opt) --LLVM--> .o -+|
.rs --+
Rust代碼仍被編譯為常規目標文件。因此,Rust代碼對于鏈接時進行的優化是不透明的。但是,看一下該圖,似乎應該不難改變,對吧?
 跨語言鏈接時間優化
 實施跨語言LTO在概念上很簡單,因為該功能是建立在巨頭的肩膀上的。由于Rust編譯器使用LLVM,因此所有重要的構建塊都可以使用。最終的圖看起來與期望的非常rustc相像,發出了優化的LLVM位代碼和LLVM鏈接器插件,并將其與其余模塊一起整合到LTO流程中:
.c --clang--> .bc --LLVM--> .bc (opt) ---------+|
.c --clang--> .bc --LLVM--> .bc (opt) ---------+|+-ld+LLVM--> bin
.rs --+                                        ||                                        |
.rs --+--rustc--> .bc --LLVM--> .bc (opt) -----+|
.rs --+
盡管如此,實現生產就緒型實施仍然是較大的時間投資。在弄清所有內容如何融合之后,主要的挑戰是讓Rust編譯器生成LLVM位代碼,該代碼既與Clang生成的位代碼兼容,又與鏈接器插件接受的兼容。遇到的一些問題是:
 ? Rust編譯器和Clang都基于LLVM,但可能使用了不同版本的LLVM。由于Rust的LLVM版本通常與特定的LLVM版本不匹配,可以是LLVM信息庫中的任意修訂版,這一事實使情況更加復雜。涉及到的所有LLVM版本實際上都必須緊密匹配,以使事情順利進行。Rust編譯器的文檔為各個版本的Rust和Clang提供了一個兼容性表。
? 默認情況下,Rust編譯器會在將相同的板條箱傳遞給鏈接器之前,在同一個板條箱的所有編譯單元上執行一種特殊的LTO形式,稱為ThinLTO。但是,很快了解到,在嘗試對已經完成該過程的模塊執行另一輪ThinLTO時,LLVM鏈接器插件因分段錯誤而崩潰。考慮并指示Rust編譯器在針對跨語言案例進行編譯時禁用其自身的ThinLTO傳遞,并且實際上一切都很好-直到幾周后即使分段錯誤仍神秘地被禁用,分段錯誤仍神秘地返回。
再次需要進行兩次LTO,這一次是常規LTO傳遞rustc進去,然后將其輸出送入鏈接器插件中的ThinLTO中。這種設置盡管在計算上很昂貴,卻是理想的,可以產生更快的代碼,并且可以在Rust方面更好地消除死代碼。從理論上講,應該可以正常工作。然而rustc,盡管不停地檢查ThinLTO是否被Rust所使用,以某種方式產生的符號名稱顯然已經通過ThinLTO的修改了。隨著問題的持續存在,逐漸開始嚴重質疑對LLVM內部工作的理解,同時,逐漸沒有關于如何進一步調試想法。
當發現Rust的預編譯標準庫仍將啟用ThinLTO時,無論用于測試的編譯器設置如何,都可以想象。標準庫(包括其LLVM位代碼表示)作為Rust二進制發行版的一部分進行編譯,始終使用Rust的構建服務器中的設置進行編譯。在本地進行的完整LTO傳遞rustc會將這個麻煩的位代碼拉到輸出模塊中,又會使鏈接器插件再次崩潰。ThinLTO被libstd默認關閉。
? 經過上述修復后,成功地啟用了跨語言LTO來編譯整個Firefox。不幸的是,發現沒有實際的跨語言優化發生。Clang和rustcLlang都生成LLVM位代碼,LLD生成可運行的Firefox二進制文件,在查看機器代碼時,甚至沒有瑣碎的函數都跨語言邊界內聯。經過調試(沒有意識到LLVM的優化說明),事實證明Clangtarget-cpu在所有函數上都發出了一個屬性,而rustc沒有,這使得LLVM拒絕了內聯機會。
為了防止將來出于類似原因靜默地回歸該功能,在擴展Rust編譯器的測試框架和CI方面付出了相當大的努力。現在,能夠編譯并運行兼容版本的Clang,并使用它來執行跨語言LTO的端到端測試,從而確保小型功能確實可以跨語言邊界內聯。
 這個列表可能還會持續一段時間,每個其它目標平臺都會帶來新的驚喜。為了保持對許多活動部件的控制,必須仔細進行每一步的回歸測試。對基礎實現充滿信心,Firefox提供了一個大型,復雜,多平臺的測試用例。
 使用跨語言LTO:一個簡單的例子
 確切的構建工具調用會有所不同,具體取決于是由rustcClang還是Clang執行最后的鏈接步驟,以及Rust代碼是通過Cargo還是rustc直接進行編譯。Rust的編譯器文檔描述了各種情況。其中最簡單的rustc一個直接生成一個靜態庫,而Clang進行鏈接,如下所示:
# Compile the Rust static library, called "xyz"
rustc --crate-type=staticlib -O -C linker-plugin-lto -o libxyz.a lib.rs# Compile the C code with "-flto"
clang -flto -c -O2 main.c# Link everything
clang -flto -O2 main.o -L . -lxyz
該-C linker-plugin-lto選項指示Rust編譯器發出LLVM位代碼,然后可將其用于“完整”和“薄” LTO。第一次進行設置可能非常麻煩,所有涉及的編譯器和鏈接器都必須是兼容版本。從理論上講,大多數主要的連接器都會起作用。實際上,LLD似乎是Linux上最可靠的LLD,第二名是Gold,BFD鏈接器的版本至少應為2.32。在Windows和macOS上,唯一經過正確測試的鏈接器ld64分別是LLD和LLD。對于ld64Firefox,使用修補程序版本是因為LLVM位代碼rustc產生喜歡觸發此鏈接器與ThinLTO預先存在的問題的信息。
結論
 針對Windows,macOS和Linux上的Firefox發布版本啟用了跨語言LTO,Mozilla的低級工具團隊,對此感到滿意。盡管仍然需要簡化該功能的初始設置,已經啟用了從Firefox中的Rust組件中刪除重復的邏輯的功能,因為代碼可以簡單地調用等效的C ++實現并依賴于這些調用。即使可以將跨語言的LTO進行適當的測試并進行持續測試,即使將與現有的C ++代碼緊密集成在一起,也肯定會降低在Rust中實現新組件的心理門檻。
從1.34版開始,Rust編譯器中提供了跨語言LTO,并且可以與Clang 8一起使用。可以嘗試一下,并報告Rust Bug Tracker中的任何問題。
總結
以上是生活随笔為你收集整理的Rust和C / C ++的跨语言链接时间优化LTO的全部內容,希望文章能夠幫你解決所遇到的問題。
 
                            
                        - 上一篇: Dockerfile构建实践
- 下一篇: MLIR Python绑定
