1997 — Self 與 HotSpot:JIT 編譯的成熟#
案件摘要#
1990 年代中期,純直譯的 JVM 慢得讓人絕望(約慢 \(O(10)\) 倍)。破案關鍵藏在兩條線索裡:1984 年 Deutsch–Schiffman 的 Smalltalk-80 論文,以及 1989–1995 年 Sun 內部神祕的 Self 語言計畫。1997 年,Self 團隊的技術被移植回 JVM,成為 HotSpot——JIT 編譯從此成熟,動態語言第一次跑得接近原生速度。
前因 -- 為什麼會有這個案子#
- 直譯器太慢:1995 年的 JVM 直譯器每條 bytecode 都要做 dispatch(取出 opcode、跳轉),開銷巨大,典型慢 \(10\times\) 以上。
- 動態語言的兩難:Smalltalk、Self、Lisp 這類語言的型別在執行期才確定,AOT 編譯(先編譯成機器碼)無法預知型別,難以產生高效碼。
- Deutsch–Schiffman(1984):PS 論文《Efficient Implementation of the Smalltalk-80 System》證明:執行期快取型別推論的結果(inline caching),可讓動態語言接近靜態語言速度。
- Self(1989–1995):Chamber & Ungar 在 Sun 實驗室的計畫,用 prototype-based 設計與激進的動態最佳化(type feedback、method inlining),達到 C 一半以上的速度——證明 JIT 路線可行。Self 計畫 1995 年結束,團隊帶著技術轉戰 Java HotSpot。
線索與推理 -- 數學式、程式、理論#
三種執行模型對照#
| 模型 | 何時產生機器碼 | 型別資訊 | 典型相對速度 |
|---|---|---|---|
| 直譯器(interpreter) | 不產生,逐條執行 | 執行期每次重新判斷 | \(1\times\) (基準,慢) |
| JIT(just-in-time) | 執行期、按熱點編譯 | 執行期觀察 + 投機假設 | \(5\text{–}10\times\) 以上 |
| AOT(ahead-of-time) | 執行前全部編譯 | 編譯期靜態已知 | 接近原生 \(1\text{–}2\times\) |
直譯器的每步開銷模型:對 \(n\) 條指令的程式,直譯成本為
其中 dispatch 開銷(計算跳轉目標、更新 pc、指令快取失效)往往佔一半以上。JIT 一旦把熱點編譯成機器碼,該段程式只剩
故重複執行次數 \(k\) 越大,攤提後成本越低:當 \(k \gg \frac{C_{\text{compile}}}{t_{\text{exec}} \cdot \Delta}\) 時,JIT 獲勝——這就是「熱點」存在的數學理由。
Self 語言:prototype-based 與 inline caching#
Self 沒有 class,物件之間用 prototype 與 delegation(委派)關係相連:訊息找不到時沿 parent 槽往上找:
"Self 語法:物件字面量與委派"
(| parent* = traitsPoint. x = 3. y = 4 |)
查詢成本是鏈長 \(d\) : \(O(d)\) 。inline caching 的解法:在呼叫點直接快取「上次命中的方法位址」與「接收者的型別(map)」,下次執行時先比對型別:
- 單型內聯快取(monomorphic inline cache):只有一個型別時,比對一次即可。
- 多型內聯快取(polymorphic inline cache, PIC):多個型別時展開為查找表,仍是 \(O(1)\) 有界成本(Self 論文建議快取前 \(k\) 個型別,通常 \(k=4\) )。
HotSpot 的熱點偵測與 method inlining#
HotSpot(1997 釋出,1999 正式產品化)的架構:
- 熱點偵測:每個方法有 invocation counter 與 backedge counter(迴圈回邊),超過門檻(如 10000 次)即觸發編譯:
- Method inlining:把小方法直接展開進呼叫點,消除呼叫開銷並 exposing 更多最佳化機會。這是 Self 技術的核心——動態語言的呼叫,因為 inline cache 保證型別正確,才能安全地內聯。
- 投機最佳化(speculative optimization):基於 profile 假設型別恆定,若日後違反(出現新型別),執行 deoptimization 退回直譯器重新收集資訊。
- 精確 GC:編譯碼保有完整的指標 map,支援準確的垃圾回收。
Python 展示 inline caching 概念#
以下模擬 monomorphic inline cache 的效果:
class Point:
__slots__ = ('x', 'y')
def __init__(self, x, y): self.x, self.y = x, y
def dist(self): return (self.x**2 + self.y**2) ** 0.5
# 無快取:每次呼叫都重新查找 dist 屬性(dict/slot lookup)
def sum_dist_nocache(points):
s = 0
for p in points: s += p.dist() # 每次都是動態查找 + 動態呼叫
return s
# 模擬 inline cache:第一次查找後快取「綁定方法」,之後直接使用
def sum_dist_cached(points):
cache = {} # type -> bound method(模擬快取槽)
s = 0
for p in points:
t = type(p)
m = cache.get(t)
if m is None or m.__self__ is not p: # 型別比對(快取命中檢查)
m = cache[t] = p.dist # miss:查找並填入快取
s += m() # hit:直接呼叫,免查找
return s
pts = [Point(i, i) for i in range(1000)]
assert sum_dist_nocache(pts) == sum_dist_cached(pts)
真實的 JIT(如 PyPy、V8)更进一步:把 p.dist() 內聯為 (self.x**2 + self.y**2)**0.5 的機器碼,並用 guard(self 是 Point 的 instance?)保護假設——這正是 Deutsch–Schiffman 1984 年構想的現代化身。
結案 -- 後果與影響#
- 1997 年 HotSpot 使 Java 效能追上 C++ 的大半,Java 得以進軍高效能伺服器市場。
- JIT 技術擴散到整個產業:.NET CLR(2002)、V8(2008,讓 JavaScript 稱霸瀏覽器與 Node.js)、PyPy、LuaJIT。
- Self 雖死,但它的遺產三重降臨:prototype 概念進入 JavaScript、inline caching 進入所有 JIT、動態最佳化進入 HotSpot。
- 現代理論後續:trace-based compilation(LuaJIT)、deoptimization 的形式語義、以及 GraalVM 的多語言 JIT(Truffle)。
關鍵人物與文獻#
- Lars Bak:Self 團隊成員,後來領導 V8。
- Craig Chambers & David Ungar:Self 的設計者與 JIT 先驅。
- L. Peter Deutsch & Allan Schiffman:1984 年 PS 論文作者,inline caching 發明人。
- Deutsch & Schiffman, Efficient Implementation of the Smalltalk-80 System, POPL 1984。
- Chambers & Ungar, An Efficient Implementation of SELF, a Dynamically-Typed Object-Oriented Language Based on Prototypes, OOPSLA 1989。
- Paleczny, Vick & Click, The Java HotSpot Server Compiler, JVM 2001。