简介
现代 Web 应用程序加载并执行的 JavaScript 代码比几年前要多得多。虽然 JIT(即时)编译器 在提高 JavaScript 性能方面非常成功,但我们需要更好的解决方案来处理这些新的工作负载。
为了解决这个问题,我们在 Firefox 70 中的 JavaScript 引擎中添加了一个新的、生成的 JavaScript 字节码解释器。该解释器现在可以在 Firefox Nightly 通道中使用,并将在 10 月份正式发布。我们没有从头开始编写或生成新的解释器,而是找到了一种方法,通过与我们现有的基线 JIT 共享大部分代码来实现这一点。
新的基线解释器提高了性能,减少了内存使用量,并简化了代码。以下是我们的方法。
执行层
在现代 JavaScript 引擎中,每个函数最初都在 字节码解释器 中执行。被频繁调用(或执行多次循环迭代)的函数会被编译成本地机器代码。(这称为 JIT 编译。)
Firefox 有一个用 C++ 编写的解释器和多个 JIT 层。
- 基线 JIT。每个字节码指令都被直接编译成一小段机器代码。它使用 内联缓存 (IC) 作为性能优化,并为 Ion 收集类型信息。
- IonMonkey(或简称 Ion),优化 JIT。它使用高级编译器优化,为热门函数生成快速代码(以较慢的编译时间为代价)。
函数的 Ion JIT 代码可能会因各种原因被“反优化”并丢弃,例如当函数被调用时使用新的参数类型时。这称为回退。当发生回退时,执行将继续在基线代码中进行,直到下一个 Ion 编译。
在 Firefox 70 之前,非常热门函数的执行管道是这样的。
问题
虽然这种方法运行良好,但我们在管道的第一个部分(C++ 解释器和基线 JIT)遇到了以下问题。
- 基线 JIT 编译速度很快,但现代 Web 应用程序(如 Google Docs 或 Gmail)执行的 JavaScript 代码量如此之大,以至于我们在基线编译器中花费了相当多的时间,编译了数千个函数。
- 由于 C++ 解释器速度很慢,并且不收集类型信息,延迟基线编译或将其移出线程将是一个性能风险。
- 如上图所示,优化的 Ion JIT 代码只能回退到基线 JIT。为了使这种方法有效,基线 JIT 代码需要额外的元数据(对应于每个字节码指令的机器代码偏移量)。
- 基线 JIT 有一些用于回退、调试器支持和异常处理的复杂代码。在这些功能相互交叉的地方尤其如此!
解决方案:生成更快的解释器
我们需要从基线 JIT 获取类型信息,以启用更优化的层,并且我们希望使用 JIT 编译来提高运行时速度。然而,现代 Web 具有如此庞大的代码库,即使是相对较快的基线 JIT 编译器也花费了大量时间进行编译。为了解决这个问题,Firefox 70 在管道中添加了一个新的层,称为基线解释器。
基线解释器位于 C++ 解释器和基线 JIT 之间,并包含两者的元素。它使用固定的解释器循环执行所有字节码指令(类似于 C++ 解释器)。此外,它使用内联缓存来提高性能和收集类型信息(类似于基线 JIT)。
生成解释器并不是一个新想法。然而,我们发现了一种很好的新方法,通过重用大部分基线 JIT 编译器代码来实现这一点。基线 JIT 是一个模板 JIT,这意味着每个字节码指令都被编译成一个几乎固定的机器指令序列。我们将这些序列生成到解释器循环中。
共享内联缓存和概要分析数据
如上所述,基线 JIT 使用内联缓存 (IC) 来提高速度,并帮助 Ion 编译。为了获取类型信息,Ion JIT 编译器可以检查基线 IC。
因为我们希望基线解释器与基线 JIT 使用完全相同的内联缓存和类型信息,所以我们添加了一个名为 JitScript 的新数据结构。JitScript 包含基线解释器和 JIT 使用的所有类型信息和 IC 数据结构。
下图显示了它在内存中的样子。每个箭头都是 C++ 中的一个指针。最初,函数只包含一个 JSScript,其中包含可以由 C++ 解释器解释的字节码。经过几次调用/迭代后,我们创建了 JitScript,将其附加到 JSScript,现在可以在基线解释器中运行脚本。
随着代码变得更热,我们也可能会创建 BaselineScript(基线 JIT 代码)和 IonScript(Ion JIT 代码)。
请注意,函数的基线 JIT 数据现在只是机器代码。我们将所有内联缓存和概要分析数据移到了 JitScript 中。
共享帧布局
基线解释器使用与基线 JIT 相同的帧布局,但我们在帧中添加了一些 特定于解释器的字段。例如,字节码 PC(程序计数器)是当前正在执行的字节码指令的指针,在基线 JIT 代码中不会被显式更新。如果需要,可以从返回地址确定它,但基线解释器必须将其存储在帧中。
像这样共享帧布局有很多优点。我们几乎没有对 C++ 和 IC 代码进行任何更改来支持基线解释器帧——它们就像基线 JIT 帧一样。此外,当脚本足够热以进行基线 JIT 编译时,从基线解释器代码切换到基线 JIT 代码仅仅是 从 解释器代码 跳转到 JIT 代码。
共享代码生成
由于基线解释器和 JIT 非常相似,因此许多代码生成代码也可以共享。为此,我们添加了一个模板化的 BaselineCodeGen
基类,它有两个派生类。
BaselineCompiler
:由基线 JIT 用于将脚本的字节码编译成机器代码。BaselineInterpreterGenerator
:用于生成基线解释器代码。
基类具有一个 Handler C++ 模板参数,可用于专门针对基线解释器或 JIT 行为。很多基线 JIT 代码都可以通过这种方式共享。例如,JSOP_GETPROP
字节码指令的实现(用于 JavaScript 代码中的属性访问,例如 obj.foo
)是共享代码。它调用了 emitNextIC
辅助方法,该方法被 专门化 用于解释器或 JIT 模式。
生成解释器
有了所有这些部件,我们就能实现 BaselineInterpreterGenerator
类来生成基线解释器!它生成一个线程化的解释器循环:每个字节码指令的代码 后跟 一个指向下一个字节码指令的间接跳转。
例如,在 x64 上,我们目前生成以下机器代码来解释 JSOP_ZERO
(字节码指令,用于将零值压入堆栈)。
// Push Int32Value(0).
movabsq $-0x7800000000000, %r11
pushq %r11
// Increment bytecode pc register.
addq $0x1, %r14
// Patchable NOP for debugger support.
nopl (%rax,%rax)
// Load the next opcode.
movzbl (%r14), %ecx
// Jump to interpreter code for the next instruction.
leaq 0x432e(%rip), %rbx
jmpq *(%rbx,%rcx,8)
当我们在 7 月份的 Firefox Nightly(版本 70)中启用基线解释器时,我们将基线 JIT 预热阈值从 10 提高到 100。预热计数是通过对函数的调用次数 + 到目前为止的循环迭代次数进行计数来确定的。基线解释器的阈值为 10,与旧的基线 JIT 阈值相同。这意味着基线 JIT 编译的代码要少得多。
结果
性能和内存使用
在它被添加到 Firefox Nightly 之后,我们的性能测试基础设施检测到了一些改进。
- 各种 页面加载改进,范围从 2% 到 8%。除了 JS 执行之外,页面加载期间还会发生很多事情(解析、样式、布局、图形)。像这样的改进非常重要。
- 许多开发者工具性能测试 提高了 2% 到 10%。
- 一些小的内存使用改进。
请注意,自从它首次发布以来,我们已经实现了更多性能改进。
为了衡量基线解释器性能与 C++ 解释器和基线 JIT 的对比,我在 Mozilla 的 Try 服务器上,在 Windows 10 64 位系统上运行了 Speedometer 和 Google Docs,并逐一启用各个层级。(以下数字反映了 7 次运行中的最佳结果。)
在 Google Docs 上,我们看到基线解释器比仅使用 C++ 解释器快得多。启用基线 JIT 后,页面加载速度略有提升。
在 Speedometer 基准测试中,当我们启用基线 JIT 层级时,获得了明显更好的结果。基线解释器再次比仅使用 C++ 解释器好得多。
我们认为这些数字非常棒:基线解释器比 C++ 解释器快得多,并且它的启动时间(JitScript 分配)比基线 JIT 编译快得多(至少快 10 倍)。
简化
在所有这些功能落地并稳定后,我们能够通过利用基线解释器来简化基线 JIT 和 Ion 代码。
例如,来自 Ion 的反优化逃逸现在会在基线解释器中恢复,而不是在基线 JIT 中恢复。解释器可以在 JS 代码中下一次循环迭代时重新进入基线 JIT 代码。在解释器中恢复比在基线 JIT 代码中间恢复要容易得多。现在我们只需要为基线 JIT 代码记录更少的元数据,因此基线 JIT 编译也变得更快。类似地,我们能够 移除大量用于调试支持和异常处理的复杂代码。
下一步是什么?
有了基线解释器,现在应该可以将基线 JIT 编译移到线程之外。我们将在未来几个月内致力于这项工作,预计这方面将有更多性能提升。
致谢
虽然我做了大部分基线解释器的工作,但许多其他人也为这个项目做出了贡献。特别是 Ted Campbell 和 Kannan Vijayan 审查了大部分代码更改,并提供了很好的设计反馈。
还要感谢 Steven DeTar、Chris Fallin、Havi Hoffman、Yulia Startsev 和 Luke Wagner 对这篇博文提出的反馈。
关于 Jan de Mooij
Jan 是 Mozilla 的软件工程师,他在那里负责 SpiderMonkey,Firefox 中的 JavaScript 引擎。他住在荷兰。
14 条评论