跳转到内容

TraceMonkey — 只编"真的走过的那一条路"

是什么

TraceMonkey 是 Mozilla 2009 年给 Firefox 3.5 做的 JavaScript JIT。它的思路和当时主流 JIT 完全相反:

  • 主流(Java HotSpot):把整个方法编译成原生码
  • TraceMonkey:只录实际跑过的那一条直线指令序列编译成原生码

日常类比:你在大型超市买东西,传统 JIT 像是”先把整张地图所有过道都铺成快车道”;TraceMonkey 是”看你这次走的是哪一条路径,把这条路径铺平就行,其它过道仍然走原路”。

这条被录下来的直线路径叫一条 trace。如果某个 for 循环每次都走相同分支,第一圈跑完就被录成 trace,之后每圈都直接跑原生码。

为什么对动态语言(JavaScript / Lua / Python)特别有效?因为这些语言里同一个变量 x 既可能是整数也可能是字符串,静态看不出来。但运行时实际看一圈就能确认”这个循环里 x 一直是整数”,于是按整数生成机器码。

为什么重要

不理解 trace JIT,下面这些问题就讲不清:

  • 为什么 Firefox 3.5 发布时被吹成”JS 提速 30 倍”,但几年后 Mozilla 自己又把它换掉了
  • 为什么 LuaJIT 2.0 在 2026 年仍是跑得最快的 Lua 实现,靠的就是这套思路
  • 为什么 PyPy 不直接给 Python 写 JIT,而是给”解释器”加 trace JIT,所有用它的语言一起加速
  • 为什么 V8 / 现代 SpiderMonkey 选择回到方法 JIT,trace 路线在 JS 上没赢到底

核心要点

TraceMonkey 把”动态语言慢”的问题拆成两个观察:

  1. 绝大多数时间花在循环里(80/20 法则),所以只优化循环就够了
  2. 循环里的类型其实是稳定的,只是静态推不出来——那就运行时观测一次

基于这两点,整套机制是:

  1. 解释执行 + 计数:每条循环回边(goto 回到循环头)经过几次就累计计数
  2. 触发录制:某个回边热度超过阈值(比如 2 次),开始录这条循环里实际执行的指令序列
  3. 特化编译:录制时把每个变量的具体类型(int32 / double / 对象指针)记下来,按这个类型生成原生码;每个类型假设都插一个 guard(运行时检查)
  4. 执行 trace:下次走到这个循环头,直接跳进原生码跑
  5. side exit:guard 失败(比如 x 突然是字符串),从这条 trace 退出回到解释器
  6. trace tree:同一个循环头如果有多种走法,每种走法各成一条 trace,挂成一棵树

整套关键词:热点循环 + 录制 trace + 类型特化 + guard 守护 + side exit 退出

实践案例

案例 1:trace 录的到底是什么

JavaScript 源码:

function sum(n) {
let s = 0
for (let i = 0; i < n; i++) s += i
return s
}

第一次调用 sum(1000000),解释器跑了几圈后开始录 trace。录到的不是 JS 源码,是这一圈实际执行的指令序列,大致像:

guard(typeof i === 'int32') // 类型守护
guard(typeof s === 'int32')
i_int = unbox(i)
s_int = unbox(s)
s_int = s_int + i_int // 直接整数加法,不再 box
i_int = i_int + 1
guard(i_int < n_int) // 循环条件守护
goto loop_head

每个 guard 不通过就 side exit 回解释器。如果 100 万圈都通过,整个循环就只跑原生整数加法,比解释器快 10-100 倍。

案例 2:guard 失败时发生什么

function process(arr) {
let total = 0
for (let x of arr) total += x // 录成 trace 时假设 x 是 int32
return total
}
process([1, 2, 3, "hello"]) // 第 4 个元素是字符串

