SIMD 加速对比(WebAssembly SIMD)

同一段 Rust 算法,普通标量版本和 128 位 SIMD 版本各跑一遍

正在检测 SIMD 支持…

每项先预热 1 次,再取 5 轮的中位数(很快的测试每轮重复多次取平均)  |  点结果列查看两边算出的具体数值

📖 原理说明

SIMD(单指令多数据)让一条 CPU 指令同时处理一组数据:一个 128 位寄存器可以装 4 个 f32、8 个 u16 或 16 个 u8,一次加法就把它们全部加完。WebAssembly 从 2021 年起在主流浏览器里支持 128 位 SIMD。这个页面把同样的三个算法各写两遍——普通标量 Rust 和用 core::arch::wasm32 显式写的 SIMD 版本——在浏览器里现场跑分,并逐项核对两边的结果。

🧮算法原理

[a₀ a₁ a₂ a₃] + [b₀ b₁ b₂ b₃] = [a₀+b₀ … a₃+b₃]一条 f32x4_add 指令完成 4 次加法
向量点积(f32 × 4)
每次从两个数组各读 4 个 f32,乘完累加到 4 个独立的部分和里,最后把 4 个部分和相加。求和顺序和标量版不同,浮点舍入误差也就不同:两者都和 f64 精确值比较,误差都在 1% 以内才算通过。有趣的是 SIMD 版往往更准——4 个部分和各自更小,每次累加损失的精度更少。
图像叠加(u8 × 16)
两张 RGBA 图片逐通道相加,超过 255 就截断为 255(饱和加法)。标量版每次处理 1 个字节并判断是否溢出;SIMD 版用 u8x16_add_sat 一条指令处理 16 个字节,溢出截断由硬件完成,所以加速最明显。校验和也用「两两相加并扩宽」的指令向量化计算。
曼德博集合(f32 × 4 像素)
相邻 4 个像素放进同一个寄存器一起迭代。难点是各像素逃逸的时刻不同:用比较指令得到一个「仍在迭代」的掩码,只给这些通道计数,全部逃逸才退出循环。4 个像素要等最慢的那个,所以实际加速低于 4 倍;边界越复杂的区域,相邻像素迭代次数差得越多,加速越少。运算顺序和标量版完全相同,所以迭代次数逐像素一致。
只给 SIMD 函数开启 simd128
如果整个 crate 用 -C target-feature=+simd128 编译,LLVM 会把「标量」循环也自动向量化,对比就失去意义。这里只在 SIMD 函数上标注 #[target_feature(enable = "simd128")],标量代码保持标量。
特性检测
只要 .wasm 里含有 SIMD 指令,不支持的浏览器连编译都会失败,没法在 WASM 里自己检测。所以页面先用 WebAssembly.validate 校验一个 31 字节的 SIMD 小模块,通过后再加载真正的模块。

🔄Rust 与 JavaScript 的分工

  1. JS页面先检测 SIMD 支持,然后调用 simd_prepare(n, 像素数),让 Rust 用固定种子生成两组 f32 向量和两张随机图片(数据留在 WASM 内存里,避免每次复制)。
  2. Rust每个测试有标量和 SIMD 两个导出函数(如 dot_scalar / dot_simd),计算同样的结果;dot_exact() 用 f64 算出参考值。
  3. JSJS 对每个函数先预热一次,再计时 5 轮取中位数,核对两边结果,并把 SIMD 算出的曼德博集合画到 Canvas 上。

⚡性能要点

  • 实测(桌面 Chromium,「中」规模):点积快 2.2~2.5 倍,图像叠加快 6~8 倍,这个曼德博区域快约 1.5 倍(在 Node 中渲染整个集合的全景图时约 1.9 倍)。
  • 点积和图像叠加的数据量很大,速度最终受内存带宽限制,测几次结果会有浮动;曼德博集合纯计算,结果最稳定。
  • 浏览器为了安全把 performance.now() 的精度限制在约 0.1ms,所以很快的测试每轮会重复调用多次,再取平均。
  • 需要 Chrome 91+、Firefox 89+、Safari 16.4+(iOS 16.4+)。更早的浏览器会显示不支持的提示。

源码crates/algorithms/simd/src/lib.rswww/simd/index.js