JavaScriptCore(2020-7-17 译文修正版)
JavaScriptCore(2020-7-17 译文修正版)

JavaScriptCore(2020-7-17 译文修正版)

JavaScriptCore是WebKit的内置JavaScript引擎。它目前实现了ECMA-262 规范中的ECMAScript。

JavaScriptCore通常会以不同的名称被涉及到,例如SquirrelFishSquirrelFish Extreme。在Safari的上下文中,Nitro和Nitro Extreme(苹果的销售术语)也经常被使用。然而,project和library的名称总是JavaScriptCore。

JavaScriptCore源代码位于WebKit源码树里面,就在Source/JavaScriptCore目录下。

Core Engine

JavaScriptCore是一个优化虚拟机。JavaScriptCore由这些构建块组成:lexer,parser,start-up interpreter(LLInt),baseline JIT,a low-latency optimizing JIT(DFG),a high-throughput optimizing jIT(FTL)。

Lexer负责词法分析,也就是将script源码分解为一系列的tokens,JavaScriptCore lexer是手写的,主要在parser/Lexer.hparser/Lexer.cpp中。

Parser带来了语法解析的功能,也就是消费lexer分解出来的tokens,并且构建相应的语法树。JavaScriptCore使用了手写的recursive descent parser,相关代码位于parser/Parser.hparser/Parser.cpp中。

LLInt,Low Level Interpreter的简称,执行Parser生产的字节码。Low Level Interpreter的大部分位于llint/中。LLInt是用一种叫做offlineasm的轻量级汇编程序编写的。除了词法分析和语法解析外,LLInt拥有0启动花销,同时遵守JIT编译器使用的调用、堆栈、注册约定。例如,调用一个LLInt函数”就像“这个函数被编译成了机器码一样,只是机器码的入口实际上是个共享的LLInt序言块。

Baseline JIT 用于至少调用了6次,或者至少循环了100次的函数(或者某种组合——比如3次调用,总共50次循环迭代)。注意,这些数字是近似值;实际的启发式方法依赖于函数的大小和当前内存压力。当LLInt在一个循环中卡住时,他会on-stack-replace(OSR)为BaselineJIT;此外,函数的所有调用者都重新链接指向已编译的代码,而不是LLInt序言块。Baseline JIT还充当了optimizingJIT(DFG)的回退:如果优化过的代码遇到它无法处理的情形,它会回退(通过OSR出口)到Baseline JIT。Baseline JIT 在 jit/中。Baseline JIT还为几乎所有堆访问执行复杂的多态内联缓存。

LLInt和Baseline JIT都收集轻量级概要信息,以支持下一层执行(DFG)的推测性执行。信息收集包括最近加载到参数中的值,从对中加载的值,或者从调用返回中加载的值。此外,LLInt和Baseline JIT中的所有内联缓存都被设计成使DFG能够轻松地获取类型信息: 例如DFG仅通过查看内联缓存的当前状态,就可以了解到一个堆访问看到特定类型的频率,是偶尔的/经常的/频繁的。这可以用来确定最有益的推测。下一节将提供JavaScriptCore中更全面的类型推断概述。

DFG JIT 用于至少调用了60次,或者至少循环了1000次的函数。同样,这些数字是近似值,收到额外的启发式支配。DFG根据较低层收集的分析信息执行主动类型推测。这允许它向前传播类型信息,省略了许多类型检查。有时,DFG会更进一步,自己对价值进行推测。例如,它可能推测从堆加载的值总是某个已知函数,以便启用内联。DFG使用反优化(我们称之为“OSR退出”)来处理猜测失败的情况。反优化可能是同步的(例如,一个检查值类型的分支是被期望的)或异步的(例如,运行时可能观察到一些对象/变量的形状/值被以违反DFG所做假定的方式改变了)。后者在DFG代码库中称为“watchpointing”。总之,Baseline JIT 和 DFG JIT 共享一个双向OSR关系:当函数变hot时,Baseline JIT 可能 OSR 到 DFG ,而在反优化的情景中,DFG可能 OSR 到 Bashline JIT。重复的从DFG进行OSR退出可以作为一个额外的分析提示:‘DFG OSR 退出系统’记录退出的原因(包括可能推测失败的值)和频次;如果退出的次数足够多,那么就需要进行重新优化:调用者将重新连接到受影响函数的BaselineJIT,收集更多的分析,可能在以后重新调用DFG。重新优化使用指数后退来防御病态代码。 DFG 在 dfg/中。

