Loading page…
Rust walkthroughs
Loading page…
rayon::split allow custom parallel recursion patterns beyond the built-in parallel iterators?rayon::split provides a low-level primitive for custom parallel recursion by splitting work into two parallel tasks that can execute independently, unlike parallel iterators which operate on collections. While par_iter() works on existing sequences, split lets you define arbitrary divide-and-conquer algorithms where the division logic is custom to your problem. The key insight is that split takes a value and a splitting function, returning two parallel tasks that can be recursively split further until a base case is reached. This enables patterns like parallel quicksort, parallel tree traversals, and parallel divide-and-conquer algorithms that don't naturally fit the iterator model.
use rayon::split;
fn main() {
let data = 1000u32;
// split divides work into parallel tasks
let (left, right) = split(data, |value| {
if value < 100 {
// Don't split further - too small
(None, None)
} else {
// Split in half
let mid = value / 2;
(Some(mid), Some(value - mid))
}
});
// left and right are rayon::Split implementations
// They execute in parallel when consumed
}split creates parallel tasks from a splitting function.
use rayon::split;
fn parallel_sum(start: u64, end: u64) -> u64 {
split((start, end), |(s, e)| {
let len = e - s;
if len <= 1000 {
// Base case: too small to split
} else {
// Split into two halves
let mid = s + len / 2;
(Some((s, mid)), Some((mid, e)))
}
})
.map(|(s, e)| {
// Sequential computation for this chunk
(s..e).sum()
})
.sum()
}
fn main() {
let result = parallel_sum(0, 1_000_000);
assert_eq!(result, 499999500000);
}split divides work; map processes each parallel task.
use rayon::prelude::*;
use rayon::split;
fn main() {
// Parallel iterator: works on collections
let sum1: u64 = (0..1_000_000).into_par_iter().sum();
// Split: custom division logic
let sum2 = split(0u64..1_000_000, |range| {
let len = range.end - range.start;
if len <= 10000 {
} else {
let mid = range.start + len / 2;
(Some(range.start..mid), Some(mid..range.end))
}
})
.map(|range| range.sum())
.sum();
assert_eq!(sum1, sum2);
}Parallel iterators are simpler; split offers more control.
use rayon::split;
#[derive(Debug)]
struct Tree {
value: i32,
left: Option<Box<Tree>>,
right: Option<Box<Tree>>,
}
impl Tree {
fn parallel_sum(&self) -> i32 {
split(self, |node| {
match (&node.left, &node.right) {
(Some(left), Some(right)) => {
// Split into left and right subtrees
(Some(left.as_ref()), Some(right.as_ref()))
}
_ => (None, None), // No more splitting
}
})
.map(|node| {
// Compute sum: value + left_sum + right_sum
let left_sum = node.left.as_ref()
.map(|l| l.parallel_sum())
.unwrap_or(0);
let right_sum = node.right.as_ref()
.map(|r| r.parallel_sum())
.unwrap_or(0);
node.value + left_sum + right_sum
})
.sum()
}
}
fn main() {
let tree = Tree {
value: 1,
left: Some(Box::new(Tree {
value: 2,
left: Some(Box::new(Tree { value: 4, left: None, right: None })),
right: Some(Box::new(Tree { value: 5, left: None, right: None })),
})),
right: Some(Box::new(Tree {
value: 3,
left: None,
right: Some(Box::new(Tree { value: 6, left: None, right: None })),
})),
};
assert_eq!(tree.parallel_sum(), 21);
}split naturally handles tree-shaped parallelism.
use rayon::split;
fn main() {
// The splitting function: S -> (Option<S>, Option<S>)
// Returns (Some(left), Some(right)) to split
// Returns (None, None) for base case
let result = split(100u32, |value| {
if value <= 10 {
// Base case: don't split
} else {
// Split: return two parts
(Some(value / 2), Some(value - value / 2))
}
})
.map(|v| v * 2) // Process each part
.sum(); // Combine results
println!("Result: {}", result);
}The splitting callback decides when to stop dividing.
use rayon::split;
fn main() {
// rayon's work-stealing scheduler handles load balancing
// Unbalanced splits still parallelize efficiently
let result = split(100u32, |value| {
if value <= 1 {
} else {
// Highly unbalanced split
(Some(1), Some(value - 1))
}
})
.map(|v| v * v)
.sum();
// rayon's work-stealing balances load across threads
// even with unbalanced splits
println!("Sum of squares: {}", result);
}Rayon's scheduler handles unbalanced work through work stealing.
use rayon::prelude::*;
fn main() {
let data: Vec<i32> = (0..10000).collect();
// par_chunks: divides into fixed-size chunks
let sum1: i32 = data.par_chunks(1000)
.map(|chunk| chunk.iter().sum())
.sum();
// split: custom division based on workload
let sum2 = split(data.as_slice(), |slice| {
if slice.len() <= 1000 {
} else {
let mid = slice.len() / 2;
(Some(&slice[..mid]), Some(&slice[mid..]))
}
})
.map(|slice| slice.iter().sum())
.sum();
assert_eq!(sum1, sum2);
}par_chunks is simpler; split offers custom division logic.
use rayon::join;
use rayon::split;
fn main() {
let data = 1000u32;
// join: explicit parallel execution of two closures
let (a, b) = join(
|| data / 2,
|| data - data / 2,
);
// split: declarative division with automatic recursion
let result = split(data, |value| {
if value <= 100 {
} else {
let mid = value / 2;
(Some(mid), Some(value - mid))
}
})
.map(|v| v)
.sum();
// join is one level of parallelism
// split can recursively divide
}join provides explicit two-way parallelism; split handles recursion.
use rayon::split;
fn main() {
// Nested splits for 2D parallelism
let data = vec![vec![1, 2, 3], vec![4, 5, 6], vec![7, 8, 9]];
let result = split(data.as_slice(), |rows| {
if rows.len() <= 1 {
} else {
let mid = rows.len() / 2;
(Some(&rows[..mid]), Some(&rows[mid..]))
}
})
.map(|rows| {
// Inner split for each row
split(rows, |row| {
if row.len() <= 1 {
} else {
let mid = row.len() / 2;
(Some(&row[..mid]), Some(&row[mid..]))
}
})
.map(|vals| vals.iter().sum::<i32>())
.sum::<i32>()
})
.sum::<i32>();
println!("Total: {}", result);
}Nested split calls enable multi-dimensional parallelism.
use rayon::split;
fn parallel_fib(n: u32) -> u64 {
if n <= 25 {
// Base case: sequential
return fib_sequential(n);
}
split(n, |&n| {
// Split into fib(n-1) and fib(n-2)
(Some(n - 1), Some(n - 2))
})
.map(parallel_fib)
.sum()
}
fn fib_sequential(n: u32) -> u64 {
if n <= 1 {
n as u64
} else {
fib_sequential(n - 1) + fib_sequential(n - 2)
}
}
fn main() {
let result = parallel_fib(40);
println!("Fib(40) = {}", result);
}split parallelizes divide-and-conquer Fibonacci.
use rayon::split;
fn parallel_quicksort<T: Ord + Send + Clone>(data: &mut [T]) {
if data.len() <= 1000 {
// Base case: sequential sort for small arrays
data.sort();
return;
}
// Choose pivot and partition
let pivot = data[0].clone();
let (less, greater): (Vec<_>, Vec<_>) = data.iter()
.skip(1)
.cloned()
.partition(|x| *x <= pivot);
// Recursively sort partitions
split(
(less, greater),
|(mut less, mut greater)| {
parallel_quicksort(&mut less);
parallel_quicksort(&mut greater);
},
);
// Reassemble: less + pivot + greater
data[..less.len()].copy_from_slice(&less);
data[less.len()] = pivot;
data[less.len() + 1..].copy_from_slice(&greater);
}Custom recursion enables parallel quicksort with custom partition logic.
use rayon::prelude::*;
use rayon::split;
fn main() {
// Use parallel iterators when:
// - You have a collection
// - Work is evenly distributed
// - Standard map/filter/reduce operations apply
let data: Vec<i32> = (0..10000).collect();
let sum1: i32 = data.par_iter().sum();
// Use split when:
// - Division logic is custom
// - Work is irregular
// - Problem naturally divides recursively
// - You control when to stop splitting
let sum2 = split(0u32..10000, |range| {
let len = range.end - range.start;
if len <= 1000 {
} else {
let mid = range.start + len / 2;
(Some(range.start..mid), Some(mid..range.end))
}
})
.map(|r| r.sum())
.sum();
}Use parallel iterators for collections; use split for custom recursion.
use rayon::{split, join};
fn parallel_process<T: Send>(data: Vec<T>) -> Vec<T> {
if data.len() <= 100 {
// Base case
return process_sequential(data);
}
let mid = data.len() / 2;
let (left, right) = data.split_at(mid);
// Use join for immediate parallelism
let (processed_left, processed_right) = join(
|| parallel_process(left.to_vec()),
|| parallel_process(right.to_vec()),
);
// Combine results
combine(processed_left, processed_right)
}
fn process_sequential<T>(data: Vec<T>) -> Vec<T> {
data
}
fn combine<T>(left: Vec<T>, right: Vec<T>) -> Vec<T> {
[left, right].concat()
}Combine split with join for custom parallel patterns.
| Feature | Parallel Iterators | rayon::split |
|---------|-------------------|----------------|
| Input | Collections | Arbitrary values |
| Division | Automatic chunking | Custom splitting function |
| Use case | Maps, filters, reduces | Divide-and-conquer |
| Recursion | Limited | Arbitrary depth |
| Control | Automatic | Manual base case |
rayon::split provides a foundation for custom parallel recursion beyond what parallel iterators offer:
Custom division logic: While parallel iterators split collections uniformly, split lets you define arbitrary division based on your problem. The splitting function decides both when to divide and how to divide.
Recursive parallelism: Each split can produce two sub-problems that are themselves recursively split. This naturally models divide-and-conquer algorithms like quicksort, merge sort, and tree traversals.
Base case control: You define when splitting stops—typically when the work is small enough that sequential processing is faster. This threshold tuning is crucial for performance.
Work stealing integration: Rayon's work-stealing scheduler handles the actual parallel execution, balancing uneven workloads across threads automatically.
Key insight: split is a declarative abstraction for divide-and-conquer parallelism. You specify what to split and when to stop; Rayon handles the parallel execution. This is more flexible than parallel iterators for problems that don't fit the collection model, but requires more careful design to achieve good parallelism. Use split when your problem has natural recursive structure that doesn't map cleanly to iterating over a collection.