引言
2026 年初,AMD Zen 5 CPU 的发布为开发者带来了令人兴奋的消息——这是首款拥有完整 512 位数据通路的 AMD 处理器。这意味着我们终于可以放心使用 AVX-512 SIMD 指令,而不必担心降频和其他性能问题。
对于 Rust 开发者来说,这无疑是一份意外的惊喜。Rust 让我们能够轻松地为热点代码添加 SIMD 加速,而无需编写汇编代码。在实际测试中,纯 Rust 实现的 ChaCha12 在 AWS m8a.2xlarge 实例上可以达到 6.7 GB/s 的吞吐量,ChaCha20 达到 5.1 GB/s,BLAKE3 更是高达 10.8 GB/s。
本文将带你深入了解如何在纯 Rust 中编写 SIMD 加速代码,无需 nightly 版本即可上手。
什么是 SIMD?
SIMD 是 Single Instruction, Multiple Data(单指令多数据)的缩写,指的是能够同时操作更大数据向量的 CPU 指令。
普通的 CPU 指令通常处理最多 64 位的数据,我们称之为"标量指令"。而 SIMD 指令则允许 CPU 处理更大的数据,对于 x86_64 架构的 AVX-512 指令集来说,最高可达 512 位。我们称这些为"向量指令"。
下面是一个伪代码示例,展示如何将 4 个 uint64 数值各加 10:
// 传统方式:逐个处理
let mut a = [1, 2, 3, 4];
for n in &a {
*n += 10;
}
// SIMD 方式:并行处理
let mut vector = u64x4::from_array([1, 2, 3, 4]); // 创建一个 256 位向量,包含 4 个 uint64
let x = u64x4::splat(10); // 创建一个 256 位向量:(10, 10, 10, 10)
let vector = vector + x;
// 结果:vector = u64x4(11, 12, 13, 14)
传统方式需要循环执行多次,而向量化代码大约只需要 3 条指令即可完成。
SIMD 编程思维
使用 SIMD 指令可以概括为三个步骤:加载 → 计算 → 存储。
第一步:加载数据
将数据从内存加载到向量寄存器中:
// 将值为 1 的 int64 加载 8 次到一个 512 位向量中
let v1 = _mm512_set1_epi64(1);
// 将包含 8 个元素的(非对齐)int64 数组加载到 512 位向量中
let v2 = _mm512_loadu_epi64([1, 2, 3, 4, 5, 6, 7, 8]);
第二步:执行计算
执行加法、异或、减法等运算:
// 并行执行 8 个 64 位通道的加法运算
let v_result = _mm512_add_epi64(v1, v2);
// v_result = __m512i(2, 3, 4, 5, 6, 7, 8, 9)
第三步:存储结果
将结果写回内存:
let result = [0i64; 8];
_mm512_storeu_epi64(result.as_mut_ptr(), v_result);
// result = [2, 3, 4, 5, 6, 7, 8, 9]
需要注意的是,从内存加载和存储数据的延迟成本相对较高,因此应尽量减少这类操作。让数据"保温"在 SIMD 寄存器中是更好的选择。
了解你的目标平台
实现 SIMD 加速代码需要投入时间,也会增加维护负担,因此你需要了解代码将在哪里运行:
如果代码主要运行在高端 Intel/AMD 处理器(如服务器)上,专注于 AVX-512 的优化可能就足够了。
如果代码主要运行在消费级设备上,那么专注于 AVX2 和 NEON(ARM64)可能是更好的选择。
另外,现在已经没有必要实现 SSE2 的 SIMD 代码了,因为 2015 年以后生产的大多数处理器都支持 AVX2。
CPU 特性检测
SIMD 加速代码依赖于 CPU 上可用的指令集。在 Rust 中有几种不同的方式来检测 CPU 特性。
运行时检测
使用 std::arch 模块提供的宏进行运行时检测:
fn foo() {
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
{
// 检测当前 CPU 是否支持 AVX2 指令集
if is_x86_feature_detected!("avx2") {
return unsafe { foo_avx2() };
}
}
// 不使用 AVX2 的回退实现
}
编译时检测
使用条件编译进行编译时检测:
#[cfg(all(target_arch = "x86_64", target_feature = "avx512f"))]
fn optimized_function() {
// AVX-512 优化实现
}
推荐的混合方案
建议默认提供运行时检测,同时提供 Cargo feature 让用户选择仅编译时检测:
# Cargo.toml
[features]
default = ["std"]
# 在支持的平台上启用标准库进行 CPU 特性检测
std = []
fn my_function() {
// 使用运行时检测
#[cfg(feature = "std")]
{
#[cfg(target_arch = "x86_64")]
if is_x86_feature_detected!("avx512f") {
return my_function_avx512();
}
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
if is_x86_feature_detected!("avx2") {
return my_function_avx2();
}
}
// 使用编译时检测
#[cfg(not(feature = "std"))]
{
#[cfg(all(target_arch = "x86_64", target_feature = "avx512f"))]
return my_function_avx512();
#[cfg(all(any(target_arch = "x86", target_arch = "x86_64"), target_feature = "avx2"))]
return my_function_avx2();
}
// 当 SIMD 加速不可用时的标量回退实现
return my_function_generic();
}
选择纯 Rust SIMD 实现方式
在纯 Rust 中使用 SIMD 指令有几种不同的方式:
1. 标准库的实验性 simd 模块
目前仅在 Rust nightly 版本中可用,但这是未来最令人期待的特性。
2. wide crate
一个第三方 crate,为稳定版 Rust 复制了 simd 模块的功能,但目前仅限于 256 位向量:
use wide::*;
fn main() {
// 创建一个填充值为 1 的向量
let a = u32x4::splat(1);
// 从数组创建向量
let b = u32x4::from([1, 2, 3, 4]);
// 执行向量加法
let result = a + b;
assert_eq!(result.to_array(), [2, 3, 4, 5]);
}
如果你不介意额外的依赖,这是推荐的方式。
3. pulp crate
一个高层次的 SIMD 抽象,可以理解为 SIMD 版的 rayon:
use pulp::Arch;
fn main() {
let mut v = (0..1000).map(|i| i as f64).collect::<Vec<_>>();
let arch = Arch::new();
// 自动分发到最优的 SIMD 实现
arch.dispatch(|| {
for x in &mut v {
*x *= 2.0;
}
});
for (i, x) in v.into_iter().enumerate() {
assert_eq!(x, 2.0 * i as f64);
}
}
4. 标准库的 arch 模块
这是最底层的方式,直接暴露原始的内联函数和向量寄存器类型。虽然代码重复较多,但这是目前唯一在稳定版 Rust 上无需任何依赖就能工作的方式。
关于自动向量化
编译器的自动向量化是一个重要的话题。对于一些常见操作,比如对两个缓冲区进行异或运算,编译器通常能够自动生成向量化代码:
// 编译器会自动识别这个模式并生成向量化代码
input_block
.iter_mut()
.zip(keystream)
.for_each(|(plaintext, keystream)| *plaintext ^= *keystream);
建议是:除非你有确凿的证据证明某处是性能瓶颈,否则不要为常见操作手动实现 SIMD 优化。编译器很可能会为你自动向量化,或者至少输出高效的代码。
测试你的实现
别忘了使用不同的 SIMD 指令集来测试你的实现。可以使用 RUSTFLAGS 环境变量来选择性禁用 CPU 特性:
# 测试通用代码(无 SIMD 加速)
RUSTFLAGS="-C target-cpu=native -C target-feature=-avx2,-avx512f" make test
# 测试 AVX2 代码
RUSTFLAGS="-C target-cpu=native -C target-feature=-avx512f" make test
# 测试 AVX-512 代码
make test
需要注意的是,GitHub Actions 目前不支持 AVX-512 指令,因此你需要在自己的机器上运行 AVX-512 测试。
可移植 SIMD:未来可期
Rust 的可移植 SIMD(simd 模块)可能是目前 nightly 版本中最令人兴奋的特性之一。
它将极大地减轻开发者的维护负担,让我们只需为每种向量大小实现一次算法,编译器会在编译时选择适合不同 CPU 架构的具体指令,并自动回退到标量实现:
fn main() {
// 为所有支持 128 位寄存器的平台创建一个 128 位向量
let a = u32x4::splat(1);
let b = u32x4::from([1, 2, 3, 4]);
let result = a + b;
assert_eq!(result.to_array(), [2, 3, 4, 5]);
}
这意味着我们不再需要学习每个平台/向量大小的特定内联函数名称,代码也将大大简化。例如,作者之前需要为 ChaCha20 的 128 位向量实现编写两次代码——一次是 ARM64 的 NEON,一次是 wasm32 的 simd128。虽然代码几乎相同,只是类型和内联函数名称不同,但这意味着更多的代码需要测试、维护和文档化。
有了可移植 SIMD,只需要基于 u32x4 类型实现一次,Rust 就会将其编译为任何支持 128 位向量指令平台的优化代码。
总结
SIMD 编程是提升性能的利器,而 Rust 让这一切变得前所未有的简单:
- 1. Zen 5 等新一代 CPU 让 AVX-512 真正可用,不再有降频困扰
- 2. SIMD 编程遵循"加载 → 计算 → 存储"的三步模式
- 3. 选择合适的目标平台,服务器端关注 AVX-512,消费端关注 AVX2 和 NEON
- 6. 期待可移植 SIMD 特性登陆稳定版 Rust
Rust 正在逐步吞噬整个计算栈,从微控制器到大型服务器,从 WebAssembly 到机器人和卫星。超过 37% 的密码学库漏洞是内存安全问题,C 和汇编正在逐渐退出历史舞台,而 Rust 是唯一合理的替代方案。
参考文章
- 1. SIMD programming in pure Rust: https://kerkour.com/introduction-rust-simd
书籍推荐
这本《Rust权威指南》中文版第二版是由 Rust 核心开发团队编写的权威学习资料,由中国 Rust 社区成员翻译。它适合所有希望评估、入门、提高和研究 Rust 语言的软件开发人员,被视为 Rust 开发工作的必读书目。
本书由浅入深地介绍了 Rust 语言的基础概念到独有实用工具的方方面面,涵盖了所有权、trait、生命周期、安全保证等高级概念,以及模式匹配、错误处理、包管理、函数式特性和并发机制等实用工具。书中包含三个完整的项目开发实战案例,手把手教读者从零开始开发 Rust 实践项目。
特别值得注意的是,本书已更新至 Rust 2021 版本内容,既能满足初学者系统学习需求,也适合有经验的开发者作为参考指南,是构建扎实 Rust 技能的最佳入口。