FTL JIT 用于至少调用了上千次,或者至少循环了上万次的函数。查看FITLJIT获得更多内容。

在任何时候,JavaScriptCore重的functions, eval blocks, 和 global code可能以LLInt,Baseline JIT ,DFG, FTL混合的方式来执行。在极端的递归函数中,甚至可能存在多个堆栈帧,其中一帧在LLInt中,另一帧在BaselineJIT中,而另一帧仍然在DFG甚至FTL中;更极端的情况是,一个堆栈帧正在执行旧的DFG或FTL编译,而另一个堆栈帧正在执行新的DFG或FTL编译,因为重新编译已经启动,但执行还没有返回到旧的DFG/FTL代码。这四个引擎被设计成维持相同的执行语义,因此,即使JavaScript程序中的多个函数在这些引擎的混合中执行,唯一可以感觉到的影响应该是执行性能。

Type Inference

类型推测是通过分析值来实现的,根据这些概要文件推测将产生什么类型的操作,根据类型推测插入类型检查,然后根据类型检查来尝试构造关于值的类型的类型推测。

考虑这个例子来激发JavaScript的类型推测、优化:

o.x * o.x + o.y * o.y

假设在使用这段代码的上下文中,‘o’是一个对象,它确实有属性‘x’、‘y’,并且这些属性没有什么特殊之处-特别是,他们不是访问器。我们也说‘o.x‘和’o.y‘经常返回doubles值,但是有时也可能返回integers值-javascript没有一个内置的integer值概念,但是为了提高效率JavaScriptCore将大多数证书表示为int32而不是double。想要理解JavaScriptCore类型推断的问题,以及它的解决方案,首先考虑一下如果没有关于‘o’,‘o.x’,‘o.y’的信息,JavaScript引擎将不得不做什么:

  • 表达式‘o.x’必须先检查是否‘o’有特殊的属性访问句柄。它可能是一个DOM对象,DOM对象可能以非明显的方式拦截对其属性的访问。如果它没有特殊句柄,引擎必须在对象中查找名为“x”的属性(其中“x”字面上是一个字符串)。对象只是将字符串映射到值或访问器的表。如果它映射到访问器,则必须调用访问器。如果它不是访问器,则返回值。如果在’o’中没有找到”x”,那么对o的prototype重复这个过程。本节不讨论优化对象访问所需的推理。
  • 表达式o.x * o.x 中的二进制乘法运算必须首先检查它操作数的类型。任何一个操作数都可以是对象,在这种情况下,必须调用其“valueOf”方法。任何一个操作数都可以是字符串,在这种情况下必须转换为数字。一旦两个操作数被适当地转换为数字(或者如果它们已经是数字),引擎必须检查它们是否都是整数;如果是,则执行整数乘。这可能会溢出,在这种情况下,将执行双精度乘运算。如果其中一个操作数是双精度数,则两个操作数都转换为双精度数,并执行双精度乘运算。因此’o.x * o.x‘可以返回一个整数或一个双精度。对于一般的JavaScript乘法,没有办法证明它将返回什么类型的数字以及该数字将如何表示。
  • 表达式o.x * o.x 中的二进制加法运算必须大致按照乘法的方式进行,除了它必须考虑操作数为字符串的情况,在这种情况下,执行一个字符串连接。在本例中,我们可以静态地证明并非如此——乘法一定返回了一个数字。但是,加法仍然必须在两个操作数上检查整数和双精度数,因为我们不知道乘法表达式返回了哪种类型。因此,加法也可能返回一个整数或一个双精度数。

