这是“快速在 WebAssembly 上运行 JavaScript”系列的延续。第一篇帖子介绍了 PBL(便携式基线解释器),此篇添加了提前编译,最后一篇将讨论提前编译的细节,建议先读第一篇获取有用背景。
世界上最流行的编程语言是 JavaScript,由于网络的普及,它被广泛使用。过去四年左右,作者一直在研究 WebAssembly 工具和平台,特别是在浏览器外运行 Wasm,用于对不受信任的服务器端代码进行强沙盒化。
本文介绍了过去 1.5 年作者构建从 JavaScript 到 WebAssembly 字节码的提前编译器的工作,实现了 3 - 5 倍的加速。该工作已集成到 Bytecode Alliance 的 SpiderMonkey 版本中(最终有望上游化),然后是基于此的共享 StarlingMonkey JS-on-Wasm 运行时(通过componentize.sh
顶层构建命令的--aot
标志可用),以及作者雇主基于 StarlingMonkey 的 JS SDK。它通过了 SpiderMonkey 树中的所有“JIT 测试”和“JS 测试”,以及运行时级别的所有 Web 平台测试,仍处于“beta”阶段且被视为实验性的,但现在是深入了解其工作原理的好时机。
JavaScript 提前编译方法基于在 SpiderMonkey 中的 Portable Baseline Interpreter 工作,并结合 weval 部分程序评估器,以“免费”方式从解释器主体提供编译(使用 Futamura 投影)。本文将深入探讨如何进行 JavaScript 提前编译,以及 weval 如何更轻松地构建必要的编译器后端。
背景:在支持 WebAssembly 的平台上运行 JavaScript 起初无需此问题,因为 Wasm 最初在现有 JS 引擎中作为向引擎编译器后端直接提供低级、强类型代码的方式出现。但在“Wasm 优先”平台上,没有系统级 JS 引擎,只能运行用户上传的 Wasm 模块。采用此限制可获得许多优势,如细粒度隔离和安全、消除 JIT 错误、模块化设计等,但目前将 JS“与其自己的引擎”捆绑的技术仍为解释器与 JS 的字节码表示相结合,未进行编译。
编译 JS:为何困难:主要困难来自 JavaScript 的动态类型,简单表达式可能有多种含义,朴素的编译器会生成大量代码进行类型检查和分发,这在运行时性能和代码大小方面都不可行,需要适应现代 JS 引擎在 Wasm 平台上的技术。现代 JS 引擎的 JIT 编译器通过生成仅针对实际出现情况的代码来解决此问题,但 Wasm 平台不适合当前 JS 引擎的设计,因为 Wasm 平台缺乏动态/运行时代码生成机制、具有受保护的调用栈和无原始控制流、支持细粒度隔离和无预热等。
Wasm 平台的独特特性:
- 无运行时代码生成:平台通常缺乏动态/运行时代码生成机制,代码加载功能在沙盒外,这允许平台更灵活管理,但无法以传统方式实现 JIT。
- 受保护的调用栈和无原始控制流:WebAssembly 具有模块和函数的一级抽象,其函数间转移指令针对已知函数入口点,维护受保护的调用栈,这有利于语言互操作性和安全性,但排除了一些语言运行时用于实现功能的控制流模式。
- 每个请求的隔离和缺乏预热:Wasm 的快速实例化允许细粒度的沙盒化方法,如每个 Wasm 实例仅服务一个 HTTP 请求,然后消失,这有利于安全和错误缓解,但使得基于观察的动态类型语言方案难以实现。
使 JS 快速:专门的运行时代码生成:JavaScript 引擎的 JIT 通常通过内联缓存观察程序行为,并根据观察到的实际情况生成专门的代码。SpiderMonkey 使用内联缓存来收集类型反馈等运行时观察,并将观察到的情况编码在专门的编译器 IR(CacheIR)中。对于内联缓存(IC),可以将常见的 IC 体包含在引擎构建中,通过查找包含预编译 IC 的表来避免运行时编译,同时收集和测试 IC 语料库以保持其更新。
关于 JS 字节码:有了涵盖所有相关情况的 IC 体语料库,通过将 JS 源到代码的翻译进行 1 对 1 的转换,包括一系列 IC 站点和调用当前 IC 存根指针,以及数据和控制流,就有了完整的 JavaScript 提前编译器。
基于 IC 的 JS 的提前(AOT)编译:构建了 JavaScript 的提前编译器,关键在于将动态性推到运行时的后期绑定,通过间接调用 IC 体实现行为的变化,这种执行模型在 SpiderMonkey 中称为基线编译,它可以进行完全的 AOT 编译,但存在不能优化 IC 组合的限制。进一步地,可以从 SpiderMonkey 的设计中学习,通过基线编译和 IC 预热,将类型反馈编译转化为内联问题,构建“概要引导的内联器”来进一步优化。
结果:在 Octane 基准测试套件上,提前编译相比通用解释器有 2.77 倍的几何平均提升,最高可达 4.39 倍,大多数基准测试在 2.5 - 3.5 倍之间。与 SpiderMonkey 的原生基线编译器相比还有提升空间,通过内联 IC 并进一步优化,未来可获得更高性能。
其他方法:其他一些 JS 编译器尝试进行专门的代码生成而无需运行时类型观察或其他 profiling/warmup,如 Manuel Serrano 的 Hopc 编译器和 porffor 编译器,但基于推理的方法存在局限性,如无法像 SpiderMonkey 的 IC 那样对对象形状进行专门化分析。同时,考虑到已有大量嵌入 SpiderMonkey 并基于其 API 编写的代码,以及引擎支持最新 JS 提案并持续维护,作者选择在 SpiderMonkey 基础上构建提前编译器。
接下来:编译器后端(免费):虽然实现了从 JS 源到 Wasm 字节码以及从语料库中的 IC 体到 Wasm 字节码的编译,但未说明如何实现,下一篇将描述如何从解释器自动派生这些编译器后端,显著减少维护负担和复杂性。
感谢 Luke Wagner、Nick Fitzgerald 和 Max Bernstein 阅读并提供反馈。在 Web 上,Wasm 模块可通过 JavaScript 加载和实例化新模块,但此机制在仅 Wasm 平台上不存在,主要是设计选择限制了代码加载方式。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。