0. RyuJIT Tutorials - RyuJIT 的歷史和架構
索引
- 上一篇:無
- 下一篇:待更新
正文開始
RyuJIT - 即 .NET 的 JIT 編譯器,負責將 IL 代碼編譯為最終用于執行的機器代碼。
本系列為 RyuJIT 教程,將分為多篇進行更新發布,旨在給對 .NET 編譯器有興趣、以及希望參與 .NET JIT 編譯器開發工作的人提供一些參考資料。
說是教程其實也只是我在社區中從事 RyuJIT 相關開發工作的一些經驗和見解,拋磚引玉。
如有疏漏請見諒。
寫在前面
RyuJIT,最初也叫做 JITBlue,是微軟在大概 2014 年前后為 .NET 發布的新一代 JIT 編譯器。“Ryu”則取自日語中龍(竜)的發音,也算是致敬編譯原理的經典書籍——“龍書”。
而 .NET 作為上世紀末就誕生的平臺,原有的 JIT 在生產中用了將近 20 年,為什么需要一個新的 JIT 呢?這就需要了解一些歷史。
一些歷史
.NET 最初的 JIT 編譯器也叫做 JIT32,顧名思義這是給 32 位架構設計的編譯器,最早可以追溯到 1996 年。在當時 32 位是主流架構,并且人們也只關心 32 位架構。JIT32 設計上是一個非常輕量的編譯器,并且擁有良好的代碼生成質量。
然而進入 21 世紀之后 IA64 架構的誕生,.NET 需要運行在 64 位的服務器上。這個時候代碼生成質量成了關鍵,而服務器并不在乎編譯時間,并且內存也很大,.NET 選擇了采用 C++ 編譯器后端(UTC,Universal Tuple Compiler)作為優化器,帶來的結果就是支持了 IA64 架構,但同時編譯器也用到了大量的 O(N^X) 復雜度的優化算法,并且很吃計算機資源。
然后 AMD64 誕生了,這個時候簡單的把 IA64 移植到 AMD64 架構上,就帶來了此后一直沿用了十年的 JIT64。然而此時 64 位架構不再是服務器的專屬,個人電腦也開始用 64 位架構了。
2010 年的時候由于 Windows RT 在 ARM 設備上的嘗試,.NET 需要支持 ARM32 架構。由于個人終端設備的資源很有限,.NET 選擇了將 JIT32 移植到 ARM32 上。然而 ARM32 和 x86 雖然都叫做 32 位,實際上幾乎沒有任何相同之處。雖然花費了大量的精力做了移植,此時的 JIT32 for ARM 的代碼質量實際上相當的糟糕,主打一個能用就行。
而到了 2012 年的時候,ARM64 要來了。此時我們回顧一下歷史,就會發現在這個時候:
- 用于 x86 的 JIT32 現在跟不上時代
- 用于 x64 的 JIT64 編譯速度很慢而且資源消耗大
- 用于 ARM32 的 JIT32 代碼質量很差且難以解決
那 ARM64 應該用哪個實現呢?很顯然無論哪個都難以滿足現代 JIT 的需求。
于是此時 RyuJIT(JITBlue)項目啟動了,既然要做一個新的玩意,那自然要跟上最新的架構。于是 RyuJIT 的目標自然就是:
- 生成的代碼質量要高(性能好)
- 吞吐量要高(編譯快)
- 在所有架構上都有一致的可預測的性能
要做到這些,自然要用上現代的編譯器架構:
- 采用基于 SSA(Static Single Assignment)的優化算法
- 能夠充分利用類型信息的 VN(Value Numbering)
- 單一代碼庫支持各種新特性,比如 SIMD 等
- 架構相關的部分(lowering、codegen)相互隔離
- 等等...
RyuJIT
RyuJIT 復用了 JIT32 的樹狀 IR 結構,重寫大部分前端,然后在 Rationalization 這個步驟將樹形 IR 轉換為線性 IR 后交給新寫的后端。后端的 register allocator 這次用上了 LSRA 而不是 JIT64 那樣的圖著色來提升編譯速度。
這里有一個有趣的小插曲,RyuJIT 最初是打算只重寫大部分前端,然后 Rationalization 后就直接扔給 JIT32 去做代碼生成的,結果做著做著發現這完全就是個錯誤的決定,于是最后把后端也重寫了。
老的 JIT64 的 IR 結構是線性的,畢竟 UTC 顧名思義就是主要在線性 IR 上做文章的編譯器,而把 IL 導入成線性 IR 的開銷非常大。由于 RyuJIT 是將 IL 導入成樹形的 IR,這相比導入到線性 IR 要容易得多,并且速度也更快。
另外,換上了現代編譯器架構,將基于 lexical 的算法更換為基于 semantic 的算法,用上了基于 SSA 和 VN 的優化,RyuJIT 能編譯出質量相當優秀的代碼。
多虧了 SSA,讓 RyuJIT 能用上各種線性或者近線性的算法,最終編譯速度也比 JIT64 快得多,開銷也要更小。而 SSA 也成為了構建 VN 的基石,允許 RyuJIT 引入基于 VN 的各種高級優化。
架構
RyuJIT 在設計上與 runtime 完全獨立,作為一個獨立的編譯器組件存在,不依賴任何的 runtime 實現。因此你可以很容易地將 RyuJIT 作為一個獨立的編譯器模塊拿去給別的項目使用。
正因此,也誕生了不少有趣的項目,例如:
- Pyjion:一個利用 RyuJIT 給 Python 實現了 JIT 的項目
- CoreRT/NativeAOT:把 RyuJIT 當作代碼生成引擎的 AOT 編譯器
- NativeAOT-LLVM:一個把 RyuJIT 和 LLVM 組合實現了 IL 編譯到原生 WebAssembly 的項目
- 等等...
RyuJIT 憑借其多架構支持、出色的編譯速度和良好的代碼生成質量成為了高性能編譯器的一個很好的選擇,可以兼顧編譯速度和代碼性能。而且 RyuJIT 雖然名字里有 JIT,但得益于其模塊化的設計,拿去集成到一個 AOT 編譯器里也是完全沒有問題!
編譯階段
RyuJIT 的編譯過程由多個階段(Phase)組成,整體的編譯流程大概如下:

