Rust SIMD 现状与可移植向量化方案选型

Rust 的 SIMD 生态在这一年明显成熟,但成熟的是 crate 层,标准库仍然停在原地。std::simd 至今只有 nightly 能用,且还在陆续出现破坏性 API 改动,跟踪 issue 已经开到 #86656。能在稳定版上落地的是几条替代路线,fearless_simd 和 wide 都在近期发布了 1.0,pulp 和 macerator 各有各的取舍。选型不取决于哪个抽象更好看,而取决于目标机器的指令集覆盖率和能不能接受 nightly。

为什么默认编译器不够用

x86_64 的默认编译目标只保证 SSE2。它是 2000 年出世的指令集,四路单精度浮点就是上限。想用上 2013 年问世的 AVX2,必须在编译期或运行期显式开启。

一次编译内核定目标的写法最简单,把目标特性写进 RUSTFLAGS 就行。

RUSTFLAGS='-C target-cpu=x86-64-v3' cargo build --release

代价是产出的二进制不能在老 CPU 上跑。Firefox 的硬件调查显示,2026 年仍有约 15% 的 x86 机器没有 AVX2。这个比例做桌面浏览器不能忽略,写库就更不能忽略。

运行时检测多版本是库的标准做法。multiversion crate 用一个属性宏就够,标注在函数上,编译期生成多个分支,加载时按当前 CPU 挑一个。

#[multiversion(targets = "simd")]
fn process(data: &mut [f32]) { /* ... */ }

它的开销小于十几条指令,但函数太小的时候这部分开销会吃掉收益。AVX-512 那一侧还有个坑,multiversion 只检测指令集存在,不检测它在当前机器上是不是真的快。

五条路线各卡在哪

可移植向量抽象有五个主要选择,问题都不在抽象层,而在生成代码。

std::simd 的问题最多。它依赖的 LLVM 能力跟不上,某个操作没有对应 LLVM 指令时,它会静默生成一个标量循环,跑起来不报错,性能却完全不是向量化的。对一个性能组件来说,这种静默退化比直接编译失败更危险。sin() 和 reduce_sum() 的性能和精度也明显差。

fearless_simd 的思路是让函数不绑定具体位宽,写成泛型,由调用方决定实际用哪个指令集。

#[simd]
fn dot<S: Simd>(simd: S, a: &[f32], b: &[f32]) -> f32 { /* ... */ }

wide 已经 1.0,但和 multiversion 根本不兼容,也不支持泛型。pulp 的安全用法是建一个上下文,把整个热循环包进去,这个问题连它自己的文档都没写清楚。macerator 除了 burn 之外在 crates.io 上几乎没人用,AVX-512 路径还要 nightly。

还有一条路是完全绕开抽象,直接写 std::arch 的 intrinsics。Rust 1.87 之后,不接收指针参数的 intrinsics 可以从安全代码里调用,但加载和存储仍然是 unsafe。intrinsics 的最大问题是和编译器优化存在结构性冲突,LLVM、GCC、MSVC 都有过把向量 shuffle 优化成旧指令集操作的 bug。

硬件画像比抽象层更决定性能

选型前先把目标机器的指令集分布摸清。Steam 的硬件调查里约 23.9% 的系统有 AVX-512,这是消费级装机里相当可观的比例。

AVX-512 的坑在早期 Intel 上,重度使用会拉低全核频率,2019 年的 Ice Lake 才修好这件事。AMD Zen 4 没有原生 512 位,靠两条 256 位拼,实测仍然胜过 Intel 的原生 512 位。Zen 5 才有了原生 512 位。

LLVM 对 gather 和 scatter 的处理也很糟,Intel 上自动向量化后慢 1.75 倍,AMD 上慢 4 倍。这类操作写 intrinsics 不如改写算法避开。

ARM 一侧,SVE2 现在成了 ARM CPU 的强制要求,但位宽只有 128 位,和 NEON 拉不开差距。

选型建议

按约束选,不按名声选。

  • 只在自己控制的机器上跑,直接 -C target-cpu=native 或写死目标特性,别引入依赖。
  • 要发布库给陌生人用,multiversion 加一个可移植抽象是当前最稳的组合。
  • 团队能接受 nightly 且愿意追上游,std::simd 长期最正统,眼下要自己兜住静默退化的问题。
  • 只服务一台已知 CPU,手写 intrinsics 并留汇编出口,瓶颈不在抽象上。
  • 手写多版本目标串时注意,AVX-512 那一串特性名极长,抄漏一个就白写。

常见问题

为什么不用 C 的 SIMD 库? 能用的前提是接受自己包一层 FFI。Rust 侧的安全保证到边界就断了。

自动向量化完全不能信? 简单循环可以交给它,gather、scatter、reduction 和有别名的循环一定要看生成的汇编。

怎么确认没被静默退化? 写一个能复现的基准,把吞吐和你手算的理论上限对一遍。差一个数量级基本就是没向量化。