为何将DOM对象作为externref传递给Wasm比通过JS值表传递速度更慢
基准测试背景
我制作了一个基准测试,用于衡量将DOM对象作为externref传递给Wasm函数时调用DOM API的速度。以下是被测试的函数(使用Rust编写,由rustc 1.55.0编译):
#[wasm_bindgen] pub fn append_and_remove(elem: web_sys::Element) { let doc = web_sys::window().unwrap().document().unwrap(); let child = &doc.create_element("br").unwrap(); elem.append_with_node_1(child).unwrap(); let _ = elem.remove_child(child).unwrap(); }
我对比了两个Wasm模块(及其JS包装层)和等效的JavaScript代码:其中“启用引用类型”版本使用wasm-bindgen --reference-types预处理,另一“未启用引用类型”版本仅使用wasm-bindgen预处理。
一百万次调用测试结果(Windows 10 64位环境)
| 浏览器 | 分组 | 耗时(毫秒) |
|---|---|---|
| Firefox 94.0.1 | 未启用引用类型 | 2167 |
| 启用引用类型 | 2687 | |
| 纯JavaScript实现 | 637 | |
| Chrome 95.0.4638.69 | 未启用引用类型 | 3432 |
| 启用引用类型 | 4129 | |
| 纯JavaScript实现 | 1039 | |
| Edge 95.0.1020.44 | 未启用引用类型 | 3416 |
| 启用引用类型 | 3858 | |
| 纯JavaScript实现 | 1187 |
问题解答
三款浏览器中未启用引用类型的版本性能反而更高,核心原因有三个:
- 引用类型的跨边界校验开销未被充分优化
未启用引用类型时,wasm-bindgen会把所有DOM对象存在JS侧的全局索引表中,传给Wasm的只是整数索引,Wasm侧完全按普通数值处理,跨边界时不需要额外类型校验,这条路径已经被浏览器引擎优化了多年,性能非常成熟。
启用引用类型后,Wasm栈直接存储JS对象引用,每次跨Wasm/JS边界调用时,引擎都需要额外做引用合法性校验:包括空值判断、类型约束校验、GC标记同步等,你测试所用的2021年发布的9x版本Chrome/Firefox/Edge对这部分逻辑的JIT优化还不到位,直接带来了显著的额外开销。 wasm-bindgen引用类型的代码生成未做极致优化
你看到的只是外层包装代码量减少,但底层的参数处理逻辑反而多了额外开销:未启用引用类型时整数参数可以直接透传,启用引用类型后,每次传递对象进Wasm、或者从Wasm返回对象时,都要做额外的引用持有/释放标记,避免对象被GC误回收,这部分逻辑也拉高了耗时。- 测试场景放大了边界开销的影响
你的测试函数本身逻辑极轻,绝大多数耗时都集中在Wasm和JS的跨边界调用上,所以边界调用的额外开销占比被完全放大。如果是计算密集型、跨边界调用频次低的场景,引用类型省去的索引查表开销反而会体现出优势,只是在你这个高频跨边界调用的场景下,当前的校验开销盖过了它的收益。
目前浏览器对Wasm引用类型的优化还在持续迭代,后续随着引擎JIT对引用类型边界调用的优化到位,这个性能差距会逐步缩小甚至反转。
内容的提问来源于stack exchange,提问作者YAMAMOTO Yuji
相关产品推荐
相关产品推荐

