3.1 暫存器機架構(Lua VM、Dalvik)#
堆疊機的反思與暫存器機的崛起#
堆疊式虛擬機在 1990 年代仍是主流(JVM、CPython、早期 .NET 草案),但 2000 年代前後,兩個因素改變了設計風向。第一是測量證據:研究者(如 Brian Gregg、後來 David Gregg 團隊的堆疊 vs 暫存器實驗,以及 2005 年 Yunhe Shi 等人的論文《Virtual Machine Showdown: Stack Versus Registers》)系統性地比較兩種架構,結論一致:暫存器位元碼的動態指令數可減少約 30-47%,直譯整體加速常在 20-40%。第二是行動裝置的需求:2005 年 Google 收購 Android 後面臨手機 CPU 慢、電池寶貴的現實,直譯每省一條指令都直接轉換為續航力;Dalvik 因此在 2007-2008 年選擇了暫存器式位元碼(Dan Bornstein 主導),明確背離 JVM 的堆疊設計。
暫存器機的核心思想:位元碼自帶一組「虛擬暫存器」(通常是每個方法框架內的一個陣列),指令直接以暫存器編號定址運算元,省去 push/pop。缺點是指令變長(需編碼暫存器編號),位元碼體積變大——但這個代價在記憶體不再稀缺的年代可以接受,而且可以用常見指令的特化編碼(如 Dalvik 的 8-bit/16-bit 暫存器欄位混合編碼)來緩解。
Lua VM:小而美的暫存器設計#
Lua(Roberto Ierusalimschy、Luiz Henrique de Figueiredo、Waldemar Celes,1993 年誕生於巴西 PUC-Rio,最初動機是讓巴西當時難以取得外國軟體授權的工程環境擁有自己的嵌入語言)的虛擬機是暫存器式設計的教科書範例。Lua 5.0(2003)改用暫存器機的論文《The Implementation of Lua 5.0》(2005)成為 VM 實作的經典文獻。其設計要點:
- 虛擬暫存器就是堆疊槽:每個函式呼叫時,其活動記錄(CallInfo)疊在資料堆疊上,函式的區域變數就是活動記錄中的連續槽位。暫存器 0..n-1 是區域變數,暫存器 n 以上是表達式求值的臨時空間。這個「暫存器窗口疊在堆疊上」的設計,兼得堆疊機的呼叫慣例簡潔與暫存器機的運算效率。
- 指令定長 32 位元,含 opcode 與最多三個運算元欄位(A、B、C),格式有三種(iABC、iABx、iAsBx)。定長指令使解碼只需位元遮罩,無需變長掃描。
- LuaJIT 的登峰造極:Mike Pall 的 LuaJIT(2005 起)以 Lua 位元碼為基礎實作了 tracing JIT(見 4 章),證明一個人也能寫出超越許多工業 VM 的引擎,成為 VM 工程的傳奇。
Dalvik 位元碼一瞥#
Dalvik 位元碼(.dex 格式)同樣以 16 位元為編碼單位、指令多為 16/32/48 位元寬。以 int add(int a, int b) { return a + b; } 比較:
JVM(堆疊式,3 條指令) Dalvik(暫存器式,1 條指令)
iload_1 add-int v0, v1, v2
iload_2 return v0
iadd
ireturn
暫存器機把「載入兩個運算元、相加」合併成一條指令,直譯迴圈的迭代次數直接除以三。函式呼叫亦然:invoke-virtual {v1, v2, v3}, LFoo;.bar:(III)V 以暫存器串列傳參,不需要 JVM 那種「先把參數依序 push、再 invoke」的多指令序列。
指令選擇與「虛擬暫存器配置」#
暫存器位元碼的產生需要解決一個編譯器問題:把無限虛擬暫存器映對到位元碼的暫存器空間。由於虛擬機的暫存器數量上限寬鬆(Dalvik 一個方法可用到數萬個虛擬暫存器,Lua 受限於函式參數 255 但內部以堆疊延伸),編譯器通常不需要做真正的圖著色暫存器配置,只需做簡單的線性編號:對每個區域變數與中間值依出現順序分配新編號即可。這是「虛擬暫存器 = SSA 值」的樸素對應——真正的暫存器壓力問題被推遲到 JIT 把位元碼翻譯成實際 CPU 碼的階段(那裡才需要圖著色或線性掃描配置)。這種分層設計再次印證了虛擬機的哲學:位元碼 ISA 追求語義表達力與驗證便利,物理效率交給執行引擎的分層優化。
與編譯器、作業系統的關聯#
暫存器機的興起也改變了語言編譯器的形狀:以 Kotlin/Android 為例,源碼先編譯成 JVM 位元碼(.class),再由 d8/R8 工具「堆疊→暫存器」轉換成 .dex——這個轉換器本身就是一個小型最佳化編譯器(包含死碼刪除、指令合併)。而直譯器在宿主機上執行時,虛擬暫存器陣列通常是 C 陣列,編譯器會把它們放进宿主 CPU 的實體暫存器——虛擬機設計者與宿主編譯器之間存在一層微妙的合作:直譯迴圈寫得越「直觀」(以區域變數持有 pc、以陣列索引存取暫存器),宿主編譯器的暫存器配置就越能把熱門狀態留在實體暫存器。這是 3.2 節討論直譯迴圈技術的伏筆。
小結#
暫存器式虛擬機以略大的位元碼換取顯著的直譯效率,Lua VM 展示了暫存器窗口疊加於堆疊之上的優雅設計,Dalvik 則證明了此架構在資源受限平台的價值。堆疊與暫存器之爭的裁決並非絕對——JVM 至今仍是堆疊碼(靠 JIT 彌補),V8 則長期使用暫存器式位元碼(Ignition,2016 年起)——但業界共識已明確:若直譯是主要執行模式,選暫存器;若位元碼只作為 JIT 的輸入,堆疊與暫存器的差異就不再關鍵。