WebAssembly做矩阵运算为什么比JavaScript还慢?

UE丶沐希 阅读 39

我用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
}
我来解答 赞 11 收藏
二维码
手机扫码查看
2 条解答
UI瑞丹
UI瑞丹 Lv1
问题出在矩阵乘法的内存访问模式上。你现在的实现方式导致了大量的cache miss,这对性能影响特别大。

先说说改进方案:把B矩阵转置一下再计算。这样能让内存访问更连续,充分利用CPU缓存。具体来说就是让k循环和j循环互换位置:

#[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 k in 0..n {
let a_ik = a[i * n + k];
for j in 0..n {
c[i * n + j] += a_ik * b[k + j * n]; // 转置后的索引
}
}
}
c
}


另外记得把结果数组预先分配好,避免动态扩容带来的开销。还有个小技巧是把内层循环里的乘法结果缓存起来,减少重复计算。

至于为什么比JS慢,很可能是因为WASM模块初始化和调用开销太大了。对于这种小规模计算,这个开销占比会很明显。如果矩阵规模更大些,性能差距就会体现出来了。

最后提醒一句,别忘了开启编译器优化,加个 opt-level = "s" 或者 "z" 看看效果。折腾这些性能优化真挺累的,但看到性能提升那一刻还挺有成就感的。
点赞
2026-03-31 22:24
Mc.海利
Mc.海利 Lv1
问题很典型,你这个情况我见过很多次了。

核心问题在于你返回了Vec,这意味着每次调用都要把WASM内存里的数据完整拷贝到JS侧。100x100的矩阵就是8万字节的数据复制,这个拷贝开销比实际计算还大。

正确的做法是预先在JS侧分配好输出数组,传给WASM函数直接写入:

#[wasm_bindgen]
pub fn matmul(a: &[f64], b: &[f64], c: &mut [f64], n: usize) {
for i in 0..n {
for j in 0..n {
let mut sum = 0.0;
for k in 0..n {
sum += a[i * n + k] * b[k * n + j];
}
c[i * n + j] = sum;
}
}
}


JS那边这样调用:

const c = new Float64Array(n * n);
wasm.matmul(a, b, c, n);
// c 就是结果,不需要拷贝


另外还有几个优化点:

第一,你的Rust代码是最朴素的三重循环,内存访问模式很差。矩阵乘法的标准优化是考虑缓存局部性,但这个属于进阶优化,先把数据拷贝问题解决了再考虑。

第二,如果追求极致性能,可以考虑用crate做循环展开或者SIMD优化,不过100x100的规模可能犯不上。

第三,wasm-bindgen默认的优化级别可能不够,编译时加opt-level = 3lto = true能提升明显。

你先试试改成传参写入的方式,性能应该能上去。
点赞
2026-03-17 00:04