组装 WebAssembly
我们很高兴地宣布 WebKit 已经全面实现了 WebAssembly。
动态二重奏
WebAssembly 是 JavaScript 的一个实用搭档。它并非设计用于手写,而是一种低级二进制格式,旨在成为 C++ 等现有语言的合适编译目标。浏览器看到的 WebAssembly 代码将已经过高级的、特定于语言的优化。这非常棒,因为它意味着实现无需了解 C++ 或其他语言是如何优化的。在开发人员的机器上运行耗费资源的特定语言优化,使得 WebKit 可以专注于特定于目标的优化。这也意味着我们可以将 WebAssembly 编译器优化集中在快速代码交付和可预测的性能上。
WebAssembly 无法完成 JavaScript 所能做的所有事情。例如,WebAssembly 除非通过调用 JavaScript,否则无法访问 DOM。WebAssembly 旨在与 JavaScript 结合使用。JavaScript/WebAssembly 这一动态二重奏协同工作,让 JavaScript 专注于掌控世界,而 WebAssembly 则加速计算密集型任务。

JavaScript 及其搭档,扮演 1966 年的蝙蝠侠
如果没有安全性和可移植性,Web 就不会是现在的 Web。WebAssembly 在这两方面都表现出色。它以接近原生代码的速度执行 C++ 代码,并提供与 JavaScript 相同的无漏洞安全保障。我们的实现支持 x86-64 和 ARM64 平台上的 WebAssembly。可移植性能才是 WebAssembly 的真正亮点:WebAssembly 虚拟指令集架构旨在成为当今现代处理器的理想编译目标。
搭档
WebAssembly 与其他 Web 平台组件无缝衔接,就像一个好搭档。它重用了现有的 Web API,并暴露了一个新的 JavaScript API。WebAssembly 有四个核心概念:WebAssembly.Module
、WebAssembly.Instance
、WebAssembly.Memory
和 WebAssembly.Table
。模块代表代码,类似于磁盘上的程序,而实例则是该程序的执行。同一个模块可以有多个并发执行的实例。最后,内存代表实例的堆。它是连续的、经过边界检查的,并且可以作为 ArrayBuffer
暴露给 JavaScript。所有 WebAssembly 内存操作都在实例的内存上进行。最后,表持有 WebAssembly 函数的句柄,允许实例内的间接调用以相同签名(在 C++ 语境中:虚函数和函数指针)指向不同的函数。有趣的是,实例可以共享相同的内存,并且表可以直接跨实例调用,从而实现动态链接。
由于 WebAssembly 将自身暴露为一个常规的 JavaScript 对象,我们能够重用 WebKit 中已存在的一些机制。一个有趣的例子是我们重用了ECMAScript 模块实现来部署 WebAssembly.Instance
的 API。目前这只是一个实现细节——对 Web 开发者来说是不可见的——但是与 ECMAScript 模块的集成正在讨论中。对于使用模块的开发者来说,JavaScript 和 WebAssembly 之间的交互将完全无缝。在模块的掩护下,我们英雄们的秘密身份将不会被揭示。
为了允许 Web Workers 之间共享模块,并为未来的线程等功能做准备,我们已经使 WebAssembly 代码的内部表示成为线程安全的。这允许开发者在 worker 之间 postMessage
一个编译好的 WebAssembly.Module
,而无需重新编译、复制或任何其他冗余工作。我们对模块的 postMessage
实现比谜语更简单:在 worker 之间共享模块涉及将对内部模块表示的引用传递给另一个 worker。该 worker 将运行与最初生成模块的代理相同的机器代码。
工具腰带
WebAssembly 直接暴露 32 位和 64 位整数以及 32 位和 64 位浮点数。它的指令集同样简单
i32.add | i64.add | f32.add | f64.add | i32.wrap/i64 | i32.load8_s | i32.store8 |
i32.sub | i64.sub | f32.sub | f64.sub | i32.trunc_s/f32 | i32.load8_u | i32.store16 |
i32.mul | i64.mul | f32.mul | f64.mul | i32.trunc_s/f64 | i32.load16_s | i32.store |
i32.div_s | i64.div_s | f32.div | f64.div | i32.trunc_u/f32 | i32.load16_u | i64.store8 |
i32.div_u | i64.div_u | f32.abs | f64.abs | i32.trunc_u/f64 | i32.load | i64.store16 |
i32.rem_s | i64.rem_s | f32.neg | f64.neg | i32.reinterpret/f32 | i64.load8_s | i64.store32 |
i32.rem_u | i64.rem_u | f32.copysign | f64.copysign | i64.extend_s/i32 | i64.load8_u | i64.store |
i32.and | i64.and | f32.ceil | f64.ceil | i64.extend_u/i32 | i64.load16_s | f32.store |
i32.or | i64.or | f32.floor | f64.floor | i64.trunc_s/f32 | i64.load16_u | f64.store |
i32.xor | i64.xor | f32.trunc | f64.trunc | i64.trunc_s/f64 | i64.load32_s | |
i32.shl | i64.shl | f32.nearest | f64.nearest | i64.trunc_u/f32 | i64.load32_u | |
i32.shr_u | i64.shr_u | i64.trunc_u/f64 | i64.load | call | ||
i32.shr_s | i64.shr_s | f32.sqrt | f64.sqrt | i64.reinterpret/f64 | f32.load | call_indirect |
i32.rotl | i64.rotl | f32.min | f64.min | f64.load | ||
i32.rotr | i64.rotr | f32.max | f64.max | |||
i32.clz | i64.clz | nop | grow_memory | |||
i32.ctz | i64.ctz | block | current_memory | |||
i32.popcnt | i64.popcnt | f32.demote/f64 | loop | |||
i32.eqz | i64.eqz | f32.convert_s/i32 | if | get_local | ||
i32.eq | i64.eq | f32.convert_s/i64 | else | set_local | ||
i32.ne | i64.ne | f32.convert_u/i32 | br | tee_local | ||
i32.lt_s | i64.lt_s | f32.convert_u/i64 | br_if | |||
i32.le_s | i64.le_s | f32.reinterpret/i32 | br_table | get_global | ||
i32.lt_u | i64.lt_u | f32.eq | f64.eq | f64.promote/f32 | return | set_global |
i32.le_u | i64.le_u | f32.ne | f64.ne | f64.convert_s/i32 | end | |
i32.gt_s | i64.gt_s | f32.lt | f64.lt | f64.convert_s/i64 | i32.const | |
i32.ge_s | i64.ge_s | f32.le | f64.le | f64.convert_u/i32 | drop | i64.const |
i32.gt_u | i64.gt_u | f32.gt | f64.gt | f64.convert_u/i64 | select | f32.const |
i32.ge_u | i64.ge_u | f32.ge | f64.ge | f64.reinterpret/i64 | unreachable | f64.const |
这些指令设计上是低级的,正是这种低级特性赋予了 WebAssembly 强大的能力。WebAssembly 作为编译目标而诞生,由编译器工程师塑造而成。
OMG! BBQ!
WebKit 的 WebAssembly 实现,就像我们的 JavaScript 实现一样,使用分层系统来平衡启动成本和吞吐量。目前,引擎有两个层级:快速构建字节码(BBQ)层和优化机器码生成器(OMG)层。两者都依赖B3 JIT作为其低级优化器。
WebAssembly 模块可以轻松包含大量代码,其中一些代码可能只执行一次或不常执行。这就是为什么我们选择使用两个层级:一个快速生成尚可的代码,另一个只在引擎认为代码足够“热”以值得优化时才生成优化后的代码。BBQ 编译代码的速度大约是 OMG 的 4 倍,但生成的代码执行速度大约是 OMG 的 2 倍。当使用 OMG 编译函数时,我们使用一个后台线程。当 OMG 编译完成后,我们会暂停正在执行的 WebAssembly 线程,并将 OMG 编译结果热补丁到模块中。
WebAssembly 是一种低级格式——与 JavaScript 这种动态语言相比——这意味着在编译 WebAssembly 时,事情不会那么变化无常。例如,WebAssembly 静态地告诉我们函数内所有值的类型,并提前给出所有函数的签名。
BBQ 🔥
为了尽快生成可执行代码,BBQ 层省略了 B3 编译器中许多可能的优化。此外,BBQ 层还使用线性扫描结合寄存器/栈分配算法。这比 B3 通常使用的图着色算法分配寄存器快约 11 倍。避免昂贵的优化使 WebKit 能够快速生成 BBQ,以便 BBQ 可以尽快被使用。
OMG 😲
当一个函数执行足够多次时,我们的 WebAssembly 运行时会决定优化该函数。由于 WebAssembly 不需要任何类型推测,我们只使用分层来节省编译时间。BBQ 代码只包含检测代码何时多次执行所需的性能分析信息。
我们在 worker 之间共享模块及其所有运行时状态(例如从 BBQ 升级到 OMG)。由于我们对 BBQ 代码进行热补丁,而这些代码可能在任意数量的线程上执行,因此我们需要确保每个调用点都可以并发更新以指向 OMG 代码。为了避免像 JavaScript 那样在每次函数调用时检查新代码,我们直接和间接跟踪每个函数的每个调用点。每当函数的 OMG 版本编译完成后,它就会用指向该代码的指针替换每个调用点。
内存信号 🦇
WebAssembly 中最重要的优化之一是减少内存访问开销,同时保留安全保障。WebAssembly 规定所有内存访问都在单个 32 位线性内存上执行。我们通过预留略多于 4GiB 的虚拟地址空间来实现这一点。这种虚拟预留不会占用物理内存,除非被访问。我们使用硬件的页保护机制将除低页之外的所有页面标记为不可读和不可写。从这个 32 位地址空间进行的所有加载和存储操作都可以正常执行,无需显式边界检查指令。如果加载或存储操作超出边界,它将触发硬件故障,最终导致 POSIX SIGSEGV
或 Mach EXC_BAD_ACCESS
异常。我们的自定义信号/Mach 异常处理程序随后会确保故障源自 WebAssembly 内存操作。如果是,我们将指令指针设置为一个特殊代码存根,该存根执行 WebAssembly 陷阱并在 JavaScript 中生成一个 WebAssembly.RuntimeError
。这种优化意味着一个 WebAssembly 内存操作通常会产生单个加载或存储指令。总体而言,我们通过这项优化在各种 WebAssembly 基准测试中测量到了 15-20% 的速度提升。

WebAssembly 内存信号