跳轉到

08.1 直譯器與位元組碼 VM 的瓶頸#

從 Lisp 的直譯迴圈說起#

動態語言的效能問題,幾乎與動態語言本身同齡。1958 年 Lisp 誕生後,最早的實作就是一個 eval 直譯迴圈:讀一個 S-表示式、遞迴求值、回傳結果。這種方式開發體驗極佳——立即回饋、無需編譯等待——但速度比編譯語言慢上一到兩個數量級。1970 年代,Smalltalk 在 Xerox PARC 誕生時採取了折衷方案:把程式編譯成 bytecode(位元組碼),由一個小型虛擬機以 dispatch loop 逐條執行。bytecode 比直接直譯語法樹快得多,也為「write once, run anywhere」的夢想鋪路——Smalltalk-80 的映像檔可以在不同機器間搬移,只要那台機器有 VM。

PARC 的工程師們很早就意識到直譯的極限,並且動手突破。1984 年,L. Peter Deutsch 與 Allan Schiffman 發表了關於 PS(Portable Smalltalk) 的論文,提出 dynamic binary translation:把 bytecode 動態翻譯成目標機器的原生指令,並快取翻譯結果。這篇論文被公認為 JIT 編譯的先驅,其中的 inline caching 技巧更是流傳至今。PARC 證明了一件事:動態語言的效能天花板,不是語言本身的宿命,而是實作技術的問題。

Java:虛擬機的商業化時刻#

1995 年,Sun 推出 Java,其核心設計就是 JVM bytecode:編譯器把 Java 編成位元組碼,任何裝有 JVM 的平台都能執行。「Write once, run anywhere」的口號直擊 1990 年代軟體業的痛點——C/C++ 程式碼得為每個平台重新編譯、移植、除錯。虛擬機是這個商業承諾的必要代價。

但代價很快顯現。早期 JVM 只有簡陋的解釋器,1990 年代後半,Java 的「慢」成了整個業界的笑柄:圖形介面卡頓、伺服器程式吞吐量遠不及 C、開發者一邊享受垃圾回收的便利一邊抱怨延遲。C 編譯器(GCC 系)把程式碼編成原生指令,Java 卻在一個迴圈裡逐位元組解讀——差距是結構性的。

解釋器的結構性瓶頸#

把 Java 的慢拆開來看,可以歸結為幾個直譯執行內建的開銷:

  1. Dispatch 開銷:每執行一條 bytecode,VM 都要經歷「取指、解碼、跳轉」的循環。即使指令本身很簡單,這個循環的成本可能是實際運算的數倍。
  2. 無法型態特化:位元組碼裡的 iadd 面對的是抽象的運算元棧,VM 不知道值是 32 位整數還是物件參照,只能以通用且保守的方式處理。
  3. 缺乏跨指令的優化視野:直譯器一次只看一條指令,常數折疊、死碼消除、暫存器分配這些依賴全域資料流分析的優化全部無從施展。
  4. 方法呼叫與物件存取的固定成本:每次虛擬呼叫、每次欄位查找都要走一遍查表流程,無法內聯或快取。

這些瓶頸不是 Sun 工程師的失職,而是「可攜性」這個商業目標的內在張力:為了在任何機器上跑得動,第一版必須最保守。但市場的耐心有限——企業採用 Java 做後端的前提,是它必須「跑得快」,而不只是「跑得動」。

.NET CLR:同一道題目的微軟版本#

2002 年,微軟推出 .NET Framework 與 CLR(Common Language Runtime),Anders Hejlsberg——Turbo Pascal 與 Delphi 的傳奇設計者——主導設計了 C#。CLR 的架構與 JVM 高度同構:C#(以及後來的 VB.NET、F#)編譯成 CIL 位元組碼,由 CLR 執行。微軟從 Java 的教訓出發,第一天就內建了 JIT 編譯器,深知解釋執行在商業上不可行。同一道題目——「位元組碼帶來的可攜性/彈性,如何不犧牲原生速度」——被兩大陣營同時背負。

鋪墊:把「跑得動」升級成「跑得快」#

回顧這段歷史,脈絡清晰:Smalltalk 證明了 bytecode VM 的可行性(1970s–80s),Deutsch 與 Schiffman 證明了動態翻譯可以追上原生碼(1984),Java 證明了虛擬機的商業價值(1995)也暴露了解釋器的市場極限(1990s),.NET 則把 JIT 列為標準配備(2002)。所有線索都指向同一個結論:虛擬機的未來在 JIT 編譯。

技術、經濟與商業在此交會:伺服器市場的爆發讓「吞吐量」成為真金白銀,企業不會為了可攜性永久支付十倍效能稅。下一章的主角 HotSpot,正是要在「跑得動」與「跑得快」之間,找到那條聰明的路——不必編譯全部程式碼,只編譯值得編譯的部分。