Wasmtime 47 默认开启 Wasm GC 和异常支持,高级语言编译到 WASM 不用自己 GC 了
之前用 Java、Kotlin、Dart、C# 这类带 GC 的语言编译到 WASM 的时候,得把自己的垃圾回收器整个塞进 .wasm 二进制里。结果就是产物又大又慢,而且很多原生 GC 实现里常用的优化手段(stack map、stack walking 找 GC root 之类的)在 WASM 里根本用不了。
Wasm GC 提案就是来解决这个问题的——直接在 WASM 层面支持 struct、array 类型定义和子类型关系,对象生命周期交给运行时管,工具链不用再自己实现 GC 了。
举个例子,WAT 里定义一个二叉树节点长这样:
(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 创建,struct.get / struct.set 读写字段,非常直观。
异常提案也一起开了
以前没有原生异常支持,工具链得自己搞一套 calling convention,每次函数调用都要额外返回"是正常返回还是抛异常了",每个 call site 都要判断分支。又臃肿又慢。
现在有了 throw 和 try/catch 原语,运行时可以用经典的 unwinding 实现,正常路径零开销。
Wasmtime 的 GC 实现细节(这部分比较硬核)
用的是 Cheney 半空间拷贝回收器,GC 堆分两半,活跃空间 bump pointer 分配,回收时把活对象拷到另一半,更新所有 GC root。不需要 read/write barrier,实现很简洁。
比较有意思的设计:GC 堆底层复用了 WASM linear memory。对象引用不是原生指针,而是 32-bit 的堆内索引。好处是:
- 安全性:就算 collector 有 bug 把堆搞坏了,恶意 WASM 程序也逃不出沙箱,碰不到宿主内存
- 性能:可以用虚拟内存 guard page 省掉显式边界检查;32-bit 引用比 64-bit 指针更省 cache;跟 pooling allocator 集成后实例化时间还是保持在 5μs
- 可移植性:linear memory 那套跨平台(包括 bare metal)的内存管理逻辑直接复用,不用重新造轮子
测试方面,除了扩展 wasm-smith 支持 GC 提案做模糊测试,还额外写了两个 fuzzer:一个专门生成各种复杂的对象图和子类型关系,另一个专门检测 collector bug 或编译器误优化导致的堆损坏。
性能方面先打个预防针
官方说了,目前主要精力放在正确性上,性能还没怎么优化。跟 V8、SpiderMonkey 那些打磨了几十年的 GC 比,吞吐和延迟肯定还有差距。而且 Wasmtime 的设计目标场景是大量短生命周期的小实例(每个实例处理几个任务就扔掉),不是那种单个长驻服务进程的场景。
后续计划
- 编译器层面:把 GC 类型信息接入 alias analysis,做更激进的 store-to-load forwarding 和冗余 load 消除
- 功能层面:GC + Component Model 集成,让带 GC 的语言在组件生态里成为一等公民,不用再为了跨组件传数据而维护一个没用的 linear memory