作者 | Nethercote
編譯 | 王瑞平、言征
Nethercote是一位研究Rust編譯器的軟件工程師。最近,他正在探索如何提升Rust編譯器的性能,在他的博客文章中介紹了Rust編譯器是如何將代碼分割成代碼生成單元(CGU)的以及rustc的性能加速。
他解釋了不同數(shù)量和大小的CGU之間的權(quán)衡以及Rustc是如何使用LLVM并行化代碼生成和優(yōu)化的。此外,Nethercote還探索了一些形成和排序CGU的替代方法,并報告了他的實驗結(jié)果。
Nethercote發(fā)現(xiàn),很多時候,無法在編譯速度、內(nèi)存占用、編譯體積和質(zhì)量上都實現(xiàn)提升,一個指標(biāo)的提升,經(jīng)常伴隨另一個性能指標(biāo)的下降。盡管他沒有發(fā)現(xiàn)比現(xiàn)有方法更明顯的改進(jìn),但還是希望在未來繼續(xù)研究這個問題。
如何提升Rust編譯器速度?這篇文章或許能幫助到你!
1、LLVM:Rust編譯加速的秘訣
Rust的MIR是HIR到LLVM IR的中間產(chǎn)物,將MIR轉(zhuǎn)換為LLVM IR,然后將其傳遞給LLVM,從而生成機(jī)器代碼。在此過程中,LLVM能通過處理多個模塊實現(xiàn)并行。Rustc使用LLVM加速Rust的編譯。我們稱其中的每個模塊為“代碼生成單元(CGU)”。
圖片
圖:時間位于 x 軸上,每條水平線代表一個線程。主線程顯示在頂部,標(biāo)有 PID。它在開始時處于活動狀態(tài),時間足以產(chǎn)生另一個標(biāo)記為 的線程rustc。rustc底部顯示的線程在大部分執(zhí)行過程中都處于活動狀態(tài)。還有 16 個 LLVM 線程標(biāo)記opt cgu.00為 到opt cgu.15,每個線程都會在短時間內(nèi)處于活動狀態(tài)。
CGU實際上是如何形成的呢?粗略地說,Rust 程序由許多函數(shù)組成,這些函數(shù)形成一個有向圖,其中從一個函數(shù)到另一個函數(shù)的調(diào)用構(gòu)成了一條邊。我們需要將這個圖分割成塊(CGU),這是一個圖分區(qū)問題。我們希望創(chuàng)建大小大致相等的 CGU(因此 LLVM 處理它們所需的時間長度大致相同),并最大限度地減少它們之間的邊數(shù)(因為這使 LLVM 的工作更輕松,并帶來更好的代碼質(zhì)量) 。
實際上,由于我們上面看到的階梯效應(yīng),我們不希望 CGU 的大小完全相同。理想的情況是 CGU 大小存在與梯度相匹配的輕微梯度。這樣,所有 CGU 將完全相同地完成處理,以實現(xiàn)最大程度的并行化。
合并之前的CGU(9個)
Nethercote認(rèn)為在合并之前“調(diào)整”CGU可能會有所幫助,在某些情況下將函數(shù)從一個CGU移動到另一個。例如,如果在CGU A中被調(diào)用f的葉函數(shù)(即不調(diào)用任何其他函數(shù)的葉函數(shù))在CGU B中有一個調(diào)用方g,那么將f從A移動到B是有意義的,從而去除CGU間的邊。(還有其他類似的情況涉及非葉函數(shù),移動也有意義)。我實現(xiàn)了這一點,它給出了一些適度的改進(jìn),但我目前還沒有決定它是否值得額外的復(fù)雜性。
調(diào)整之后的CGU(5個)
在實現(xiàn)這一點的同時,我還花了一些時間來可視化調(diào)用圖。我從GraphViz開始。這些圖表對于非常小的程序來說看起來不錯,但對于較大的程序來說,它們很快就變得無法讀取和導(dǎo)航。我在Mastodon上抱怨過這一點,并得到了使用d2的建議,d2速度較慢,但圖形可讀性更強(qiáng)。
圖片
2、后端并行方法的軟肋
圖劃分是一個 NP 難題。有幾種常見的算法,實現(xiàn)起來相當(dāng)復(fù)雜。相反,rustc 做了一些更簡單的事情。首先簡單地為每個 Rust 模塊創(chuàng)建一個 CGU:模塊中的每個函數(shù)都放入同一個 CGU 中。然后,如果 CGU 數(shù)量超過限制(默認(rèn)情況下,非增量構(gòu)建為 16 個,增量構(gòu)建為 256 個),它會重復(fù)合并兩個最小的 CGU,直到達(dá)到限制。這種方法簡單、快速,并以有用的方式利用特定領(lǐng)域的知識——程序模塊往往提供良好的自然邊界。
所有這一切都依賴于測量 CGU 大小的方法。目前使用CGU中的MIR語句的數(shù)量來估計LLVM處理CGU需要多長時間。這里有很大的設(shè)計空間,有許多其他可能的形成和規(guī)劃CGU 的方法。
圖片
這種轉(zhuǎn)換對Rust眾多語法糖進(jìn)行了脫糖,并且極大精簡了Rust的語法(但并非其語法子集),是觀察和分析Rust代碼的常用手段,尤其是在控制流圖和借用檢查等方面。
在這篇文章的最后,Nethercote提供了幾個數(shù)據(jù)集的鏈接,每個數(shù)據(jù)集都記錄了編譯rust -performance基準(zhǔn)時每個CGU的測量值。這些數(shù)據(jù)集包括許多測量靜態(tài)代碼大小的輸入(獨立變量),例如,函數(shù)數(shù)量和MIR數(shù)量等。
Nethercote試著用scikit-learn做一些基本的分析。并且,通過這些基本的分析,能讓Nethercote仔細(xì)推敲到底應(yīng)該搜集哪些測量值。
通過一系列的改進(jìn)優(yōu)化,他獲得的最終數(shù)據(jù)集比剛開始時的數(shù)據(jù)更準(zhǔn)確。但是,并沒有通過這些數(shù)據(jù)獲得多少實際的結(jié)果。實際上,每次我對測量的內(nèi)容改變后都會得到完全不同的結(jié)果。
3、實現(xiàn)更快的Lexer
詞法分析(lexical analysis)是編譯器的第一個階段,實現(xiàn)詞法分析的代碼稱為lexer。
有人最近研究了logos(https://github.com/maciejhirsz/logos)這個在rust中廣受歡迎的lexer。
此前,logos聲稱其目標(biāo)是能比手動實現(xiàn)的lexer更快,作者提出了質(zhì)疑,因為在他看來,通用性和性能無法兼得。因此,他一步步實現(xiàn)了lexer,探索了多種優(yōu)化技巧,并與logos進(jìn)行了多輪性能對比。
最終的結(jié)果表明,手動實現(xiàn)的基于狀態(tài)機(jī)的lexer比logos實現(xiàn)了20%左右的性能提升。
4、從錯誤中學(xué)習(xí):使用Rust實現(xiàn)DLL注入
Rust是一種注重安全性的編程語言,但在某些情況下,開發(fā)人員可能需要使用unsafe關(guān)鍵字來執(zhí)行某些操作。unsafe可以提供更高的性能,但可能會犧牲安全性。因此,開發(fā)人員在使用時需要非常小心。幾個使用unsafe的常見場景包括:訪問裸指針、調(diào)用外部C函數(shù)等,并提供了一些建議和最佳實踐,以確保在使用unsafe時不會引入潛在的安全隱患。
舉個應(yīng)用方面的例子:原來,作者一直在用C++編寫逆向工具,但是,C++這門語言并不友好,于是研究了下如何使用Rust實現(xiàn)DLL注入的“工具”。
大致原理就是讓Rust首先生成一個C樣式的DLL,然后,使用unsafe操作裸指針,操作程序內(nèi)存,最后實現(xiàn)DLL注入就可以了。
5、期待更準(zhǔn)確的估計函數(shù)
Nethercote 希望具有數(shù)據(jù)分析專業(yè)知識的人可以做得更好,重點關(guān)注以下幾個方面:
1)更匹配的估計函數(shù)
2)想要使編譯器比現(xiàn)在更快,一個更好的估計函數(shù)也許不會達(dá)到預(yù)期的效果。我提出了一些更好的統(tǒng)計方法,但并沒有提升編譯速度,甚至變差。
3)CGU調(diào)度效果不可預(yù)測,你不能假設(shè)一個估計函數(shù)好幾個百分點就會使編譯器更快。話雖如此,我希望改進(jìn)力度足夠大,能夠轉(zhuǎn)化為實際的加速。
4)對于估計函數(shù)來說,最好高估CGU編譯所需的時間,而不是低估。
5)我很擔(dān)心過度擬合。數(shù)據(jù)集來自一臺機(jī)器,但實際上,rustc會運行在不同的機(jī)器上,具有各種各樣的體系結(jié)構(gòu)和微體系結(jié)構(gòu)。
6)這些數(shù)據(jù)集來自單一版本的rustc,使用單一版本的LLVM。我擔(dān)心隨著時間的推移準(zhǔn)確性可能會漂移。
7)我更喜歡不太復(fù)雜且易于理解的估計函數(shù)。當(dāng)前的函數(shù)非常簡單,在大多數(shù)情況下只是增加了基本模塊和語句的數(shù)量。例如:0大小的CGU應(yīng)該別估計為花費非常接近于0的時間。
8)估計函數(shù)有一個明確的問題,即如果不考慮其內(nèi)部公式,計算MIR語句可能非常不準(zhǔn)確。特別是,單個MIR語句可能變得很長。舉個例子:深度向量壓力測試的MIR包含一條語句,該語句定義了包含超過100,000個元素的向量字面量。不出所料,當(dāng)前的估計函數(shù)嚴(yán)重低估了編譯這個基準(zhǔn)所需的時間。
Nethercote最后提醒:希望以上的請求是合理的!
以下是上文提到的數(shù)據(jù)集:
- 調(diào)試構(gòu)建,主要基準(zhǔn)測試
https://nnethercote.github.io/aux/2023/07/25/Debug-Primary.txt
- 選擇構(gòu)建,主要基準(zhǔn)
https://nnethercote.github.io/aux/2023/07/25/Opt-Primary.txt
- 調(diào)試構(gòu)建,二級基準(zhǔn)測試
https://nnethercote.github.io/aux/2023/07/25/Debug-Secondary.txt
- 選擇構(gòu)建,二級基準(zhǔn)
https://nnethercote.github.io/aux/2023/07/25/Opt-Secondary.txt
- 順便說一句:在這些數(shù)據(jù)集中,主要基準(zhǔn)測試比次要基準(zhǔn)測試更重要,次要基準(zhǔn)測試包括壓力測試、微基準(zhǔn)測試和其它不符合實際的代碼。
參考資料:
1.https://nnethercote.github.io/2023/07/25/how-to-speed-up-the-rust-compiler-data-analysis-assistance-requested.html
2.https://geo-ant.github.io/blog/2023/unsafe-rust-exploration/
3.https://nnethercote.github.io/2023/07/11/back-end-parallelism-in-the-rust-compiler.html