JavaScriptCore¶
JavaScriptCore 是 WebKit 内置的 JavaScript 引擎,它实现了 ECMA-262 规范中的 ECMAScript。
概述¶
JavaScriptCore 通常被称为不同的名称,例如 SquirrelFish 和 SquirrelFish Extreme。在 Safari 的语境中,Nitro 和 Nitro Extreme(苹果公司的营销术语)也常被使用。然而,项目和库的名称始终是 JavaScriptCore。
JavaScriptCore 源代码位于 WebKit 源代码树中,它在 Source/JavaScriptCore 目录下。
核心引擎¶
JavaScriptCore 是一个优化虚拟机。JavaScriptCore 由以下构建块组成:词法分析器、解析器、启动解释器(LLInt)、基线 JIT、低延迟优化 JIT(DFG)和高吞吐量优化 JIT(FTL)。
词法分析器¶
词法分析器负责 词法分析,即将脚本源代码分解为一系列标记。JavaScriptCore 的词法分析器是手写的,主要位于 parser/Lexer.h 和 parser/Lexer.cpp 中。
解析器¶
解析器执行 语法分析,即消费来自词法分析器的标记并构建相应的语法树。JavaScriptCore 使用手写的 递归下降解析器,代码位于 parser/JSParser.h 和 parser/JSParser.cpp 中。
LLInt¶
LLInt,全称 Low Level Interpreter(低级解释器),执行解析器生成的字节码。低级解释器的大部分代码位于 llint/。LLInt 使用一种名为 offlineasm(参见 offlineasm/)的可移植汇编语言编写,该语言可以编译为 x86、ARMv7 和 C。LLInt 的目标是除了词法分析和解析之外,启动成本为零,同时遵循即时编译器的调用、栈和寄存器约定。例如,调用 LLInt 函数的效果“就像”该函数被编译成本机代码一样,只不过机器代码入口点实际上是一个共享的 LLInt 序言。LLInt 包含诸如内联缓存等优化,以确保快速属性访问。
基线¶
当函数至少被调用 6 次,或循环至少 100 次(或某种组合——例如 3 次调用总共 50 次循环迭代)时,基线 JIT 就会启动。请注意,这些数字是近似值;实际的启发式算法取决于函数大小和当前内存压力。如果 LLInt 陷入循环,它会进行栈上替换 (OSR) 到 JIT;并且函数的所有调用者都会重新链接以指向编译后的代码,而不是 LLInt 序言。基线 JIT 也作为优化 JIT 编译函数的备用方案:如果优化后的代码遇到无法处理的情况,它会(通过 OSR 退出)回退到基线 JIT。基线 JIT 位于 jit/。基线 JIT 还对几乎所有堆访问执行复杂的、多态的内联缓存。
LLInt 和基线 JIT 都收集轻量级分析信息,以使下一层执行(DFG)能够进行推测性执行。收集的信息包括最近加载到参数中、从堆中加载或从调用返回值中加载的值。此外,LLInt 和基线 JIT 中的所有内联缓存都经过设计,以便 DFG 能够轻松地抓取类型信息:例如,DFG 可以通过查看内联缓存的当前状态来检测堆访问有时、经常或总是看到特定类型;这可以用于确定最有利的推测级别。JavaScriptCore 中类型推断的更详尽概述将在下一节中提供。
DFG¶
当函数至少被调用 60 次,或循环至少 1000 次时,DFG JIT 就会启动。同样,这些数字是近似值,并且会受到额外启发式算法的影响。DFG 基于较低层收集的分析信息执行激进的类型推测。这使得它能够前向传播类型信息,省略许多类型检查。有时 DFG 更进一步,对值本身进行推测——例如,它可能推测从堆中加载的值总是某个已知函数,以便启用内联。DFG 使用去优化(我们称之为“OSR 退出”)来处理推测失败的情况。去优化可以是同步的(例如,检查值类型是否符合预期的分支)或异步的(例如,运行时可能观察到某个对象或变量的形状或值以违反 DFG 假设的方式发生了变化)。后者在 DFG 代码库中被称为“watchpointing”。总而言之,基线 JIT 和 DFG JIT 共享双向 OSR 关系:当函数变热时,基线 JIT 可能会 OSR 到 DFG;而在去优化的情况下,DFG 可能会 OSR 到基线 JIT。DFG 的重复 OSR 退出作为额外的分析提示:DFG OSR 退出机制记录了退出原因(包括可能失败推测的值)以及发生的频率;如果退出发生得足够频繁,则会重新优化:调用者重新链接到受影响函数的基线 JIT,收集更多分析信息,然后 DFG 可能会稍后重新调用。重新优化使用指数退避来防御病态代码。DFG 位于 dfg/。
FTL¶
当函数被调用数千次或循环数万次时,FTL JIT 就会启动。有关更多信息,请参阅 FTLJIT。
在任何时候,JavaScriptCore 中的函数、eval 块和全局代码都可能在 LLInt、Baseline JIT、DFG 和 FTL 的混合环境中执行。在递归函数的极端情况下,甚至可能存在多个栈帧,其中一个帧在 LLInt 中,另一个在 Baseline JIT 中,而另一个仍在 DFG 甚至 FTL 中;更极端的情况是,一个栈帧正在执行旧的 DFG 或 FTL 编译,而另一个正在执行新的 DFG 或 FTL 编译,因为重新编译已启动但执行尚未返回到旧的 DFG/FTL 代码。这四个引擎旨在保持相同的执行语义,因此即使 JavaScript 程序中的多个函数在这些引擎的混合环境中执行,唯一可察觉的影响也应该是执行性能。