走到 "hello" 时,guard(typeof x === 'int32') 失败,side exit

  1. 把寄存器里的 total_int 重新 box 回 JS 值
  2. 跳回解释器栈帧,把 PC 设到对应字节码
  3. 解释器接着跑,处理字符串相加(变成 "6hello"

side exit 不是免费的——它要还原寄存器、重建栈帧。如果一条 trace 频繁 side exit,TraceMonkey 会把它加入黑名单(blacklist)不再录制,因为录了反而更慢。

案例 3:trace tree 怎么长

for (let i = 0; i < n; i++) {
if (i % 2 === 0) doA(i)
else doB(i)
}

第一条 trace 录到偶数分支 doA,第二圈 i=1 走 else,guard 失败 side exit。如果这条侧路也热,TraceMonkey 把”从 side exit 点开始的 else 分支”录成第二条 trace,挂在第一条 trace 的 side exit 下面。整体形成一棵 trace tree,从循环头出发,按分支不同走不同子路径。

但分支太多 trace tree 会爆炸——这是后来被替代的重要原因。

踩过的坑

  1. 把 trace 当 method 的子集来想:错。method 是程序静态结构,trace 是程序动态执行。同一个 method 可能产生很多条 trace,反过来一条 trace 也可能跨多个 method(inline)。

  2. 以为 type specialization == 静态类型推导:完全不同。HM/TypeScript 是看代码推;trace JIT 是先跑一遍看实际值再特化。前者编译期,后者运行时。

  3. 以为 guard 和 if 一样便宜:错。if 是普通分支,CPU 分支预测器友好;guard 失败要做完整状态还原(解释器栈帧重建),代价 100+ 周期。设计 trace 时核心目标就是让 guard 极少失败

  4. 以为 trace JIT 全面优于 method JIT:现代 V8 / SpiderMonkey 都用 method JIT + 类型反馈(type inference)。trace 在分支密集的真实业务代码上不稳,TraceMonkey 在 Firefox 4+ 被 IonMonkey 取代。

适用 vs 不适用场景

适用

  • 数值循环密集的动态语言:游戏脚本、科学计算、DSL 解释器
  • Lua(LuaJIT 至今最快 Lua)、嵌入式 JS、配置脚本
  • 需要”用解释器写、自动得 JIT” 的场景(PyPy meta-tracing)

不适用

  • 分支密集的业务逻辑代码(前端框架、ORM 层)— trace tree 爆炸
  • 已经静态类型的语言(Java / C#)— 没有 unbox 红利,方法 JIT 更稳
  • 深层多态调用(OOP 重的代码)— 每个 receiver type 一条 trace,难合并

历史小故事(可跳过)

  • 1996 年:HP 实验室的 Dynamo 项目证明可以在二进制层面录 trace 做优化
  • 2006 年:Andreas Gal 博士论文 HotpathVM 把 trace 思路搬到嵌入式 Java
  • 2009 年:Gal 加入 Mozilla,把 HotpathVM 的思想做成 TraceMonkey,集成到 Firefox 3.5(PLDI 2009 论文)
  • 2010 年:LuaJIT 2.0(Mike Pall)把 trace JIT 推到极致,性能逼近 C
  • 2011 年起:Mozilla 用 IonMonkey 逐步替换 TraceMonkey;2013 年 trace 路径完全下线
  • 2014 年起:PyPy 把”meta-tracing”(不录用户程序,录解释器)发展成完整体系

学到什么

  1. 静态看不出来,那就动态看一眼——这是 trace JIT 最朴素也最深刻的洞见
  2. 优化要顺着 80/20:循环占 80% 时间,那就只优化循环;guard 必须便宜,不便宜就别守
  3. 没有银弹:trace 在数值循环上赢,在分支业务代码上输;技术选型要看实际负载形状
  4. 失败的方案也是历史:TraceMonkey 在 JS 上输了,但思想被 LuaJIT / PyPy 接住,反而比赢家活得久

延伸阅读

关联

  • hotspot-server-compiler —— 方法级 JIT,对照组
  • pypy-tracing-jit —— 直接接班的 meta-tracing 体系
  • graalvm-truffle —— partial evaluation 路线,思路相邻但起点不同
  • self-pic —— 早于 TraceMonkey 的动态类型加速方案(内联缓存)
  • strongtalk —— 早期给 Smalltalk 做类型反馈的尝试
  • hindley-milner —— 静态类型推导,与 trace JIT 的”动态观测”形成对照

反向链接

  • branch-prediction-yeh-patt-1991 —— Yeh-Patt 1991 — 用最近 12 条分支的历史给 CPU 算命
  • graalvm-truffle —— GraalVM Truffle — 写一棵会自我特化的语法树就能自动得到 JIT
  • hindley-milner —— Hindley-Milner — 编译器自己猜变量类型
  • hotspot-server-compiler —— HotSpot Server Compiler — JVM 在运行时把热点 Java 代码翻译成飞快的本地码
  • pypy-tracing-jit —— PyPy meta-tracing JIT — 给解释器加一次 JIT,所有用它的语言一起加速
  • quickjs —— QuickJS — 装进口袋的 JavaScript 引擎
  • self-pic —— Self / PIC — 内联缓存的诞生
  • strongtalk —— Strongtalk — 可以装可以卸的 Smalltalk 类型系统
  • tomasulo-1967 —— Tomasulo 算法 — 让 CPU 自己决定指令的执行顺序