Firefox引入新的JS解释器,编译速度比Chrome快10倍!
Posted 前端之巅
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Firefox引入新的JS解释器,编译速度比Chrome快10倍!相关的知识,希望对你有一定的参考价值。
与几年前相比,现代 Web 应用程序需要加载并执行的 JavaScript 代码要多很多。虽然 JIT(即时)编译器大幅提升了 JavaScript 的执行性能,但我们还是需要一个更好的解决方案来应对不断增长的负载需求。
为了解决这个问题,我们在 Firefox 70 中为 JavaScript 引擎添加了一个新的 JavaScript 字节码解释器。这个解释器现在已进入 Firefox Nightly 通道试用,将在 10 月份正式发布;它并不是从零开始新造的一个轮子,而是在现有的基线 JIT 代码基础上发展而来的。
这个新生成的基线解释器提升了性能、减少了内存占用并简化了代码库。本文将具体介绍其背后的机制:
在现代 JavaScript 引擎中,所有函数都是在字节码解释器中开始执行的。需要频繁调用(或执行许多循环迭代)的函数被编译为原生机器码。(这称为 JIT 编译。)
-
基线 JIT。(Baseline JIT)每条字节码指令直接编译为一小段机器码。它使用 内联缓存 来提升性能并从 Ion 收集类型信息。 IonMonkey(简称 Ion),优化版的 JIT。它使用高级编译优化技术为热点函数生成速度较快的代码(代价是编译时间较长)。
函数的 Ion JIT 代码可能因为种种原因被“去优化”并丢弃,例如使用新的参数类型调用函数时就会出现这种情况。这被称为 bailout。当 bailout 发生时,在下一次 Ion 编译之前都会使用基线代码执行。
在 Firefox 70 之前的版本中,一个高热函数的执行流水线如下所示:
C++ 解释器➡️基线编译器➡️基线 JIT 代码➡️为 Ion 准备➡️基线 JIT 代码 / 主线程外的 Ion 编译➡️Ion JIT 代码
-
基线 JIT 编译速度很快,但像谷歌文档或 Gmail 这样的现代 Web 应用程序需要运行的 JavaScript 代码太多了,基线编译器往往需要花费很长的时间来编译数以千计的函数。 -
因为 C++ 解释器速度非常慢,而且不收集类型信息,所以不管是等待基线编译结果还是将其移出主线程(off-thread)都可能带来性能损失。 -
正如你在上图中所看到的,优化过的 Ion JIT 代码只能 bailout 到基线 JIT 代码上。为此基线 JIT 代码需要额外的元数据(对应于每条字节码指令的机器码偏移)。 基线 JIT 的 bailout、调试器支持和异常处理功能需要很复杂的代码,当这些功能交叉应用时代码尤其复杂!
我们需要来自基线 JIT 的类型信息来优化编译层,并使用 JIT 编译来提高运行时性能。但现代 Web 应用的代码库太大了,即便是相对较快的基线 JIT 编译器也需要花费大量时间。为了解决这个问题,Firefox 70 在流水线中新生成了一个名为基线解释器(Baseline Interpreter)的处理层:
基线解释器位于 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 数据现在还只是机器码。我们将所有内联缓存和分析(profile)数据都移动到了 JitScript 里。
像这样的共享帧布局有很多优点。我们的基线解释器帧几乎没对 C++ 和 IC 代码做什么更改——它们和基线 JIT 帧是一回事。此外当脚本热度够高,适合用基线 JIT 编译时,从基线解释器代码切换到基线 JIT 代码也会很简单,只需从解释器代码跳转到 JIT 代码: https://searchfox.org/mozilla-central/rev/8ea946dcf51f0d6400362cc1d49c8d4808eeacf1/js/src/jit/BaselineCodeGen.cpp#1262-1275
-
BaselineCompiler:基线 JIT 用其将脚本的字节码编译为机器码。 BaselineInterpreterGenerator:用于生成基线解释器代码。
模板化 BaselineCodeGen 基类: https://searchfox.org/mozilla-central/rev/8ea946dcf51f0d6400362cc1d49c8d4808eeacf1/js/src/jit/BaselineCodeGen.h#264-269
这个基类有一个 Handler C++ 模板参数,可用于基线解释器或 JIT 的特定行为。可以通过这种方式共享许多基线 JIT 代码。例如,JSOP_GETPROP 字节码指令的实现(用于像 JavaScript 代码中的 obj.foo 这样的属性访问)是共享代码。它调用emitNextIC 辅助方法,该方法专用于解释器或 JIT 模式。
emitNextIC 辅助方法: https://searchfox.org/mozilla-central/rev/8ea946dcf51f0d6400362cc1d49c8d4808eeacf1/js/src/jit/BaselineCodeGen.cpp#531-588
万事俱备,接下来我们就能够实现 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 需要编译的代码要少得多。
-
2-8%不等的页面加载速度提升。页面加载和 JS 执行(解析、样式、布局、图形)有很多改善,这样的提升幅度还是很明显的。 -
许多开发工具性能测试结果提高了 2-10%不等。 内存占用也有小幅改善。
请注意,我们之后版本的性能提升幅度还会更大一些。
为了对比基线解释器与 C++ 解释器和基线 JIT 的性能,我在 Mozilla 的测试服务器的 Windows 10 64 位系统中测试了 Speedometer 和谷歌文档,并逐个启用了这些层。(以下结果是七次测试中的最好成绩):
在谷歌文档上,我们看到基线解释器比 C++ 解释器快很多。启用基线 JIT 也会使页面加载速度提高一点。
在 Speedometer 基准测试中,启用基线 JIT 层时结果要好得多。基线解释器还是比 C++ 解释器更快:
我们认为这样的结果很不错:基线解释器比 C++ 解释器快得多,它的启动时间(JitScript 分配)比基线 JIT 编译快得多(至少快 10 倍)。
这些工作都完成后,我们就能利用基线解释器来简化基线 JIT 和 Ion 代码了。
例如,Ion 的去优化 bailout 现在会在基线解释器中恢复,无需再用基线 JIT。解释器可以在 JS 代码的下一个循环迭代中重新输入基线 JIT 代码。在解释器中恢复比在基线 JIT 代码中恢复要容易得多。我们现在不用为基线 JIT 代码记录那么多元数据,因此基线 JIT 编译的性能也随之提升。此外我们还能移除许多用于调试器支持和异常处理的复杂代码: https://hg.mozilla.org/mozilla-central/rev/49a2da59aa3e#l3.535
有了基线解释器后,现在我们应该可以把基线 JIT 编译放到主线程外了。我们将在未来几个月内开展这项工作,并期望在这一领域取得更多性能提升。
基线解释器的大部分工作是我做的,但也有很多人为此项目作出了贡献。尤其值得一提的是 Ted Campbell 和 Kannan Vijayan 审核了多数代码更改并提供了很好的设计反馈意见。
还要感谢 Steven DeTar、Chris Fallin、Havi Hoffman、Yulia Startsev 和 Luke Wagner 对本文的反馈。
英文原文: https://hacks.mozilla.org/2019/08/the-baseline-interpreter-a-faster-js-interpreter-in-firefox-70/
以上是关于Firefox引入新的JS解释器,编译速度比Chrome快10倍!的主要内容,如果未能解决你的问题,请参考以下文章
在Firefox 58中,WebAssembly组件性能提升了10倍