深入 JavaScriptCore:四层执行流水线
前置知识
- ›第 1 篇:WebKit 导览——架构概览与代码库地图
- ›第 2 篇:WTF 与内存管理——RefPtr、WeakPtr 及容器类型
- ›编译器基础理论:AST、中间表示(IR)、SSA 形式
- ›熟悉 JIT 编译的基本概念与推测式优化
深入 JavaScriptCore:四层执行流水线
JavaScriptCore(JSC)是一个完整的 JavaScript 引擎,负责解析源代码、编译为字节码,再通过逐层递进、愈加激进的优化策略对热点函数反复重编译。这套多层架构正是 JSC 在启动延迟(用户希望页面快速加载)与峰值吞吐量(Web 应用希望计算高效运行)之间取得平衡的关键。
正如第 1 篇所介绍的,JSC 位于 WebKit 构建栈的第 3 层,依赖 WTF 提供容器和智能指针(第 2 篇),但与 WebCore 没有任何依赖关系。你可以将 JSC 作为独立 shell 构建和运行——jsc.cpp 文件就提供了这样的入口点,负责初始化 VM 并从命令行执行脚本。
词法分析与解析:从源代码到 AST
JavaScript 的执行始于文本。Lexer<T> 类将原始源字符转化为 token 流。它以字符类型为模板参数(LChar 对应 Latin-1,UChar 对应 UTF-16),为纯 ASCII 源代码提供了专门的优化路径——而实践中绝大多数 JavaScript 代码都属于这种情况。
Parser<T> 是一个递归下降解析器,消费 token 流并生成 AST,AST 节点类型定义在 Nodes.h 中。节点层次体系相当丰富,涵盖表达式节点、语句节点、声明节点等众多类型。
flowchart LR
Source["JavaScript Source"] --> Lexer["Lexer<br/>Tokenization"]
Lexer --> Tokens["Token Stream"]
Tokens --> Parser["Parser<br/>Recursive Descent"]
Parser --> AST["AST<br/>(Nodes.h types)"]
AST --> BytecodeGen["BytecodeGenerator"]
BytecodeGen --> CodeBlock["CodeBlock<br/>(bytecode + metadata)"]
解析器有一个值得关注的设计决策:它采用了"TreeBuilder"抽象层。这意味着解析器并不总是构建完整的 AST,而是可以使用 SyntaxChecker 树构建器——仅做语法校验,不分配任何节点——来扫描那些可能永远不会被调用的函数体。
提示: JSC 对函数体采用懒解析策略。脚本首次加载时,只有顶层代码会被完整解析,内部函数只做语法检查,AST 只在函数首次被调用时才会真正构建。对于包含大量未被调用函数的大型脚本,这一机制能显著减少解析耗时。
字节码生成与字节码格式
BytecodeGenerator 遍历 AST 并生成基于寄存器的字节码。与基于栈的虚拟机(如 JVM)不同,JSC 的字节码操作的是虚拟寄存器,这在 JIT 编译时更容易映射到物理 CPU 寄存器。
字节码指令定义在 bytecode/ 目录中,每条指令都是一个紧凑的操作,例如 get_by_id(属性访问)、call(函数调用)或 add(算术运算)。生成的字节码连同常量、异常处理器和类型分析元数据,统一存储在 CodeBlock 中——这是 JSC 中编译代码的基本单元。
| 概念 | 位置 | 用途 |
|---|---|---|
| 字节码指令 | Source/JavaScriptCore/bytecode/ |
指令定义与编码 |
| BytecodeGenerator | Source/JavaScriptCore/bytecompiler/ |
AST → 字节码降级 |
| CodeBlock | Source/JavaScriptCore/bytecode/CodeBlock.h |
函数字节码与元数据的容器 |
| 值分析 | Source/JavaScriptCore/bytecode/ |
执行期间收集的类型反馈 |
每个 CodeBlock 最初在解释器层执行,随着函数逐渐"变热",会被提升到更高的优化层。
第 1 层 —— LLInt:低级解释器
低级解释器(LLInt)是第一个执行层。它直接执行字节码指令,不生成任何本机代码。入口点是 LLInt::setEntrypoint,负责将 CodeBlock 指向解释器的派发表。
LLInt 使用一种自定义的宏汇编 DSL(定义在 llint/ 中的 .asm 文件)编写,再由"离线汇编器"将其处理为各目标平台的 C++ 或本机代码。这一设计让 LLInt 在保持跨架构可移植性的同时,性能也优于朴素的 C++ switch 派发解释器。
flowchart TD
CB["CodeBlock<br/>(bytecode)"] --> LLInt["LLInt<br/>Interpreter"]
LLInt --> Exec["Execute bytecodes<br/>one at a time"]
Exec --> Profile["Collect type profiles:<br/>• Value types seen<br/>• Branch targets taken<br/>• Call targets"]
Profile --> Hot{"Function hot<br/>enough?"}
Hot -->|No| Exec
Hot -->|Yes| Baseline["Promote to<br/>Baseline JIT"]
LLInt 在执行过程中会收集值分析数据,记录每个操作中流经的类型。当函数的执行次数超过阈值后,LLInt 会触发向 Baseline JIT 的提升。
第 2 层 —— Baseline JIT:模板编译
Baseline JIT 是一个简单的"模板式"编译器:针对每条字节码指令,它生成一段固定的本机指令序列。这里没有跨指令的优化、没有寄存器分配、也没有内联,但编译速度很快,生成的代码通常比解释执行快 5 到 10 倍。
Baseline 编译代码的数据结构定义在 BaselineJITCode.h 中,包括用于属性访问的内联缓存(PropertyInlineCache)和算术分析信息。
Baseline JIT 具有双重职责:既是执行层,也是分析器。它在 LLInt 的基础上继续收集类型反馈,并借助内联缓存获取更丰富的信息:
- 属性访问 IC 记录通过
get_by_id和put_by_id访问的对象的Structure(隐藏类)。 - 调用 IC 记录每个调用点实际调用的函数。
- 算术分析 记录操作数是整数、浮点数还是 BigInt。
这些积累的分析数据将驱动下一层的推测式优化。
第 3 层 —— DFG JIT:推测式优化
数据流图(DFG)JIT 才是真正精彩的地方。DFG 不再逐条翻译字节码,而是构建一种中间表示——数据流图——来捕获操作之间的值依赖关系,再结合低层收集的类型分析数据,对类型做出推测性假设。
sequenceDiagram
participant Baseline as Baseline JIT
participant DFG as DFG Compiler
participant Native as DFG-compiled Code
participant OSR as OSR Exit
Baseline->>DFG: Function is hot, promote
DFG->>DFG: Build DFG IR from bytecode
DFG->>DFG: Insert type guards based on profiles
DFG->>DFG: Optimize: CSE, constant folding, etc.
DFG->>Native: Emit native code with guards
Note right of Native: Fast path: types match
Native->>Native: Execute optimized code
Note right of Native: Slow path: type mismatch
Native->>OSR: OSR Exit - deoptimize
OSR->>Baseline: Resume in Baseline JIT
举个例子:如果分析数据显示 x + y 始终接收整数操作数,DFG 就会生成一条带有类型守卫的快速整数加法指令。如果运行时 x 实际上是一个字符串,守卫便会失败,代码随即执行 OSR Exit(栈上替换退出)——将执行权转回 Baseline JIT,由其中未经优化的代码正确处理这种意外类型。
DFG 的源代码位于 Source/JavaScriptCore/dfg/,包含:
- DFG IR 定义(节点、边、类型)
- 推测式优化器
- OSR 入口(在循环执行中途提升正在运行的 Baseline 函数)
- OSR 退出(推测失败时的回退机制)
- 多个优化阶段(CSE、死代码消除、强度削减等)
第 4 层 —— FTL JIT 与 B3 后端
Faster Than Light(FTL)JIT 是吞吐量最高的执行层。它将 DFG IR 降级到 B3(Bare Bones Backend)——JSC 自研的基于 SSA 的编译器后端。B3 于 2016 年取代 LLVM 成为后端,提供了与 LLVM 相当的经典优化能力,但编译速度快了一个数量级。
入口点是 FTL::compile:
namespace JSC { namespace FTL {
void compile(State&, Safepoint::Result&);
} }
B3 拥有自己的 IR(定义在 Source/JavaScriptCore/b3/),是包含基本块、值和操作的传统 SSA 形式。B3 之下是 Air(Assembly IR),一种接近实际机器指令的低级表示,负责寄存器分配和指令选择,最终生成机器码。
flowchart TD
DFG_IR["DFG IR"] --> FTL["FTL Lowering"]
FTL --> B3_IR["B3 SSA IR"]
B3_IR --> Opts["B3 Optimizations:<br/>• Constant folding<br/>• CFG simplification<br/>• Strength reduction<br/>• Loop-invariant code motion"]
Opts --> Air["Air IR<br/>(low-level)"]
Air --> RegAlloc["Register Allocation"]
RegAlloc --> MachineCode["Native Machine Code<br/>(x86-64 / ARM64)"]
关键在于,B3 施加的是 DFG 所不具备的编译器级优化。DFG 在 JavaScript 语义层面优化(类型特化、内联);B3 则在机器层面优化(寄存器压力、指令调度、冗余加载消除)。两个层级相辅相成,共同发挥作用。
Riptide:并发垃圾回收器
JSC 使用名为 Riptide 的垃圾回收器来管理 JavaScript 对象(与第 2 篇介绍的 C++ 对象引用计数机制相互独立)。Riptide 是一个并发、分代的标记-清除回收器,实现代码位于 Source/JavaScriptCore/heap/。
核心设计要点如下:
- 并发标记。 GC 线程与 mutator(JavaScript 执行线程)并行遍历对象图,最大限度减少停顿时间。
- 写屏障。 JIT 编译代码在将指针写入对象时,必须执行写屏障以通知 GC。各 JIT 层级均已将屏障集成到生成的代码中。
- 保守式栈扫描。 回收期间,GC 扫描本机栈,查找可能是对象指针的值。这一点至关重要,因为 JIT 编译代码会将 JS 值存储在 CPU 寄存器和栈槽中。
- 退缩波前算法。 Riptide 无需全局暂停来修复并发标记期间被修改的对象,而是采用退缩波前算法优雅地处理并发写入。
heap 目录包含:Heap 类(负责协调回收过程)、MarkedBlock(分配单元)、SlotVisitor(标记的核心执行者),以及贯穿 JSC 代码库的 WriteBarrier<T>(屏障类型)。
运行时内置对象与 JSC Shell
runtime/ 目录包含 JavaScript 内置对象的 C++ 实现:JSArray、JSObject、JSFunction、JSPromise、RegExpObject、MapObject 等数十个类型,它们是 JavaScript 代码所交互的对象在底层的本机支撑。
独立的 jsc.cpp shell 将一切串联起来:创建 VM(JSC 的核心状态对象)、创建 GlobalObject,并让脚本走完完整的执行流水线。这对于独立测试 JSC 非常有价值——你可以向它输入 JavaScript 代码,观察哪些层级被触发,转储 DFG IR,或在不涉及 WebCore 和任何浏览器界面的情况下分析 GC 行为。
| 目录 | 内容 |
|---|---|
parser/ |
Lexer、Parser、AST 节点类型 |
bytecompiler/ |
BytecodeGenerator |
bytecode/ |
字节码定义、CodeBlock、值分析 |
llint/ |
LLInt 解释器(宏汇编 DSL) |
jit/ |
Baseline JIT 编译器 |
dfg/ |
DFG IR、推测式优化器、OSR 机制 |
ftl/ |
DFG IR 到 B3 的 FTL 降级 |
b3/ |
B3 SSA 编译器后端 |
b3/air/ |
Air 低级 IR 与寄存器分配器 |
heap/ |
Riptide GC:标记、清除、写屏障 |
runtime/ |
JS 内置对象与 VM 类 |
迈向下一篇
JavaScriptCore 提供了执行引擎,但网页远不止 JavaScript。下一篇文章将深入探索 WebCore——代码库中体量最大的组件——并追踪从 HTML 字节到像素的完整渲染流水线。我们将了解 DOM 树的构建过程、CSS 的解析与层叠计算(包括复用了 JSC 汇编器基础设施的 CSS JIT 选择器编译器),以及布局与绘制如何最终产出视觉输出。此外,我们还将重点审视 Web IDL 绑定系统——正是它在 JSC 的 JavaScript 世界与 WebCore 的 C++ DOM 对象之间架起了桥梁。