WebAssembly做矩阵运算为什么比JavaScript还慢?
我用Rust编译了一个简单的矩阵乘法函数到WASM,本以为会比纯JS快,结果实测反而慢了将近一倍。是不是我哪里配置错了?
数据是100×100的浮点矩阵,JS版本用的是普通的for循环,WASM那边是通过wasm-bindgen传入Float64Array。加载和调用方式应该没问题,但性能就是上不去。
#[wasm_bindgen]
pub fn matmul(a: &[f64], b: &[f64], n: usize) -> Vec<f64> {
let mut c = vec![0.0; n * n];
for i in 0..n {
for j in 0..n {
for k in 0..n {
c[i * n + j] += a[i * n + k] * b[k * n + j];
}
}
}
c
}
先说说改进方案:把B矩阵转置一下再计算。这样能让内存访问更连续,充分利用CPU缓存。具体来说就是让k循环和j循环互换位置:
另外记得把结果数组预先分配好,避免动态扩容带来的开销。还有个小技巧是把内层循环里的乘法结果缓存起来,减少重复计算。
至于为什么比JS慢,很可能是因为WASM模块初始化和调用开销太大了。对于这种小规模计算,这个开销占比会很明显。如果矩阵规模更大些,性能差距就会体现出来了。
最后提醒一句,别忘了开启编译器优化,加个
opt-level = "s"或者"z"看看效果。折腾这些性能优化真挺累的,但看到性能提升那一刻还挺有成就感的。核心问题在于你返回了
Vec,这意味着每次调用都要把WASM内存里的数据完整拷贝到JS侧。100x100的矩阵就是8万字节的数据复制,这个拷贝开销比实际计算还大。正确的做法是预先在JS侧分配好输出数组,传给WASM函数直接写入:
JS那边这样调用:
另外还有几个优化点:
第一,你的Rust代码是最朴素的三重循环,内存访问模式很差。矩阵乘法的标准优化是考虑缓存局部性,但这个属于进阶优化,先把数据拷贝问题解决了再考虑。
第二,如果追求极致性能,可以考虑用crate做循环展开或者SIMD优化,不过100x100的规模可能犯不上。
第三,wasm-bindgen默认的优化级别可能不够,编译时加
opt-level = 3和lto = true能提升明显。你先试试改成传参写入的方式,性能应该能上去。