Wasmtime 47 默认启用 GC 和异常处理,推动更多语言运行在 WebAssembly 上
Wasm GC 和异常处理提案在今天的 Wasmtime 47 版本中均已默认启用!我们很高兴能够帮助更多语言运行在 WebAssembly 以及 Wasmtime 支持的任何地方。走到这一步,Wasmtime 经历了大规模改动,也是多年工程努力的结晶。
Wasmtime
Wasmtime 是一个快速、安全且可移植的 WebAssembly 运行时。它独立、轻量,易于嵌入。Wasmtime 的维护者致力于开放标准,并积极参与 Wasm 标准化工作。
Wasm GC
最初,在 WebAssembly 的早期版本中,采用对象和引用数据模型(而非原始指针和内存数据模型)的高级语言,不得不将自己的垃圾回收器嵌入到 .wasm 二进制文件中。这导致 .wasm 文件臃肿,并且许多原生代码中常用的垃圾回收器实现技术(例如使用 栈映射 和栈遍历来识别 GC 根)都无法使用。不幸的是,许多语言都属于这种情况。
Wasm GC 提案改善了这种情况,为 WebAssembly 添加了对这些高级语言的高效支持。1 它扩展了 WebAssembly 语言,允许 Wasm 程序定义自己的 struct 和 array 类型以及子类型关系。Wasm 程序无需担心管理这些类型实例的生命周期或手动释放它们;运行时负责所有这一切。因此,这些编程语言不再需要嵌入自己的垃圾回收器,而是可以利用 WebAssembly 运行时的回收器。这为更多语言轻松高效地编译到 WebAssembly 打开了大门。
作为示例,下面展示了一个 Wasm 程序如何定义二叉树的节点类型:
(rec
(type $node (struct
(field $key (mut f64))
(field $left (mut (ref null $node)))
(field $right (mut (ref null $node)))
(field $value (mut (ref null $payload)))
))
)
新实例通过struct.new $node创建,字段访问则通过struct.get $node $key或struct.set $node $left等指令完成。
Wasm 异常处理
Wasm 的异常提案对使用异常的语言的目标,与 Wasm GC 提案对面向对象和引用语言的目标类似:旨在让 WebAssembly 高效支持异常,使其成为带异常语言更好的编译目标。2 如果没有这个提案,工具链就需要实现自定义调用约定,不仅要返回函数结果,还要返回函数是正常返回还是抛出了异常。每个调用点都必须检查这个条件并做相应分支,这既增加了 .wasm 二进制体积,也给常见的正常返回路径带来了运行时开销。而有了异常提案,这一切都不复存在,取而代之的是 throw 和 try/catch 风格的构造。WebAssembly 运行时可以自由地采用其展开方式来实现这些构造,对常见的正常返回调用路径零开销,从而带来更快的执行速度和更小的 .wasm 二进制文件。
Wasmtime 的 GC 实现
Wasmtime 使用了一个简单的 Cheney 风格半空间复制收集器。GC 堆被分成两半:活跃半空间(用于分配新对象)和空闲半空间。回收时,存活对象从空闲半空间(即之前的活跃空间)复制到新的活跃空间,所有 GC 根(例如 Wasm 栈帧中的活跃引用)都被更新指向新位置。分配只是在活跃半空间内移动一个指针,无需任何读写屏障。 我们在底层复用 WebAssembly 线性内存来实现 GC 堆并对其进行沙箱化。GC 对象的引用不是原生指针,而是指向 GC 堆底层线性内存的 32 位索引。WebAssembly 承诺快速、安全、可移植,但真正承担这些责任的必须是运行时。复用线性内存作为 GC 堆在这三个方面都有好处。最明显的是深度防御安全:即便收集器出现 bug 破坏了 GC 堆,恶意 Wasm 程序也无法逃出沙箱访问宿主内存。在速度方面,这让我们可以使用虚拟内存守护页来省略显式边界检查,就像对线性内存做的那样;与池化实例分配器紧密集成,确保 5 微秒的实例化时间;在 64 位机器上,32 位 GC 引用比 64 位指针更紧凑,能更高效地利用 CPU 缓存。最后,跨多个平台(包括裸机)快速分配、释放和重置大块内存,每个平台的能力都略有不同,这涉及大量特殊处理。我们现有的线性内存实现已经跨平台可移植,并且已经处理了这些特殊情况,因此在线性内存之上构建 GC 堆,我们也免费获得了可移植的 GC 堆。 为了进一步验证垃圾回收器的正确性,我们扩展了模糊测试基础设施,对Wasm GC进行高强度测试。首先,我们扩展了wasm-smith以支持GC提案。理论上,只要给足时间,它就能生成几乎任意使用GC的Wasm程序3。但这可能需要非常长的时间,因此我们又补充了两个模糊测试器:
1. 一个用于测试各种有趣且任意的对象图、类型引用和子类型关系。
2. 另一个用于检测因收集器bug或编译器优化错误导致的堆损坏。
最后,关于性能的一个小提示:目前我们主要将工程精力集中在收集器的正确性上,而非性能。它是一个全新的系统,没有像V8和SpiderMonkey中的收集器那样经过数十年的性能优化。今天,我们的收集器在吞吐量和延迟上还无法与它们匹敌。另外,我们设计收集器及其权衡时,主要面向的是Wasmtime在生产中最常用的场景:创建大量小型、一次性使用的Wasm实例,每个实例处理少量任务后,其GC堆就会被丢弃。系统首先考虑的是跨多个实例的水平扩展,而不是将单实例性能置于首位。这与一个长期运行的单一服务器进程(生命周期无限)不同,因此收集器的调优方式也会不同。
下一步计划
性能优化工作还有很多要做。例如,我们目前正在将 GC 类型信息引入编译器的别名分析优化(如存储转发和冗余加载消除)中。一旦确定两个类型永远不会别名(即它们不可能占据同一内存位置),我们就可以更激进地应用这些优化。
从功能角度看,下一个重要里程碑是,在延迟值降低的基础上,原型化GC 与组件模型的集成。这项工作将提升垃圾回收语言在组件生态系统中的地位,使其成为一等公民,因为它们不再需要为了跨组件传递数据而使用原本用不到的线性内存。
结论
我们很高兴达成这一里程碑!快来试试 Wasmtime 最新启用的 GC 和异常支持,并告诉我们你的体验。
-
注意,GC 提案已合并到主 WebAssembly 规范中,因此提案页面现在是某个时间点的存档快照。↩
-
与 GC 提案类似,异常提案也已合并到主 WebAssembly 规范中,提案仓库现在是存档的历史快照。↩
-
wasm-smith目前对于 Wasm GC 唯一的盲点是它永远不会生成非空引用。↩