WebAssembly 里跑一次大整数运算比原生慢 2 到 7 倍——Firefox 155 用四条指令把 128 位整数补进了 Wasm
WebAssembly 里跑一次大整数运算,比原生慢 2 到 7 倍——这个数字在今天之前一直没人正面解决。Firefox 155 把宽位算术(Wide Arithmetic)提案默认打开了,Wasm 第一次有了原生的 128 位整数指令。如果你用 Rust、Zig、C 编译到 Wasm 做过加密、大数运算或者哈希,这次升级可能直接把性能曲线掰弯。
先说结论:这次加的是四条指令——i64.add128、i64.sub128、i64.mul_wide_s、i64.mul_wide_u。它们不改变你的源码,改的是编译器能生成什么机器码。
为什么以前慢
128 位整数在底层就是两个 64 位拼起来。问题出在 CPU 有专门的进位/溢出标志位(x86 的 adc/sbb,ARM 的 adc/sbc),原生代码能一条接一条地把两个 64 位加法串起来,几乎不浪费周期。
而 WebAssembly 的指令集里没有暴露溢出标志,也没法让编译器做指令融合。于是 LLVM 只能把一次 128 位加法展开成三条 add 加一堆比较;128 位乘法更惨,直接调 __multi3 这个 libcall,结果还得走内存往返一趟 ABI。
结果是:同样一段大整数代码,编译到 Wasm 后比原生慢 2-7 倍。加密库、bignum 库、PRNG 的内层循环全卡在这。
这次补了什么
四条指令的语义很直接:
i64.add128/i64.sub128:栈上是四个 i64(左右操作数各拆成低 64 位 + 高 64 位),返回两个 i64。i64.mul_wide_s/i64.mul_wide_u:两个 i64 相乘,返回带符号/无符号的 128 位结果(高低两半)。
关键不是”多了一个类型”,而是引擎现在能直接映射到原生的 adc、sbb、mul、umulh,让进位在硬件标志位里自然流动,不用再绕一圈 libcall。
看一个真实的例子。Rust 里想判断 u64 加法有没有溢出,以前得手动比较;现在编译器可以把 u64::overflowing_add 直接降成一条 i64.add128:
(module
(func $u64::overflowing_add (param i64 i64) (result i64 i64)
(i64.add128
(local.get 0) (i64.const 0) ;; 左操作数 lo/hi
(local.get 1) (i64.const 0) ;; 右操作数 lo/hi
)
)
)
返回值第二个 i64 就是进位标志,保证是 0 或 1。
数据说话
2026 年的 WebAssembly 运行时基准测试里,一组用 libsodium 编译的密码学负载给出了很清晰的对照:
- Wasmtime:不开启宽位算术时是原生的 2.41 倍慢,开启后降到 1.46 倍。
- Wasmer:从 2.08 倍降到 1.33 倍。
换算下来,光是把这一个提案打开,重度加密运算的 slowdown 就砍掉了大约 40%。这不是微优化,是数量级的经验修正。
现在能用吗
Firefox 155 是第一个默认开启的浏览器,全平台生效,首选项是 javascript.options.wasm_wide_arithmetic。另外两家:Blink(Chrome)和 WebKit(Safari)都已经实现,但还没发布。
也就是说,现在做 Wasm 加密或大数运算的项目,可以做渐进增强——运行时探测一下指令支持,有就走快路径,没有就退回原来的多指令展开。你不用等三个浏览器全齐。
你可以怎么落地
- 如果你的工具链是 Rust/Zig/C 编译到 Wasm,先确认 LLVM 版本是否已经会生成这四条指令(看反汇编里有没有
adc/mul融合,而不是__multi3libcall)。 - 在运行时用
WebAssembly.validate()探测一个含i64.add128的小模块,据此分支出快/慢两条构建。 - 重点看加密(RSA、ChaCha20)、大整数(BigInt 后端)、哈希表索引这三类热点循环,别的场景收益有限。
一个拖了好几年的性能债,这次是被一条指令级别的补丁填上的。值得花半小时,去看一眼你的 Wasm 热点函数反汇编里还有没有那个 __multi3。
评论区
登录后可评论。