一行代码提速30倍!RustRayon并行计算:告别多线程管理困境在高性能计算领域,多核CPU的潜力常常被传统顺序代码所限制。我们渴望并行加速,但又惧怕手动管理线程、锁和数据竞争带来的复杂性与风险。Rust语言以其安全性和性能著称,而Rayon库则是Rust生态中解决数据并行
在高性能计算领域,多核 CPU 的潜力常常被传统顺序代码所限制。我们渴望并行加速,但又惧怕手动管理线程、锁和数据竞争带来的复杂性与风险。Rust 语言以其安全性和性能著称,而 Rayon 库则是 Rust 生态中解决数据并行化问题的"银弹"。它承诺:用近乎零成本的语法切换,即可将顺序迭代器华丽地升级为并行计算,且完全继承 Rust 的数据竞争安全保证。 本文将通过两个实战案例——百万级数据求和与高效质数查找,展示 Rayon 如何让你真正专注于业务逻辑,将繁重的并行任务安全、高效地交给它处理。
Rayon 是用于 Rust 的数据并行库
cargo new rayon_iters
cd rayon_iters
ls
cc # open -a cursor .
cargo add rayon
use rayon::prelude::*;
fn main() {
let nums: Vec<u64> = (0..1000000).collect();
let sum: u64 = nums.par_iter().sum::<u64>();
println!("Sum: {sum}");
}
Rayon 会自动创建一个线程池,每个CPU就是一个线程。并且还会使用一个任务队列,这个队列实现了工作窃取机制,这个机制是避免线程空闲,同时还支持子任务 sub task,就是一个任务可以等待另一个任务的完成。
par_iter
用于不可变引用的并行迭代
par_iter_mut
用于可变引用的并行迭代
into_par_iter
用于所有权的并行迭代
这段代码的核心目标是:创建一个包含一百万个数字的向量,并利用 Rust 的 Rayon 库,以最简洁的方式在多个 CPU 核心上并行计算这些数字的总和。
use rayon::prelude::*;
fn main() {
// ...
}
**: 这是使用 Rayon 并行的关键。它导入了
ParallelIteratortrait,允许您在集合类型(如
Vec)上调用以
par_开头的方法(如
par_iter()`)。let nums: Vec<u64> = (0..1000000).collect();
:
nums
的向量。u64
)。let sum: u64 = nums.par_iter().sum::<u64>();
这是整个代码中最强大的一行,它将顺序计算变成了并行计算:
nums.par_iter()
: 这一步是 Rayon 魔法的起点。它将 Rust 标准库的顺序迭代器 (iter()
) 转换成一个 并行迭代器。
.sum::<u64>()
: 这是一个归约(Reduction)操作。
sum
结果。println!("Sum: {sum}");
这段代码的亮点在于,它展示了 Rayon 如何实现数据并行性 (Data Parallelism),即在多个处理器上同时对数据的不同部分执行相同的操作(本例中是求和)。
nums.iter().sum()
切换到 nums.par_iter().sum()
几乎零成本,无需手动管理线程、锁或互斥量。简而言之,这段代码是一个教科书式的例子,说明了在 Rust 中利用 Rayon 库,可以轻松、安全地将计算密集型任务并行化,从而显著提高执行速度。
➜ cargo run
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.01s
Running `target/debug/rayon_iters`
Sum: 499999500000
这段运行结果有力地证明了 Rayon 并行计算的高效性和准确性。输出的 Sum: 499999500000
是从0到999,999的所有整数之和的精确理论值,这表明 Rayon 成功地将一百万个数字的向量切分给多个 CPU 核心,让它们独立计算部分和,并最终安全、无误地将这些部分结果合并(归约) 成了正确的最终总和。同时,Finished
语句中极短的耗时,也暗示了 Rayon 在处理这类计算密集型任务时,所展现出的出色并行加速能力。
use std::time::Instant;
use rayon::prelude::*;
fn is_prime(n: u32) -> bool {
(2..=n / 2).into_par_iter().all(|i| n % i != 0)
}
fn main() {
let now = Instant::now();
let nums: Vec<u64> = (2..1000000).collect();
let mut primes: Vec<&u64> = nums.par_iter().filter(|&n| is_prime(*n as u32)).collect();
let elapsed = now.elapsed();
primes.par_sort_unstable();
println!("{primes:?}");
println!("{} ms to find {} primes", elapsed.as_millis(), primes.len());
}
这段 Rust 代码是 Rayon 在双重并行计算中应用的一个高级示例,它用于高效地找出一百万以内(从 2 到 999,999)的所有质数,并精确测量性能。程序的核心是 is_prime
函数,它利用 into_par_iter().all()
将质数判断过程本身并行化:检查一个数 n 是否为质数时,对其除数范围
$$
(2...n/2)
$$
的遍历和取模检查是并发进行的。在 main
函数中,主逻辑通过 nums.par_iter().filter()
将对百万个数字的筛选也并行化,这意味着一百万次独立的、且自身也是并行的质数检查都在 Rayon 线程池上同时进行,实现了最大化的 CPU 利用率。代码使用 std::time::Instant
精确测量了从开始筛选到收集结果的总耗时,最终通过 println!
语句打印出找到的质数列表、总数量和运行时间,有力地证明了 Rayon 在这种计算密集型任务中带来的显著性能提升。
在 Rust 项目中,cargo run 命令的作用是:
编译 (Compile): 自动编译您的项目源代码(如果自上次运行以来代码有更改)。
运行 (Run): 执行编译成功后生成的二进制可执行文件。
➜ cargo run
... ...
99091, 999101, 999133, 999149, 999169, 999181, 999199, 999217, 999221, 999233, 999239, 999269, 999287, 999307, 999329, 999331, 999359, 999371, 999377, 999389, 999431, 999433, 999437, 999451, 999491, 999499, 999521, 999529, 999541, 999553, 999563, 999599, 999611, 999613, 999623, 999631, 999653, 999667, 999671, 999683, 999721, 999727, 999749, 999763, 999769, 999773, 999809, 999853, 999863, 999883, 999907, 999917, 999931, 999953, 999959, 999961, 999979, 999983]
47750 ms to find 78498 primes
➜ cargo run --release ... ... 998813, 998819, 998831, 998839, 998843, 998857, 998861, 998897, 998909, 998917, 998927, 998941, 998947, 998951, 998957, 998969, 998983, 998989, 999007, 999023, 999029, 999043, 999049, 999067, 999083, 999091, 999101, 999133, 999149, 999169, 999181, 999199, 999217, 999221, 999233, 999239, 999269, 999287, 999307, 999329, 999331, 999359, 999371, 999377, 999389, 999431, 999433, 999437, 999451, 999491, 999499, 999521, 999529, 999541, 999553, 999563, 999599, 999611, 999613, 999623, 999631, 999653, 999667, 999671, 999683, 999721, 999727, 999749, 999763, 999769, 999773, 999809, 999853, 999863, 999883, 999907, 999917, 999931, 999953, 999959, 999961, 999979, 999983] 1552 ms to find 78498 primes
这段运行输出展示了使用 **Rust Rayon 库**在不同编译模式下执行百万以内质数查找任务(一个计算密集型任务)的性能对比和结果的准确性。
### 运行结果详细解释
1. **结果的准确性与一致性:**
- 两次运行都成功找到了 **78,498 个质数**(`to find 78498 primes`)。
- 输出的质数列表(部分可见)在两次运行中也是一致的。
- 这证明了您的 Rayon 并行算法(很可能采用了嵌套的并行迭代,即并行查找和并行质数判断)是**线程安全且正确的**,无论在哪个线程上执行,结果都具有确定性。
2. **Debug 与 Release 模式的性能天壤之别(核心发现):**
- **Debug 模式 (`cargo run`) 耗时:** 47,750 ms(约 47.75 秒)。
- **Release 模式 (`cargo run --release`) 耗时**:1,552 ms (约 1.55 秒)。
这个巨大的差异(快了约 **30 倍**)是 Rust 编译器的典型表现:
- **Debug 模式**编译速度快,但**关闭了所有优化**,代码执行效率极低。
- **Release 模式**花费更长的编译时间,但会进行极致的优化(如内联、循环展开等),因此执行速度极快。对于计算密集型任务,**并行化 (Rayon) 只有在 Release 优化下才能真正发挥出极致性能。**
**总结:**
这段结果完美地验证了两个关键点:您的 Rayon 代码是**正确且线程安全的**(两次运行结果一致),并且通过 Rust 编译器配合 Rayon 的并行能力,成功地将一个需要近一分钟的计算任务**加速到了仅需 1.5 秒左右**,展示了 Rust 在高性能计算方面的巨大优势。
## 总结
Rayon 库完美地践行了 Rust 在安全和性能上的双重承诺。通过本文的两个实战案例——百万级求和与复杂质数筛选——我们深刻体会到 Rayon 的核心价值:
1. **极简的并行化**:从 `iter()` 到 `par_iter()` 的**几乎零成本切换**,让并行编程变得触手可及。
2. **内置的安全性**:Rayon 的设计基于 Rust 的所有权系统,**从根本上杜绝了多线程编程中常见的数据竞争**,让开发者可以安心地享受并行带来的性能提升。
3. **极致的性能**:得益于其智能的**工作窃取(Work-Stealing)调度器**,Rayon 能最大限度地利用所有 CPU 核心,并在 Release 模式的优化下,为计算密集型任务带来**数十倍的性能加速**。
对于任何希望在 Rust 中处理大数据集或计算密集型任务的开发者而言,Rayon 都是一个不可或缺的高性能工具。它让高性能不再意味着高风险和高复杂度。
## 参考
- https://github.com/rayon-rs/rayon
- https://docs.rs/rayon/latest/rayon/
- https://rustcc.cn/article?id=181e0a73-6742-42a9-b7a1-1c00bef436c2
- https://www.reddit.com/r/rust/comments/1348njv/is_rayon_always_worth_it/
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!