Read OSS

深入 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_idput_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++ 实现:JSArrayJSObjectJSFunctionJSPromiseRegExpObjectMapObject 等数十个类型,它们是 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 对象之间架起了桥梁。