JavaScriptCore:从解析器到优化 JIT 流水线
前置知识
- ›第 2 篇:WTF 与内存管理(GC 桥接概念)
- ›编译器基础知识:词法分析、语法分析、AST、中间表示
- ›对 JIT 编译的基本理解,以及分级编译存在的原因
- ›熟悉 SSA 形式与控制流图(有帮助,但非必须)
JavaScriptCore:从解析器到优化 JIT 流水线
JavaScriptCore(JSC)是 WebKit 的 JavaScript 与 WebAssembly 引擎。它是一套极为精密的编译器基础设施——能够将天生动态、弱类型的 JavaScript,通过四级编译流水线转化为优化后的机器码,在紧密循环中的性能可与静态类型语言相媲美。
JSC 设计背后的核心洞察在于:编译速度与执行速度是一对相互竞争的目标。优化编译器能生成更快的代码,但编译本身耗时更长。JSC 通过分级编译来化解这一矛盾:先用快速解释器立即开始执行,同时收集运行时行为的 profile 数据,再对更热的代码逐步采用更激进的优化策略进行编译。
本文将完整追踪从源代码到机器码的整条编译流水线。
词法分析与语法分析:从源码到 AST
第一步是将 JavaScript 源文本转化为结构化的表示形式,分为两个阶段:词法分析(tokenization)和语法分析(AST 构建)。
flowchart LR
SRC["JavaScript Source<br/>'function add(a,b) { return a+b; }'"] --> LEX["Lexer<T>"]
LEX --> TOKENS["Token Stream<br/>FUNCTION, IDENT, LPAREN, ..."]
TOKENS --> PARSE["Parser<LexerType>"]
PARSE --> AST["AST<br/>(Nodes.h types)"]
Lexer<T> 是一个以字符类型为模板参数的类(LChar 对应 8 位,UChar 对应 16 位)。对于纯 ASCII 源文件,这样做可以避免始终使用 16 位字符所带来的性能开销——这是 WebKit 中常见的优化手段。类上的 CACHE_LINE_ALIGNED 注解确保词法分析器的热字段不会与其他数据共享缓存行。
Parser<LexerType> 是一个递归下降解析器,负责从 token 流构建 AST。AST 节点类型定义在 Nodes.h 中——这是一个庞大的文件,包含了 JavaScript 所有语法构造(表达式、语句、声明、模式等)的类型层次结构。
JavaScript 的语法出了名地复杂。解析器必须处理自动分号插入、箭头函数的语义歧义((a) => a 与 (a) 后接 => a 的区别)、模板字面量的嵌套,以及其他许多上下文敏感的语法结构。JSC 的解析器在单遍扫描中完成了所有这些处理。
提示: 独立的 JSC shell
jsc.cpp是探索 JSC 内部机制最便捷的方式。你可以通过命令行参数来输出字节码、打印 JIT 编译决策,以及追踪层级提升事件。
字节码编译与 CodeBlock
AST 不会被直接执行。字节码编译器会将其转换为字节码,并存储在 CodeBlock 中。
flowchart TD
AST["AST Nodes"] --> BC["BytecodeGenerator"]
BC --> CB["CodeBlock"]
CB --> |"contains"| INST["Bytecode instructions"]
CB --> |"contains"| PROF["Profiling metadata"]
CB --> |"contains"| CONST["Constant pool"]
CB --> |"will contain"| JIT["JIT code (later)"]
CodeBlock 是 JSC 中核心的编译产物,其中存储了以下内容:
- 字节码指令 — 程序操作的紧凑编码形式。
- 常量池 — 字节码引用的字符串字面量、数字及其他常量。
- Profiling 元数据 — 解释执行期间收集的计数器和类型信息,后续用于 JIT 层级提升的决策依据。
- JIT 代码 — 函数经过 JIT 编译后,生成的机器码会附加到同一个
CodeBlock上。
字节码指令集相当丰富,不仅涵盖基本操作,还包含针对常见模式的专用快速路径(例如,用于属性访问的 op_get_by_id,用于函数调用的 op_call)。每条指令都包含元数据槽,供解释器和基线 JIT 存储类型 profile 信息。
LLInt:底层解释器
第一个执行层级是 LLInt(Low-Level Interpreter,底层解释器)。函数被首次调用时,其字节码由 LLInt 负责执行。
LLInt 的"慢路径"逻辑位于 LLIntSlowPaths.cpp,负责处理那些无法内联到解释器主调度循环中的复杂操作,包括缓存未命中的属性查找、对未知目标的函数调用,以及最关键的——层级提升检查。
层级提升机制通过统计每个函数和循环的执行次数来工作。当执行计数超过阈值时,LLInt 会触发对该函数在下一层级(基线 JIT)的编译。整个过程对上层透明——函数继续在解释器中运行,而基线 JIT 则在后台并发编译。
flowchart TD
START["Function call"] --> LLINT["LLInt Interpreter"]
LLINT --> |"execution count > threshold"| BL_COMP["Baseline JIT compilation"]
BL_COMP --> BL["Baseline JIT code"]
BL --> |"+ profiling data"| DFG_COMP["DFG JIT compilation"]
DFG_COMP --> DFG["DFG optimized code"]
DFG --> |"+ more profiling"| FTL_COMP["FTL compilation (via B3)"]
FTL_COMP --> FTL["FTL maximum optimization"]
DFG --> |"speculation fails"| OSR_EXIT["OSR Exit"]
FTL --> |"speculation fails"| OSR_EXIT
OSR_EXIT --> |"deoptimize"| BL
分级 JIT 编译:Baseline → DFG → FTL
JSC 在解释器之上设有三个 JIT 层级,每一层都在编译时间与执行速度之间做出不同的权衡:
Baseline JIT — 第一个 JIT 层级。它以最少的优化将字节码编译为机器码,承担双重职责:既要比解释器跑得更快,又要收集类型 profile 数据。基线 JIT 会对属性访问、函数调用和算术运算进行插桩,记录流经每个操作的类型信息。
DFG(Data Flow Graph)JIT — 第一个优化层级。DFG 构建基于图的中间表示,并利用基线 JIT 收集的 profile 数据进行类型推断。DFGAbstractInterpreter 在 DFG 图上执行抽象解释,传播类型信息以支撑投机优化。
"投机"是这里的关键词。DFG 假设未来的执行将看到与过去相同的类型。如果 a + b 始终以整数参与运算,DFG 就会将其编译为带有守卫(类型检查)的整数加法。若守卫在运行时失败(比如传入了字符串),JIT 会执行 OSR 退出(On-Stack Replacement),回退到基线 JIT。
FTL(Faster Than Light)JIT — 最高优化层级。FTL 将 DFG 图下降到 B3 中间表示(详见下文),并应用激进的经典编译器优化:循环不变代码外提、全局值编号、常量折叠、死代码消除以及寄存器分配。
FTL 编译的入口是 FTLCompile.cpp,它负责编排整个 B3 编译流水线并安装最终生成的机器码。
B3 后端与 Air
B3 是 JSC 的编译器后端——一种专为高效生成机器码而设计的低层 IR,同时服务于 FTL JIT(JavaScript)和 OMG 层级(WebAssembly)。
flowchart TD
DFG_IR["DFG IR (high-level)"] --> LOWER["FTL Lowering"]
LOWER --> B3_IR["B3 IR (SSA form)"]
B3_IR --> OPT["B3 Optimizations<br/>(strength reduction, CSE, DCE)"]
OPT --> AIR["Air (Assembly IR)"]
AIR --> RA["Register Allocation"]
RA --> MC["Machine Code"]
WASM["Wasm bytecode"] --> OMG["OMG tier"]
OMG --> B3_IR
B3::BasicBlock 是 B3 IR 的基本单元——由一系列 Value(操作)组成的序列,通过前驱与后继列表构成控制流图。B3 使用 SSA 形式,每个值只被定义一次,使得数据流分析变得直接清晰。
Air(Assembly IR)位于 B3 与最终机器码之间。它使用机器特定的指令来表示程序,但仍使用虚拟寄存器。寄存器分配器负责分配物理寄存器并输出最终的机器码。
这一设计在理念上与 LLVM 相近(B3 的灵感来源于 LLVM IR),但更为轻量,编译速度也更快。JSC 曾将 LLVM 作为其 FTL 后端,后来切换到 B3,主要是为了获得更短的编译时间和更紧密的集成。
垃圾回收:Riptide 收集器
JSC 使用名为 Riptide 的垃圾回收器来管理 JavaScript 对象的生命周期——这是一个退缩波前(retreating-wavefront)并发 GC。
"退缩波前"意味着收集器无需暂停所有 JavaScript 执行即可完成一次收集周期。它在 JavaScript 持续运行的同时并发追踪存活对象,并借助写屏障来追踪追踪过程中发生的内存变更。
JSObject 是 GC 堆中所有 JavaScript 对象的基类,继承自 JSCell——后者提供了 GC 基础设施(标记位、结构指针、类型信息)。
GC 必须通过绑定层与 WebCore 的引用计数对象进行协调,这一点在第 3 篇中已有讨论。当一个 JSDocument 包装器(由 GC 管理)持有对某个 Document(引用计数管理)的引用时,GC 必须知道在该 Document 仍可从 C++ 侧访问的情况下,不能回收这个包装器。DOMWrapperWorld 和不透明根(opaque root)机制负责处理这种协调关系。
stateDiagram-v2
[*] --> Idle
Idle --> ConstraintFixpoint : allocation threshold
ConstraintFixpoint --> Concurrent_Mark : begin marking
Concurrent_Mark --> Concurrent_Mark : trace objects (concurrent)
Concurrent_Mark --> Reloop : found new objects
Reloop --> Concurrent_Mark : continue marking
Concurrent_Mark --> Sweep : marking complete
Sweep --> Idle : collection done
WebAssembly:BBQ 与 OMG 层级
JSC 的 WebAssembly 支持同样采用了与 JavaScript 流水线相似的两级编译体系:
- BBQ(Build Bytecode Quickly)— 快速基线编译器,能迅速生成质量可接受的代码,让 WebAssembly 模块以最短的延迟开始执行。
- OMG(Optimized Machine code Generator)— 优化层级,复用与 JavaScript FTL 层级相同的 B3 后端,对热点 WebAssembly 函数应用完整的优化策略。
JavaScript FTL 与 WebAssembly OMG 共享同一个 B3 后端,这是一项重要的工程优势——对 B3 代码生成的任何改进,都能同时惠及这两种语言。
WebAssembly 的实现代码位于 Source/JavaScriptCore/wasm/。由于 WebAssembly 是静态类型的,编译在某些方面更为简单(无需投机优化),但在另一些方面则更为复杂(SIMD 操作、内存边界检查、表间接引用)。
下一步
在本系列的最后一篇文章中,我们将把关注点从架构转向实践:如何构建 WebKit、运行其完整的测试套件,以及如何提交代码贡献。我们将介绍双构建系统(Xcode 与 CMake)、统一源码优化、布局测试基础设施,以及 git-webkit 贡献者工作流。掌握这些实践知识,才能将架构层面的理解真正转化为参与代码库开发的能力。