跳轉到

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\) 條指令的程式,直譯成本為

\[ T_{\text{interp}} \;=\; n \cdot (t_{\text{fetch}} + t_{\text{decode}} + t_{\text{dispatch}} + t_{\text{exec}}) \]

其中 dispatch 開銷(計算跳轉目標、更新 pc、指令快取失效)往往佔一半以上。JIT 一旦把熱點編譯成機器碼,該段程式只剩

\[ T_{\text{native}} \;=\; n \cdot t_{\text{exec}} + C_{\text{compile}} / \text{(重複執行次數)} \]

故重複執行次數 \(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)」,下次執行時先比對型別:

\[ \text{呼叫點} \;\to\; \underbrace{(T_{\text{cached}} \,\overset{?}{=}\, T_{\text{recv}})}_{\text{一次比對 } O(1)} \;\to\; \text{直接跳到快取的方法} \]
  • 單型內聯快取(monomorphic inline cache):只有一個型別時,比對一次即可。
  • 多型內聯快取(polymorphic inline cache, PIC):多個型別時展開為查找表,仍是 \(O(1)\) 有界成本(Self 論文建議快取前 \(k\) 個型別,通常 \(k=4\) )。

HotSpot 的熱點偵測與 method inlining#

HotSpot(1997 釋出,1999 正式產品化)的架構:

  1. 熱點偵測:每個方法有 invocation counter 與 backedge counter(迴圈回邊),超過門檻(如 10000 次)即觸發編譯:
\[ \text{count}(m) + \text{backedge}(m) > \theta \;\Longrightarrow\; \text{compile}(m) \]
  1. Method inlining:把小方法直接展開進呼叫點,消除呼叫開銷並 exposing 更多最佳化機會。這是 Self 技術的核心——動態語言的呼叫,因為 inline cache 保證型別正確,才能安全地內聯。
  2. 投機最佳化(speculative optimization):基於 profile 假設型別恆定,若日後違反(出現新型別),執行 deoptimization 退回直譯器重新收集資訊。
  3. 精確 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。