Wasmi 2.0 - 工程实现最快的Wasm解释器
摘要
Wasmi 2.0,一款WebAssembly解释器,已发布,带来了显著的性能提升,运行速度比1.0版本快约2.2倍,还新增了减小二进制大小和稳定燃料计量等功能。
<p><a href="https://lobste.rs/s/sccfuu/wasmi_2_0_engineering_fastest_wasm">评论</a></p>
查看缓存全文
缓存时间: 2026/09/01 15:41
# Wasmi 2.0 - 速度最快的Wasm解释器工程揭秘
来源:https://wasmi-labs.github.io/blog/posts/wasmi-v2.0/
在我上一篇关于 Wasmi 1.0 的文章 (https://wasmi-labs.github.io/blog/posts/wasmi-v1.0/) 中,我承诺将为未来的 Wasmi 版本进行一次彻底的引擎革新 (https://wasmi-labs.github.io/blog/posts/wasmi-v1.0/#the-next-gen-engine)。未来已至!Wasmi 是一个高效且功能丰富的 WebAssembly (Wasm) (https://webassembly.org/) 解释器。它是物联网设备、插件系统 (Typst (https://typst.app/docs/reference/foundations/plugin/)、Zellij (https://github.com/zellij-org/zellij)、Josh (https://github.com/josh-project/josh))、云主机、智能合约 (Soroban (https://stellar.org/soroban)、Ripple (https://ripple.com/)) 甚至轻量级游戏主机 (Firefly Zero (https://fireflyzero.com/)) 的绝佳选择。
在深入细节之前,非常感谢 **Stellar Development Foundation (SDF) (https://stellar.org/foundation)**,他们自2024年10月以来一直赞助 Wasmi 项目。没有他们的赞助,Wasmi 项目就不可能达到今天的成就。同时特别感谢 **Felix Kutzner (https://github.com/fkutzner)** 审阅了本文并提出了许多改进建议。
## Wasmi 2.0 发布
今天,我很高兴地宣布,经过八个月的专注工作,Wasmi 2.0 终于完成并可以使用了。此版本专注于执行性能:在 Apple M2 Pro 上,使用 `wasmi-benchmarks` (https://github.com/wasmi-labs/wasmi-benchmarks) 测试套件,Wasmi 2.0 的几何平均性能比 Wasmi 1.0 快约 2.2 倍。Wasmi 2.0 还引入了新的配置项,例如 `validate` crate 特性,显著减少了其二进制产物大小。2 (https://wasmi-labs.github.io/blog/posts/wasmi-v2.0/#fn:2) 一些用户请求的功能,例如稳定的燃料计量3 (https://wasmi-labs.github.io/blog/posts/wasmi-v2.0/#fn:3)、对 WebAssembly 确定性配置文件 (https://github.com/WebAssembly/profiles/blob/main/proposals/profiles/Overview.md) 的支持,以及改进的 Wasmi CLI (https://crates.io/crates/wasmi_cli) 工具,也已纳入此版本。
- **查看更新日志** (https://github.com/wasmi-labs/wasmi/releases/tag/v2.0.0)
- **查看迁移指南:1.0 → 2.0** (https://github.com/wasmi-labs/wasmi/tree/v2.0.0/docs/migration-v1-to-v2.md)
- **查看 Crate** (https://crates.io/crates/wasmi/2.0.0)
- **查看文档** (https://docs.rs/wasmi/2.0.0/wasmi/)
## Wasmi 2.0 的表现
比 Wasmi 1.0 快 2.2 倍很棒,但 Wasmi 2.0 相比其竞争对手表现如何?为此,我将 Wasmi 2.0 与一些最快的便携式 Wasm 解释器进行了基准测试对比:4 (https://wasmi-labs.github.io/blog/posts/wasmi-v2.0/#fn:4)
- Wasm3 (https://github.com/wasm3/wasm3)
- WAMR fast-interpreter (https://github.com/wasm-micro-runtime/wasm-micro-runtime)
- Wasmtime Pulley (https://github.com/bytecodealliance/wasmtime/blob/main/pulley/README.md)
- Makepad Stitch (https://github.com/makepad/stitch)
- Wasmi 1.0 (https://crates.io/crates/wasmi/1.0.9)
> **注意:** Wasmi 2.0 的灵感来源于以上所有解释器!
基准测试使用的是 `wasmi-benchmarks` (https://github.com/wasmi-labs/wasmi-benchmarks) 项目,这应该可以让你在自己的机器上轻松复现这些测试。我在三种不同的硬件配置上运行了基准测试,以清晰展示解释器的性能差异:
### Apple M2 Pro
### AMD EPYC 7763
### Intel Xeon Platinum 8370C
请注意,这仅仅是 `wasmi-benchmarks` (https://github.com/wasmi-labs/wasmi-benchmarks) 项目支持的全部基准测试和运行时间的一瞥,但它提供了一个很好的概览。
### 几何平均值
以下两张图表展示了上述 Wasm 运行时在 `wasmi-benchmarks` (https://github.com/wasmi-labs/wasmi-benchmarks) 测试套件中所有 `execute` 和所有 `startup` 基准测试的几何平均值 (https://en.wikipedia.org/wiki/Geometric_mean)。
> **注意:** 我不得不为 `startup` 使用对数缩放,因为 Wasmtime Pulley 是一个显著的异常值。5 (https://wasmi-labs.github.io/blog/posts/wasmi-v2.0/#fn:5) 尽管 Wasmi 2.0 专注于执行性能,其启动性能仍然非常出色,并且与之前的版本基本持平。6 (https://wasmi-labs.github.io/blog/posts/wasmi-v2.0/#fn:6)
### 基准测试结论
可以说,Wasmi 2.0 明确属于最快便携式 Wasm 解释器的类别。在后续文章中,我将展示 `wasmi-benchmarks` (https://github.com/wasmi-labs/wasmi-benchmarks) 测试套件的所有结果和发现,并让其众多支持的 Wasm 运行时获得应有的关注。
## Wasmi 2.0 为何如此之快?
Wasm3 和 Stitch 在底层与 Wasmi 2.0 有很多相似之处。虽然本节详述了哪些想法被融入了 Wasmi 2.0,但在相关处也会讨论与它们的相似之处和刻意的区别。
> **注意:** 本节假定读者具备 Wasm 和解释器的基本理解。
### 指令分发的新模式
正如在最初的 Wasmi 1.0 博客文章中所承诺的,Wasmi 2.0 现在拥有四种不同的指令分发模式:
| 模式 | 描述 | Crate 特性 |
|------|------|------------|
| **直接线程化代码** | 最快的配置,Wasm3 和 Stitch 都使用了它。它将函数指针直接嵌入解释器的内部 IR 中,并使用尾调用从一个指令处理器跳转到下一个。 | - |
| **间接线程化代码** | 与直接线程化代码非常相似,但将操作码嵌入内部 IR,并在分发时使用跳转表将操作码映射到其指令处理器的函数指针。比直接线程化代码慢约 10-15%,但占用的 IR 内存显著减少。 | `indirect-dispatch` |
| **Switch-循环** | 这是 Wasmi 1.0 中使用的技术。它是使用循环和 switch(或 match)构建解释器的朴素方式。不幸的是,它浪费了大量性能,尤其是在 Apple Silicon 上。 | `portable-dispatch` + `indirect-dispatch` |
| **Call-循环** | 这是在循环中调用下一个指令处理器,而不使用尾调用。不幸的是,它非常慢且内存效率低下,因此我不推荐使用它。它之所以存在,仅仅是因为 `portable-dispatch` 和 `indirect-dispatch` 是独立的 crate 特性,这种组合只是配置矩阵的产物。 | `portable-dispatch` |
Wasmi 用户应该使用:
- **直接线程化代码:** 如果希望最大化解释器性能。
- **间接线程化代码:** 在解释器性能和内存使用之间取得良好平衡。
- **Switch-循环:** 用于不支持尾调用的平台。
Wasmi 2.0 提供了 `auto-dispatch` crate 特性,可在可能的情况下自动使用基于线程化代码 (https://en.wikipedia.org/wiki/Threaded_code) 的配置。7 (https://wasmi-labs.github.io/blog/posts/wasmi-v2.0/#fn:7)
#### 指令分发模式的性能如何?
> **注意:** 直接线程化代码的 CoreMark 结果与上面不完全匹配,因为那是另一次运行,并且我们使用了 `wasm-coremark-rs` (https://github.com/wasmi-labs/wasm-coremark-rs) 项目。
尽管性能差异极大,但所有这些指令分发模式在底层共享相同的解释器执行逻辑和架构。如果你对 Wasmi 中指令分发选择的详细工作方式感兴趣,可以在此找到代码:Wasmi Dispatch Selection (https://github.com/wasmi-labs/wasmi/tree/v2.0.0/crates/wasmi/src/engine/executor/handler/dispatch)
### 执行处理器签名
在执行之前,Wasmi 将 Wasm 字节码翻译成 Wasmi IR。每条 Wasmi IR 指令都有自己的指令处理器(或执行处理器),定义了如何执行该指令。在 Wasmi 2.0 中,所有指令处理器共享相同的签名:
```rust
fn(
store: &mut PrunedStore, // 与执行关联的 `Store` 的引用。
ip: Ip, // 指令指针。
sp: Sp, // 栈指针。
mem0: Mem0Ptr, // 指向默认线性内存 `(memory 0)` 数据的指针。
mem0_len: Mem0Len, // 默认线性内存的字节数。
instance: Inst, // 指向当前执行函数所使用的 Wasm 实例的指针。
ireg: Ireg, // 用于整数和引用值的累加器寄存器。
freg32: Freg32, // 用于 `f32` 值的累加器寄存器。
freg64: Freg64, // 用于 `f64` 值的累加器寄存器。
) -> Done; // 用于发出陷阱或成功停止信号的状态。
```
- `store` 参数基本上是一个按其 `T` 类型“裁剪”过的 `Store`。这很重要,因为指令处理器不能是泛型的。`store` 用于燃料计量、主机调用、`memory.grow` 和 `table.grow` 操作。
- `ip` 参数是指令指针,告诉执行器它在编码指令流中的位置以及它需要解码和执行哪条指令。
- `sp` 参数是当前执行的函数在值栈中的位置。
- `mem0` 和 `mem0_len` 参数用于优化对默认内存 `(memory 0)` 的访问。这在 Wasm 中非常常见,即使使用 Wasm `multi-memory` 提案也是如此。
- `instance` 参数用于加载与 Wasm 实例相关的对象,如全局变量、函数、表、内存、数据段和元素段。我们将在后文详细讨论。
- `ireg`、`freg32` 和 `freg64` 参数是所谓的累加器寄存器,用于在指令之间高效地存储中间结果。我们将在后文详细讨论。
- `Done` 结果只是一个比特模式,告诉执行器为什么停止执行。更详细的信息通过 `store` 传递,以便后续检索。
#### 问题:调用约定
Wasmi 指令处理器中的 9 个参数中有 7 个需要在通用寄存器 (GPR) 中传递其值,即 `store`、`ip`、`sp`、`mem0`、`mem0_len`、`instance` 和 `ireg`。然而,像 `sysv64` 这样的常见调用约定只为整数参数提供了最多 6 个 GPR。第 7 个整数参数会破坏性能,因为它必须在每次分发时溢出到栈上。
Stitch 和 Wasm3 通过分别只使用 6 个和 4 个 GPR 来规避这个问题。简单的解决方案是在必要时将 Wasmi 的一个 GPR 参数转换为浮点值。选择 `instance` 参数是因为它本来就只用于相对昂贵的操作。基准测试表明,整数到浮点的寄存器域移动并非大问题。
> **注意:** 目前不稳定的 `preserve_none` (https://github.com/rust-lang/rust/issues/151401) ABI 未来一旦稳定并在更多平台上可用,可能会改善这种情况。
### 累加器寄存器
#### Wasmi 1.0 的工作原理
Wasmi 1.0 广泛使用栈偏移(栈槽)作为 IR 指令的操作数和结果。下面是一个简化的 `i64.add` 指令处理器,计算 `res = lhs + rhs`,其中 `res` 和 `lhs` 是栈槽,`rhs` 是一个立即数 `i64` 值:
```rust
fn i64_add(ip: Ip, sp: Sp, ..) -> Done {
let res: Slot = decode_slot(ip);
let lhs: Slot = decode_slot(ip);
let rhs: i64 = decode_i64(ip);
let lhs: i64 = lhs.load(sp);
let sum: i64 = lhs + rhs;
res.store(sp, sum);
ip.offset(encode_size::);
next!(ip, sp, ..)
}
```
1. 从 `ip` 解码结果 `Slot`。
2. 从 `ip` 解码左侧操作数 `lhs` 的 `Slot`。
3. 从 `ip` 解码右侧操作数 `rhs: i64`。
4. 从 `lhs` 加载值 (`sp[lhs]`)。
5. 计算总和 `sp[lhs] + rhs`。
6. 将总和存入 `sp[result]`。
7. 偏移 `ip` 以指向下一条指令处理器。
8. 执行下一条指令处理器。8 (https://wasmi-labs.github.io/blog/posts/wasmi-v2.0/#fn:8)
#### Wasmi 2.0 的工作原理
Wasmi 2.0 引入了三个新的累加器寄存器:`ireg`、`freg32` 和 `freg64`。这允许 Wasmi 2.0 从实际的硬件寄存器加载和存储指令操作数和结果。一个简化的 `i64.add` 示例,其中 `res` 和 `lhs` 引用 `ireg` 累加器,而 `rhs` 是一个 `i64` 立即值,看起来像这样:
```rust
fn i64_add(ip: Ip, sp: Sp, ireg: i64, ..) -> Done {
let rhs: i64 = decode_i64(ip);
ireg = ireg + rhs;
ip.offset(encode_size::);
next!(ip, ireg, ..)
}
```
1. 从 `ip` 解码右侧操作数 `rhs: i64`。
2. 计算总和 `ireg + rhs`。
3. 将总和存入 `ireg`。
4. 偏移 `ip` 以指向下一条指令处理器。
5. 执行下一条指令处理器。
这明显更简单、更高效,因为累加器寄存器始终是隐式的,无需进行昂贵的解码、加载或存储值。这是用 `x1 = ip` 和 `x6 = ireg` 编译为 `aarch64` 汇编的样子:
```assembly
i64_add_rri:
ldr x7, [x1, #16]! ; 将 ip 增加 16 字节并加载下一个处理器
ldur x8, [x1, #-8] ; 从 ip 获取 rhs 立即操作数
add x6, x8, x6 ; ireg = ireg + rhs
br x7 ; 尾调用下一个处理器
```
### 复制指令
Wasmi 2.0 中的指令通常将结果存储到隐式累加器寄存器中。这种设计的缺点是需要旧设计不需要的复制指令。
#### 示例:`local.set` 与 `local.tee`
```wasm
local.get 0 ;; 将 (local 0) 压栈
i32.const 10 ;; 将 10 压栈
i32.add ;; 弹出 2 个操作数,将它们的和压栈
local.set 1 ;; 弹出和,存入 (local 1)
```
这段 Wasm 序列将 `(local 0) + 10` 相加并将结果存入 `(local 1)`。Wasmi 1.0 可以在一条 Wasmi 1.0 IR 指令中完成所有这些工作:
> **注意:** 我们使用后缀 `s` 表示栈槽,`r` 表示累加器寄存器,`i` 表示立即数。
Wasmi 2.0 需要两条指令来完成相同的工作:
```wasm
i32_add_rsi 0 10 ;; ireg = (local 0) + 10
u64_copy_sr 1 ;; (local 1) = ireg
```
#### 示例:寄存器保持
```wasm
local.get 0 ;; 将 (local 0) 压栈
i32.const 10 ;; 将 10 压栈
i32.add ;; 弹出 2 个操作数,将它们的和压栈
local.get 1 ;; 将 (local 1) 压栈到待定和之上
i32.const 20 ;; 将 20 压栈
i32.mul ;; 弹出 2 个操作数,将它们的积压栈
```
Wasmi 2.0 必须将其翻译为大致如下的 Wasmi 2.0 IR:
```wasm
i32_add_rsi 0 10 ;; ireg = (local 0) + 10
u64_copy_sr A ;; (slot A) = ireg
i32_mul_rsi 1 20 ;; ireg = (local 1) * 20
```
`u64_copy_sr A` 指令是必需的,因为我们将在不实际使用它的下一条指令中覆盖 `ireg`,因此我们需要保持 `ireg` 的先前值。
#### 解决方案 1:更高效的复制
如前所示,由于这种设计,Wasmi 2.0 需要多得多的复制指令。有一些有效的方法可以减少它们的数量,并且对于剩下的复制,出现了一些模式。Wasmi 2.0 针对常见情况引入了优化的 IR 指令:
- `u64_copy_sNr`:将 `ireg` 复制到固定的 `(local N)`,其中 `N = 0..10`
- `f32_copy_sNr`:将 `freg32` 复制到固定的 `(local N)`,其中 `N = 0..10`
- `f64_copy_sNr`:将 `freg64` 复制到固定的 `(local N)`,其中 `N = 0..10`
- `u64_copy_sNsM`:将 `(local M)` 复制到 `(local N)`,其中 `N,M = 0..5` 且 `N != M`
- 针对 `freg32` 和 `freg64` 的变体不需要,因为栈槽始终被视为通用的 64 位模式。
#### 解决方案 2:操作码融合
出于某种原因,Wasm 生成大量 `add` 和 `load` 指令,其后立即跟随着 `local.set` 或 `local.tee`。这种情况非常普遍,因此值得引入特殊的融合变体,这些变体不仅将其结果存储在累加器寄存器中,还存储到栈槽中,以便 Wasmi 2.0 可以用一条 IR 指令表示这些用例。
### 跨控制流边界的累加器
对于控制流,WebAssembly 使用 `block`、`loop` 和 `if` 帧。可以使用 `br`(分支)、`br_if`(条件分支)或 `br_table`(分支表)指令跳转到 `block` 或 `if` 的末尾,或者继续一个 `loop`。WebAssembly 中的控制流是组织为...(后续内容接续)
相似文章
SpaceWASM:NASA/JPL 用于航天器序列化的 WebAssembly 解释器
SpaceWASM 是一个来自 NASA/JPL 的开源 WebAssembly 解释器,专为航天器机载序列化设计,注重确定性内存使用和在资源受限环境下的流式处理能力。
2026 年 WebAssembly 运行时性能
本文使用 libsodium 加密库对多种 WebAssembly 运行时(WAVM、WasmEdge、WAMR、wasm2c、Wasmer、Wasmtime、Wazero、Node、Bun)进行了性能基准测试,比较了 2024、2025 和 2026 年的版本。结果显示,WAVM、WasmEdge(AOT)、WAMR(AOT)、wasm2c、Wasmer 和 Wasmtime 在 CPU 密集型加密任务中实现了接近原生的性能,而 wide_arithmetic 指令对加密代码有益。
WATaBoy:将Game Boy指令即时编译为Wasm,性能超越原生解释器
本文介绍了WATaBoy,一个Game Boy模拟器,它使用即时编译到WebAssembly的方式,实现了超越原生解释器的性能,是JIT到Wasm在模拟领域的一个概念验证。
WASI 0.3 发布
这篇文章宣布了WASI 0.3的发布,该版本通过组件模型将异步原语原生集成到WebAssembly组件中,简化了API并实现了更好的组件组合。
最好的WebAssembly运行时可能仍然是根本没有运行时
本文使用支持宽算术的wasm2c对WebAssembly-to-C方法进行了基准测试,证明其在速度和内存使用方面仍然与专门的WebAssembly运行时(如Wasmer和Wasmtime)具有竞争力。