JavaScriptCore类型推断背后的直觉是,如果我们能猜出流进该操作的类型,我们就可以很有可能地说出一个数值操作(比如加法或乘法)将返回什么类型,以及它将走哪条路径。这就形成了一种归纳步骤,适用于一般不操作堆的操作:如果我们可以预测它们的输入,那么我们就可以预测它们的输出。但是归纳法需要一个基本情况。在JavaScript操作的情况下,基本情况是获得非本地值的操作:例如,从堆加载一个值(如’o.x’),访问一个函数的参数,或使用一个函数调用返回的值。对于参数,我们把函数序言块视为某种‘堆操作’,它将参数“加载”到参数变量中。我们通过使用值分析来引导我们关于类型预测的归纳推理:LLInt和Baseline JIT都会记录在任何堆操作中看到的最新值。每个堆操作都有一个与之关联的值配置桶,并且每个值配置桶将恰好存储一个最近的值。

JavaScriptCore类型推断的一个稻草人视图是,我们将每个值配置文件的最新值转换为一个类型,然后应用归纳步骤在函数中的所有操作中传播这个类型。这为我们提供了函数中每个值生成操作的类型预测。所有变量也都变成了预测类型。

实际上,JavaScriptCore在每个值配置文件中包含了第二个字段,该字段是一个类型,它限定了所看到的值的随机子集。该类型使用了SpeculatedType(简称SpecType)类型系统,该系统在SpeculatedType.h中实现。每个值配置文件的类型一开始都是SpecNone——即不对应任何值的类型。你也可以认为这是bottom (see Lattice),或者“contradiction”。当BaselineJIT的执行计数器超过一个阈值时,它将生成一个新类型来绑定上一个类型和最近的值。它还可以选择调用DFG,或者选择让baselineSpecTypes代码运行更多时间。我们的启发式方法倾向于后者,这意味着当DFG编译开始时,每个值配置文件通常都有一个边界多个不同值的类型。

SpecTypes传播的用意在于函数中所有操作和变量的值配置使用一个标准 forward data flow 公式来执行,实现为一个flow-insensitive定点。这是DFG编译的第一个阶段,只有在Baseline JIT根据执行计数决定函数最好执行优化的代码时才会激活。见 DFGPredictionPropagationPhase.cpp.

在函数中的每个值标记为预测类型之后,我们插入基于这些预测的推测类型检查。例如,在一个数字操作(如’o.x * o.y’),我们对乘法的操作数插入预测double检查。如果猜测检查失败,执行将从优化的DFG代码转移到Baseline JIT。这可以证明DFG中后续代码的类型。考虑一下简单的加法运算“a + b”是如何被分解的,如果“a”和“b”都被指定SpecInt32是它们的预测类型:

check if a is Int32 -> else OSR exit to Baseline JIT
check if b is Int32 -> else OSR exit to Baseline JIT
result = a + b // integer addition
check if overflow -> else OSR exit to Baseline JIT

在此操作完成后,我们知道:

  • ‘a’ is an integer.
  • ‘b’ is an integer.
  • the result is an integer.

对“a”或“b”的任何后续操作都不需要检查它们的类型。对结果的操作也是如此。通过第二个数据流分析(简称DFG CFA)消除后续检查。与构建类型预测的预测传播阶段不同,CFA关注的是构建类型证明。The CFA, found in DFGCFAPhase.cpp and DFGAbstractInterpreterInlines.cpp,遵循对流敏感的前向数据流公式。它还实现了稀疏条件常数传播,这使它有时能够证明值是常量,以及证明它们的类型。

把这些组合在一起,表达式’o.x * o.x + o.y * o.y’只需要对从‘o.x’和‘o.y’加载的值进行类型检查。在那之后,我们知道值是双精度的,我们知道我们只需要发出一个双精度乘流程,然后是一个双精度加法。当与类型检查提升相结合时,DFG代码通常在每个堆负载中最多执行一次类型检查。

原文地址

发表评论

邮箱地址不会被公开。