其中,Importer 到 Rationalization 之前被稱為 RyuJIT 的前端,而 Rationalization 到 Code Generation 被稱為 RyuJIT 的后端。
Importer
IL 代碼首先會經由 Importer 被導入到 RyuJIT IR。
這個過程會展開各種 intrinsics。
Inliner
決定導入的方法調用是否應該被內聯,這一階段涉及到了各種玄學 heuristics 計算收益值去決定是否應該內聯一個方法。
Morph
這個過程會對 BB 進行各種變換,對后續的優化階段做準備。
首先利用指針分析決定對象到底是分配在堆上還是棧上,然后消除死代碼和不必要的地址暴露等等,然后構建 liveness 信息得到 use-def 圖,以進行 forward substitution、physical promotion、copy omission 等等,最后插入 GS cookies。
于此同時 QMARK 和 COLON 也被展開成了塊。
Loop Optimizations
這個過程會識別循環并對循環進行優化。
循環優化包含了一系列優化的組合,例如 loop inversion、loop cloning、loop unrolling 等等。
順便這個過程還會試圖刪除沒必要的 try-catch-finally 塊。
SSA 和 VN-based Optimizations
這個過程會進行數據流的分析,構建 SSA 和 VN。
然后進行各種 VN-based 的優化:loop invariants hosting、copy propagation、branch removal、CSE(Common Sub-expression Elimination)、assertion propagation、bounds check elimination、induction variable optimization 和 dead-store removal 等等。
這里多虧了 SSA 和 VN 使得這些不需要依賴 lexical-based 的方法,而可以通過 VN 來判斷等價計算從而做到精確的優化。
接著刪除不必要的 try-catch-finally,并內聯類型轉換、runtime lookup、static 成員初始化和 thread-local 訪問。
最后做各種 boolean 表達式折疊,以及識別 switch 方便后續展開成 jump table 等等。
Rationalization
這個階段構建 IR 的線性表達形式,使得 IR 既可以按照樹的形式進行遍歷,也可以按照執行順序的線性形式進行遍歷。線性形式將主要用于后端的各階段。
Rationalization 階段還會消除掉所有的 COMMA 和 statements,從而使得 BB 的執行順序能夠完全被 GenTree 的鏈表來表達。
順便一提,這一階段后的 RyuJIT IR 可以輕而易舉地被轉換為 LLVM IR。
Lowering
在這個階段,會按照執行順序遍歷 IR,展開 jump table,并計算 addressing mode,還會給每個節點標記寄存器的需求和約束,以方便后續 register allocator 分配寄存器。
此階段是架構相關的,各架構有著獨立的實現。
Register Allocation
這個階段會進行寄存器分配。這里采用了線性算法(LSRA)來分配寄存器。
Code Generation
最后來到代碼生成。這個階段同樣是架構相關的,各架構有著獨立的實現。
這個階段會決定 frame 布局,遍歷各 block 按照執行順序生成代碼、GC 和調試信息,然后生成 prolog 和 epilog。
架構隔離設計
目前的 RyuJIT 擁有廣泛的架構支持,例如 x86、x64、ARM32、ARM64、LoongArch64 和 RISC-V64 等等。
還記得我前面說的 RyuJIT 在架構相關的部分相互隔離的設計嗎?正是因為這個設計使得給 RyuJIT 添加新的架構和平臺支持變得非常簡單。
最前面提到的 NativeAOT-LLVM 項目就是例子之一。
NativeAOT-LLVM 在 Rationalization 之后把 RyuJIT IR 轉換為 LLVM IR 后去調用 LLVM 的編譯器,從而使得代碼能夠被 LLVM 優化;然后按照 Lowering 和 Code Generation 的接口分別實現了 WebAssembly 平臺的實現。不僅同時享受到了來自 RyuJIT 和 LLVM 兩邊的優化,同時還為 RyuJIT 擴展出了 WebAssembly 這一新架構的支持。
結尾
這一篇文章就暫時就先寫到這里。
JIT 是一個很復雜的項目,還涉及到和 runtime 的各種交互。在之后文章里,我將會首先帶著大家了解 RyuJIT 和 runtime 是如何進行交互、如何請求類型系統的,然后講講上手 RyuJIT 開發的工具鏈和流程,再然后帶著例子講講一些主要的編譯階段,最后再談一談一些主要的優化內容。

浙公網安備 33010602011771號