跳到主要内容
浏览器
导航
内核与技术专题

分层编译与 JIT 的执行流程

梳理脚本引擎从源码到执行的层次化路径:字节码与解释器、内联缓存与类型反馈、基线编译器、优化编译器与去优化,以及编译层次切换的判定条件与垃圾回收的配合方式。

发布于 5 分钟阅读 1700 字

一个 JavaScript 函数在被反复调用时,其执行方式会随调用次数发生变化:起初由解释器逐条执行字节码,随后可能被编译为机器码,热点路径再被进一步优化。这套「先解释、再基线编译、后优化编译」的层次化策略,是当前多数脚本引擎采用的执行方式,V8 中的三个层次依次对应 Ignition、Sparkplug 与 TurboFan。

分层的目的在于平衡启动开销与长期运行的效率。编译越激进,生成的机器码质量通常越高,但编译本身消耗的时间与内存也越多。对只执行一次的代码,编译成本难以被回收;对长期运行的代码,优化编译的收益则可以逐步累积。

从源码到字节码

源码先被解析为抽象语法树,再由字节码生成器转换为字节码。字节码是一种面向栈式虚拟机的中间表示,指令粒度较小,一条源码语句通常对应若干条指令。

function add(a, b) {
  return a + b;
}

对上面的函数,解释器会生成形如「加载参数 a、加载参数 b、执行加法、返回」的指令序列。字节码的执行由解释器的分派循环驱动:取指、分派、执行,然后进入下一条。因为不涉及机器码生成,这一步的启动成本较低,函数在解析完成后即可投入运行。

字节码同时承担另外两个职责:作为优化编译的输入,以及用于记录执行过程中的类型信息。

内联缓存与类型反馈

JavaScript 是动态类型语言,a + b 在编译期无法确定操作数的类型。引擎在字节码执行时收集类型信息,并在属性访问处放置内联缓存(inline cache,IC)。

内联缓存记录该访问点此前见过的对象形状。对象的属性布局由引擎映射为一种内部描述(V8 中称为隐藏类,其它实现中称为形状或结构),具有相同属性集合与相同定义顺序的对象共享同一份描述。当访问点遇到的对象形状单一时,缓存命中,属性偏移可以直接取出;当出现多种形状时,缓存退化为多态乃至超态,需要通过额外的查找路径定位属性。

function getX(p) {
  return p.x;
}

getX({ x: 1, y: 2 });   // 记录形状 A
getX({ x: 3, y: 4 });   // 仍为形状 A,命中
getX({ y: 5, x: 6 });   // 形状 B,访问点转为多态

收集到的类型反馈会随字节码一起保存,成为后续优化编译判断类型假设的依据。

基线编译器

基线编译器把字节码逐条翻译为机器码,不做跨指令的分析与优化。它的作用是去掉解释器的分派开销:执行同一段逻辑时,直接执行机器码可以省去反复取指与分派的过程。

因为翻译过程基本是一对一的,基线编译的耗时与字节码长度大致成正比,适合在函数被调用若干次之后触发。生成的代码仍保留对类型反馈的依赖,也保留了随时回退到字节码的能力。

优化编译器

当函数进一步变热,优化编译器会基于类型反馈做更激进的处理:内联被调用的函数、把属性访问固化为固定偏移、把动态运算特化为针对特定类型的机器指令、消除冗余检查。

优化编译依赖「此处的类型在将来不会改变」这类假设。假设一旦被打破,优化后的代码不再正确,引擎必须丢弃它并回退到字节码或基线代码,这个过程称为去优化。

常见的去优化触发条件包括:

  • 某个访问点遇到的对象形状超出优化时假设的范围
  • 传入参数的类型与反馈记录不符
  • 被内联函数的调用目标发生变化
  • delete 等操作改变了对象的属性布局
层次输入主要产出典型触发时机
解释执行字节码直接求值结果初次调用
基线编译字节码逐条翻译的机器码调用次数达到较小阈值
优化编译字节码与类型反馈特化后的机器码函数成为热点
去优化运行时的类型不符回退到低层代码优化假设被打破

编译层次的切换判定

层次之间的切换由计数器与启发式规则驱动。函数入口处的调用计数达到阈值时进入上一层;循环体内部另有回边计数,用于让长时间运行的循环提前触发优化,而不必等待外层函数被反复调用。

阈值本身会被引擎动态调整:当编译线程繁忙或内存紧张时,提高阈值可以减缓编译压力;当某个函数频繁去优化时,引擎会降低它再次被优化的意愿,避免陷入「优化—去优化」的往复循环。

内存回收的配合

编译产物、类型反馈与对象本身都占用内存,需要回收机制配合。多数实现采用分代假设:新分配的对象中大部分很快不再使用,因此对新生代采用复制式回收,对存活时间较长的对象所在的老生代采用标记清除或标记整理。

为了减少主线程停顿,标记阶段通常采用增量或并发方式进行,把标记工作拆成若干小段,与脚本执行交错推进。回收过程中若移动了对象,引擎需要修正指向这些对象的引用,包括优化代码中内联保存的地址。因此回收与编译之间存在协同:代码中保存对象引用时,需要让回收器能够识别并更新它们。

小结

分层编译把「快速启动」与「长期运行效率」拆给不同的执行层次:解释器负责起步,基线编译器去掉分派开销,优化编译器针对类型反馈做特化,去优化则确保假设失效时结果仍然正确。三者的触发条件由计数器与启发式规则决定,而内存回收在后台为整套机制提供空间与引用维护。

内核与技术专题 · 相关文章