Skip to Content
Course content

129: rayon for Data Parallelism

Click on the "Edit" button in the top corner of the screen to edit your slide content.

You've probably noticed that Rust's ownership model makes concurrency "fearless," but actually splitting a massive task across all your CPU cores manually using std::thread is still a lot of boilerplate. You have to handle joining, slicing your data into chunks, and dealing with Arc or Mutex if you aren't careful. It's tedious.

I ran into this last week while processing a huge set of simulation data. I had a vector of about 50 million floats and needed to perform some heavy math on each one. I started with a standard iterator, thinking it would be "fast enough."

The Single-Core Slog


fn main() {
    let data: Vec = (0..50_000_000).map(|x| x as f64).collect();
    
    let start = std::time::Instant::now();
    let result: f64 = data.iter()
        .map(|x| x.sin().powi(2) + x.cos().powi(2)) // Heavy-ish math
        .sum();
    
    println!("Result: {}, took: {:?}", result, start.elapsed());
}

On my machine, this takes a few seconds. Not a disaster, but it's only using one core. If I open my activity monitor, I see one CPU spike to 100% while the other seven just sit there. That feels like a waste of hardware. I want all of them working.

Dropping in Rayon

This is where rayon comes in. It’s the gold standard for data parallelism in Rust. The goal of rayon is to make parallelism feel like a natural extension of iterators. I added rayon = "1.10" to my Cargo.toml and tried the most basic swap: changing .iter() to .par_iter().


use rayon::prelude::*; // This trait is key

fn main() {
    let data: Vec = (0..50_000_000).map(|x| x as f64).collect();
    
    let start = std::time::Instant::now();
    // Just one word change: iter -> par_iter
    let result: f64 = data.par_iter()
        .map(|x| x.sin().powi(2) + x.cos().powi(2))
        .sum();
    
    println!("Result: {}, took: {:?}", result, start.elapsed());
}

I ran this, and the time dropped significantly—nearly 6x faster on my 8-core machine. Rayon is doing "work-stealing" under the hood. It recursively splits the vector into halves until the chunks are small enough to be handled efficiently, and then it distributes those chunks across a thread pool. I didn't have to manage a single thread handle.

Hitting the Borrow Checker Wall

Now, I wanted to do something a bit more complex. Instead of just summing, I wanted to filter the results and push them into a shared Vec. I tried this:


let mut results = Vec::new();
data.par_iter().for_each(|x| {
    if *x > 10.0 {
        results.push(*x); // Error!
    }
});

Immediately, the compiler yelled at me. It makes sense: results.push() requires a mutable reference, and you can't have multiple threads mutating the same Vec simultaneously without synchronization. I could have wrapped the Vec in a Mutex, but that would create a massive bottleneck because every thread would be fighting for the lock, effectively turning my parallel code back into serial code.

The "Rayon way" is to avoid shared mutable state and instead lean on collect(). Since par_iter() returns a ParallelIterator, I can just collect the results back into a collection. Rayon handles the complex logic of merging the partial results from different threads in the correct order.


let results: Vec = data.par_iter()
    .filter(|&&x| x > 10.0)
    .map(|x| x * 2.0)
    .collect(); // Rayon handles the thread-safe merging here

That's the mental shift you have to make. Don't try to "push" data into a shared bucket from inside a parallel loop. Instead, describe the transformation as a pipeline and let collect() or reduce() assemble the final piece. It keeps the code clean and prevents you from fighting with Arc<Mutex<T>> everywhere.




📋 Practical Task

Exercise: Parallelizing a Large-Scale Prime Filter

You have a dataset of 10 million integers. Your task is to filter this list to keep only the prime numbers and then calculate the sum of those primes. Because primality testing is CPU-intensive, a serial iterator is too slow. Implement the following requirements using rayon:

  • Create a Vec<u64> containing numbers from 2 to 10,000,000.
  • Implement a helper function is_prime(n: u64) -> bool.
  • Use par_iter() to filter the vector for primes.
  • Use sum() to calculate the total of all identified primes.
  • Print the final sum and the time elapsed.

Ensure you include rayon = "1.10" in your dependencies and import rayon::prelude::* to bring the parallel iterator traits into scope.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.