Rust · Parallel Iterators

Parallel Iterators

use forkunion::StaticScheduler;use forkunion::DynamicScheduler;use forkunion::ParallelSchedule;use forkunion::ParallelIterator;use forkunion::ParallelIteratorExt;use forkunion::ParallelRunner;use forkunion::IntoParallelIterator;use forkunion::Map;use forkunion::Filter;use forkunion::ParallelSlice;use forkunion::ParallelSliceZip;use forkunion::ParallelSliceMut;use forkunion::ParallelRange;use forkunion::ParallelExactIter;use forkunion::CacheAligned;use forkunion::*; 4.0.0-rc.1 · d927445f

For Rayon-style ergonomics, use the parallel iterator API with the prelude. Unlike Rayon, ForkUnion’s parallel iterators don’t depend on the global state and allow explicit control over the thread pool and scheduling strategy. For statically shaped workloads, the default static scheduling is more efficient:

use forkunion as fu;
use forkunion::prelude::*;

let topology = fu::Topology::new().expect("Failed to detect hardware topology");
let mut pool = fu::spawn(&topology, 4);
let mut data: Vec<usize> = (0..1000).collect();

(&data[..])
    .into_par_iter()
    .with_pool(&mut pool)
    .for_each(|value, _task, _at| {
        println!("Value: {}", value);
    })?;

For dynamic work-stealing, use with_schedule with DynamicScheduler:

(&mut data[..])
    .into_par_iter()
    .with_schedule(&mut pool, DynamicScheduler)
    .for_each(|value, _task, _at| {
        *value *= 2;
    })?;

This easily composes with other iterator adaptors, like map, filter, and zip:

(&data[..])
    .into_par_iter()
    .filter(|&x| x % 2 == 0)
    .map(|x| x * x)
    .with_pool(&mut pool)
    .for_each(|value, _task, _at| {
        println!("Squared even: {}", value);
    })?;

For parallel reductions, ForkUnion provides Rayon-like convenience methods with automatic cache-aligned scratch allocation:

let data: Vec<u64> = (0..1_000_000).map(|i| i as u64).collect();

// Sum all elements with automatic scratch allocation
let total: u64 = (&data[..])
    .into_par_iter()
    .with_pool(&mut pool)
    .sum()?;

// Count elements matching a predicate
let evens = (&data[..])
    .into_par_iter()
    .filter(|&x| x % 2 == 0)
    .with_pool(&mut pool)
    .count()?;

// Custom reduction (maximum)
let maximum = (&data[..])
    .into_par_iter()
    .with_pool(&mut pool)
    .reduce(
        || 0u64,                                     // initial value
        |acc, value, _, _| *acc = (*acc).max(*value), // fold function
        |a, b| a.max(b)                              // combine function
    )?;

For manual control over scratch allocation, use reduce_with_scratch:

// Cache-line aligned wrapper to prevent false sharing
let mut scratch: Vec<CacheAligned<u64>> =
    (0..pool.threads_count()).map(|_| CacheAligned(0)).collect();

let total = (&data[..])
    .into_par_iter()
    .with_pool(&mut pool)
    .reduce_with_scratch(
        scratch.as_mut_slice(),
        |acc, value, _, _| acc.0 += *value,  // fold
        |a, b| a.0 += b.0                 // combine in-place
    )?;

Beyond reductions, the iterators offer short-circuiting searches - find_first and find_last for the deterministic lowest- or highest-index match, find_any for whichever match is found first with cooperative cancellation, and any / all for boolean predicates that stop the moment the answer is known. Fallible bodies get for_each_fallible and fold_with_scratch_fallible, which propagate the first error and signal the other workers to stop. Their two answers stay apart: the outer Result reports whether the dispatch ran, the inner one whether the caller’s own work succeeded, so the caller’s error type carries no constraint. This Rayon-style layer is Rust-only; C, C++, Zig, and Mojo expose the pool primitives for_threads, for_n, for_n_dynamic, and for_slices directly. Zig additionally mirrors Rust’s for_slices_mut as forSlicesMut, which hands each thread a disjoint sub-slice rather than the TasksRange its indices would form.

API

StructStaticSchedulerScheduler that uses static chunk assignment.2
parallel.rs:50 ↗
pub struct StaticScheduler;

Implements

Clone, Copy, Debug, ParallelSchedule
*× 3Runs function once per task index, each on the thread the schedule picks.
parallel.rs:71 ↗

Inherited from ParallelSchedule

fn dispatch<F>(&self, pool: &mut ThreadPool, tasks: usize, function: F) -> Result<()>
where
    F: Fn(usize, ThreadInDomain) + Sync,
dispatch_*_slices× 2Runs function over contiguous runs.
parallel.rs:82 ↗

Inherited from ParallelSchedule

fn dispatch__slices<F>(&self, pool: &mut ThreadPool, tasks: usize, function: F) -> Result<()>
where
    F: Fn(TasksRange, ThreadInDomain) + Sync,

The count is a real run length under static scheduling and always 1 under dynamic, which has no batched entry point.

StructDynamicSchedulerScheduler that enables dynamic work stealing.1
parallel.rs:54 ↗
pub struct DynamicScheduler;

Implements

Clone, Copy, Debug, ParallelSchedule
dispatch_slicesWork-stealing hands out one task at a time, so every run is length 1 - the C core exposes no batched dynamic entry point.
parallel.rs:108 ↗

Inherited from ParallelSchedule

fn dispatch_slices<F>(&self, pool: &mut ThreadPool, tasks: usize, function: F) -> Result<()>
where
    F: Fn(TasksRange, ThreadInDomain) + Sync,

Use StaticScheduler when the batching is what you want.

TraitParallelScheduleHow a parallel iterator's tasks are assigned to the threads of a pool.
parallel.rs:57 ↗
pub trait ParallelSchedule: Copy

Implementors

TraitParallelIteratorA source of items that a pool consumes in parallel.7
parallel.rs:122 ↗
pub trait ParallelIterator: Sized

Implementors

ItemWhat the consumer receives for every task.
parallel.rs:124 ↗
type Item;
*× 8Returns the number of tasks.
parallel.rs:127 ↗
fn len(&self) -> usize
is_*_empty× 8Returns true iff there are no tasks.
parallel.rs:130 ↗
fn is__empty(&self) -> bool
*× 8Feeds every item to consumer along with its task index and executing thread.
parallel.rs:135 ↗
fn drive<S, F>(self, pool: &mut ThreadPool, schedule: S, consumer: &F) -> Result<()>
where
    S: ParallelSchedule,
    F: Fn(Self::Item, usize, ThreadInDomain) + Sync,
drive_*× 16As drive under the static schedule.
parallel.rs:141 ↗
fn drive_static<F>(self, pool: &mut ThreadPool, consumer: F) -> Result<()>
where
    F: Fn(Self::Item, usize, ThreadInDomain) + Sync,
Symbol
drive_static
drive_dynamic
drive_static
drive_dynamic
drive_static
drive_dynamic
drive_static
drive_dynamic
drive_static
drive_dynamic
drive_static
drive_dynamic
drive_static
drive_dynamic
drive_static
drive_dynamic
*× 8Transforms every item with mapper before it reaches the consumer.
parallel.rs:157 ↗
fn map<M, U>(self, mapper: M) -> Map<Self, M>
where
    M: Fn(Self::Item) -> U + Sync,
*× 8Keeps only the items for which predicate holds.
parallel.rs:165 ↗
fn filter<P>(self, predicate: P) -> Filter<Self, P>
where
    P: Fn(&Self::Item) -> bool + Sync,
TraitParallelIteratorExtBinds a parallel iterator to the pool that will run it.2
parallel.rs:174 ↗
pub trait ParallelIteratorExt: ParallelIterator + Sized

Implementors

with_*_pool× 8Runs on pool under the static schedule.
parallel.rs:176 ↗
fn with__pool<'pool>(
    self,
    pool: &'pool mut ThreadPool,
) -> ParallelRunner<'pool, Self, StaticScheduler>
with_*_schedule× 8Runs on pool under the given schedule.
parallel.rs:185 ↗
fn with__schedule<'pool, S>(
    self,
    pool: &'pool mut ThreadPool,
    schedule: S,
) -> ParallelRunner<'pool, Self, S>
where
    S: ParallelSchedule,
StructParallelRunnerA parallel iterator paired with the pool and schedule that will run it.13
parallel.rs:200 ↗
pub struct ParallelRunner<'pool, I, S>
with_scheduleReplaces the schedule.
parallel.rs:213 ↗
pub fn with_schedule<S2>(self, schedule: S2) -> ParallelRunner<'pool, I, S2>
where
    S2: ParallelSchedule,
for_eachCalls function for every item with its task index and executing thread.
parallel.rs:226 ↗
pub fn for_each<F>(self, function: F) -> Result<()>
where
    F: Fn(I::Item, usize, ThreadInDomain) + Sync,
fold_with_scratchFolds every item into the per-thread slot of scratch that runs it.
parallel.rs:239 ↗
pub fn fold_with_scratch<T, F>(self, scratch: &mut [T], fold: F) -> Result<()>
where
    T: Send,
    F: Fn(&mut T, I::Item, usize, ThreadInDomain) + Sync,
reduce_with_scratchParallel reduction with caller-provided scratch buffer.
parallel.rs:282 ↗
pub fn reduce_with_scratch<T, F, C>(self, scratch: &mut [T], fold: F, combine: C) -> Result<T>
where
    T: Send + Default,
    F: Fn(&mut T, I::Item, usize, ThreadInDomain) + Sync,
    C: Fn(&mut T, T),

Reduces items in parallel by folding into per-thread accumulators, then combining results on the caller thread. Uses cache-aligned scratch to prevent false sharing. Indexes by thread_index, which works with dynamic scheduling.

Returns

The final reduced value of type T

Arguments

  • scratch - Per-thread accumulators, at least pool.threads_count() long
  • fold - Function to accumulate items: fn(&mut T, I::Item, usize, ThreadInDomain)
  • combine - Function to merge two accumulators: fn(T, T) -> T

Examples

use forkunion::*;
let topology = Topology::new().unwrap();
let mut pool = ThreadPool::spawn(&topology, 4).unwrap();
let data: Vec<u64> = (0..1000).collect();
let mut scratch: Vec<CacheAligned<u64>> =
    (0..pool.threads_count()).map(|_| CacheAligned(0)).collect();

let total = (&data[..]).into_par_iter().with_pool(&mut pool)
    .reduce_with_scratch(
        scratch.as_mut_slice(),
        |acc, value, _, _| acc.0 += *value,
        |a, b| a.0 += b.0,
    );
fold_with_scratch_fallibleFold with early-exit on error, using caller-provided scratch buffer.
parallel.rs:347 ↗
pub fn fold_with_scratch_fallible<T, F, E>(
    self,
    scratch: &mut [T],
    fold: F,
) -> Result<Result<(), E>>
where
    T: Send,
    F: Fn(&mut T, I::Item, usize, ThreadInDomain) -> Result<(), E> + Sync,
    E: Send,

Similar to fold_with_scratch, but allows the fold function to return Result. Stops processing on the first error. Scratch buffers are indexed by thread_index.

Returns

  • Ok(()) if all items were folded successfully
  • Err(E) with the first error encountered

Arguments

  • scratch - Per-thread accumulators, at least pool.threads_count() long
  • fold - Fallible fold function: fn(&mut T, I::Item, usize, ThreadInDomain) → core::result::Result<(), E>

Examples

use forkunion::*;

fn checked_add(acc: &mut u64, value: &u64) -> core::result::Result<(), &'static str> {
    *acc = acc.checked_add(*value).ok_or("overflow")?;
    Ok(())
}

let topology = Topology::new().unwrap();
let mut pool = ThreadPool::spawn(&topology, 4).unwrap();
let data: Vec<u64> = (1..100).collect();
let mut scratch: Vec<CacheAligned<u64>> =
    (0..pool.threads_count()).map(|_| CacheAligned(0)).collect();

let result = (&data[..])
    .into_par_iter()
    .with_pool(&mut pool)
    .fold_with_scratch_fallible(scratch.as_mut_slice(), |acc, value, _, _| {
        checked_add(&mut acc.0, value)
    });

assert!(result.is_ok());
for_each_fallibleExecutes a fallible operation on each item, stopping at the first error.
parallel.rs:423 ↗
pub fn for_each_fallible<F, E>(self, function: F) -> Result<Result<(), E>>
where
    F: Fn(I::Item, usize, ThreadInDomain) -> Result<(), E> + Sync,
    E: Send,

Uses cooperative cancellation: once an error occurs, no further items are processed. Items already "in flight" may still complete, but new items won't start processing.

Returns

  • Ok(()) if all items were processed successfully or were skipped after stop
  • Err(E) with the first error encountered

Performance

Overhead is one atomic load per item (~2% in compute-bound workloads). The atomic swap on error is negligible as it happens at most once.

Examples

use forkunion::*;

fn validate(x: &u64) -> core::result::Result<(), &'static str> {
    if *x < 100 { Ok(()) } else { Err("value too large") }
}

let topology = Topology::new().unwrap();
let mut pool = ThreadPool::spawn(&topology, 4).unwrap();
let data: Vec<u64> = (0..50).collect();

let result = (&data[..])
    .into_par_iter()
    .with_pool(&mut pool)
    .for_each_fallible(|x, _, _| validate(x));

assert!(result.is_ok());
find_*× 2Searches for the first element that matches a predicate, deterministically, by index.
parallel.rs:485 ↗
pub fn find_first<P>(self, predicate: P) -> Result<Option<I::Item>>
where
    I::Item: Send,
    P: Fn(&I::Item) -> bool + Sync,
Symbol
find_first
find_last

Returns the element with the smallest task_index among all matches.

Returns

  • Some(item) with the lowest index if any match was found
  • None if no item matched or the iterator was empty

Returns the element with the largest task_index among all matches.

Returns

  • Some(item) with the highest index if any match was found
  • None if no item matched or the iterator was empty

Examples

use forkunion::*;

let topology = Topology::new().unwrap();
let mut pool = ThreadPool::spawn(&topology, 4).unwrap();
let data: Vec<u64> = vec![10, 20, 30, 20, 10];

let found = (&data[..])
    .into_par_iter()
    .with_pool(&mut pool)
    .find_first(|&&x| x == 20)
    .unwrap();

assert_eq!(found, Some(&20)); // Index 1, not 3
use forkunion::*;

let topology = Topology::new().unwrap();
let mut pool = ThreadPool::spawn(&topology, 4).unwrap();
let data: Vec<u64> = vec![10, 20, 30, 20, 10];

let found = (&data[..])
    .into_par_iter()
    .with_pool(&mut pool)
    .find_last(|&&x| x == 20)
    .unwrap();

assert_eq!(found, Some(&20)); // Index 3, not 1
find_anySearches for any element that matches a predicate, non-deterministically.
parallel.rs:657 ↗
pub fn find_any<P>(self, predicate: P) -> Result<Option<I::Item>>
where
    I::Item: Send,
    P: Fn(&I::Item) -> bool + Sync,

Uses cooperative cancellation: once a match is found, no further items are processed. If multiple items match, any one of them may be returned.

Returns

  • Some(item) if a matching item was found
  • None if no item matched or the iterator was empty

Examples

use forkunion::*;

let topology = Topology::new().unwrap();
let mut pool = ThreadPool::spawn(&topology, 4).unwrap();
let data: Vec<u64> = (0..1000).collect();

let found = (&data[..])
    .into_par_iter()
    .with_pool(&mut pool)
    .find_any(|&&x| x == 42)
    .unwrap();

assert_eq!(found, Some(&42));
anyReturns true if any item matches the predicate.
parallel.rs:711 ↗
pub fn any<P>(self, predicate: P) -> Result<bool>
where
    I::Item: Send,
    P: Fn(&I::Item) -> bool + Sync,

Stops searching after the first match is found.

Examples

use forkunion::*;

let topology = Topology::new().unwrap();
let mut pool = ThreadPool::spawn(&topology, 4).unwrap();
let data: Vec<u64> = (0..1000).collect();

let has_large = (&data[..])
    .into_par_iter()
    .with_pool(&mut pool)
    .any(|&&x| x > 500)
    .unwrap();

assert!(has_large);
allReturns true if all items match the predicate.
parallel.rs:740 ↗
pub fn all<P>(self, predicate: P) -> Result<bool>
where
    I::Item: Send,
    P: Fn(&I::Item) -> bool + Sync,

Stops searching after the first non-match is found.

Examples

use forkunion::*;

let topology = Topology::new().unwrap();
let mut pool = ThreadPool::spawn(&topology, 4).unwrap();
let data: Vec<u64> = (0..100).collect();

let all_small = (&data[..])
    .into_par_iter()
    .with_pool(&mut pool)
    .all(|&&x| x < 200)
    .unwrap();

assert!(all_small);
reduceParallel reduction with a cache-aligned per-thread scratch accumulator.
parallel.rs:770 ↗
pub fn reduce<T, Init, F, C>(self, init: Init, fold: F, combine: C) -> Result<T>
where
    Init: Fn() -> T + Sync,
    T: Send + Sync + Default,
    F: Fn(&mut T, I::Item, usize, ThreadInDomain) + Sync,
    C: Fn(T, T) -> T,

Allocates one CacheAligned<T> accumulator per thread in a node-local PinnedVec, so threads fold into local memory before the serial combine.

Nearly identical to Rayon's reduce API, just requires explicit pool.

Arguments

  • init - Function to create initial accumulator value
  • fold - Function to accumulate items: fn(&mut T, I::Item, usize, ThreadInDomain)
  • combine - Function to merge two accumulators: fn(T, T) -> T

Examples

use forkunion::*;
let topology = Topology::new().unwrap();
let mut pool = ThreadPool::spawn(&topology, 4).unwrap();
let data: Vec<u64> = (0..1000).collect();

let total = (&data[..]).into_par_iter().with_pool(&mut pool)
    .reduce(|| 0, |acc, value, _, _| *acc += *value, |a, b| a + b);
sumSum all items in parallel with NUMA-aware local accumulators.
parallel.rs:833 ↗
pub fn sum<T>(self) -> Result<T>
where
    T: Send + Sync + Default + Copy + AddAssign<I::Item> + Add<Output = T>,

Works for owned values, such as usize and u64, and for references, such as &u64.

Examples

use forkunion::*;
let topology = Topology::new().unwrap();
let mut pool = ThreadPool::spawn(&topology, 4).unwrap();
let data = vec![1u64, 2, 3, 4, 5];
let sum: u64 = (&data[..]).into_par_iter().with_pool(&mut pool).sum().unwrap();
assert_eq!(sum, 15);
countCount all items in parallel with NUMA-aware local counters.
parallel.rs:850 ↗
pub fn count(self) -> Result<usize>

Examples

use forkunion::*;
let topology = Topology::new().unwrap();
let mut pool = ThreadPool::spawn(&topology, 4).unwrap();
let data: Vec<usize> = (0..1000).collect();
let count = (&data[..]).into_par_iter().with_pool(&mut pool).count();
TraitIntoParallelIteratorConversion into a ParallelIterator.3
parallel.rs:856 ↗
pub trait IntoParallelIterator

Implementors

ItemThe item the resulting iterator yields.
parallel.rs:858 ↗
type Item;
IterThe resulting iterator.
parallel.rs:860 ↗
type Iter: ParallelIterator<Item = Self::Item>;
into_par_iterConverts self into a parallel iterator.
parallel.rs:863 ↗
fn into_par_iter(self) -> Self::Iter
StructMapA parallel iterator that transforms every item of its base.
StructFilterA parallel iterator that keeps only the items of its base that satisfy a predicate.
parallel.rs:929 ↗
pub struct Filter<I, P>

Implements

StructParallelSliceA parallel iterator over the elements of a shared slice.3
parallel.rs:963 ↗
pub struct ParallelSlice<'a, T>

Implements

newWraps a slice.
parallel.rs:970 ↗
pub fn new(data: &'a [T]) -> Self
for_each_*× 4Calls function for every element under the static schedule.
parallel.rs:975 ↗
pub fn for_each_static<F>(self, pool: &mut ThreadPool, function: F) -> Result<()>
where
    T: Sync,
    F: Fn(&'a T, usize, ThreadInDomain) + Sync,
Symbol
for_each_static
for_each_dynamic
for_each_static
for_each_dynamic
zipPairs the elements of two slices by position.
parallel.rs:994 ↗
pub fn zip<'b, U>(self, other: ParallelSlice<'b, U>) -> ParallelSliceZip<'a, 'b, T, U>
StructParallelSliceZipA parallel iterator over the paired elements of two shared slices.
parallel.rs:1030 ↗
pub struct ParallelSliceZip<'a, 'b, T, U>

Implements

StructParallelSliceMutA parallel iterator over the elements of an exclusive slice.1
parallel.rs:1070 ↗
pub struct ParallelSliceMut<'a, T>

Implements

newWraps a slice.
parallel.rs:1079 ↗
pub fn new(data: &'a mut [T]) -> Self
StructParallelRangeA parallel iterator over a range of indices.1
parallel.rs:1135 ↗
pub struct ParallelRange

Implements

newWraps a range.
parallel.rs:1142 ↗
pub fn new(range: Range<usize>) -> Self
StructParallelExactIterA parallel iterator that builds each item from its task index.1
parallel.rs:1173 ↗
pub struct ParallelExactIter<T, I>

Implements

newBuilds an iterator of len items, each produced by indexer from its index.
parallel.rs:1185 ↗
pub fn new(len: usize, indexer: I) -> Self
StructCacheAlignedCache-line aligned wrapper to prevent false sharing between threads.
types.rs:203 ↗
pub struct CacheAligned<T>(pub T);

When multiple threads access separate data that resides on the same cache line, modifications by one thread invalidate the cache line for all others, causing performance degradation known as "false sharing".

This wrapper ensures each wrapped value occupies its own DEFAULT_ALIGNMENT bytes, eliminating false sharing at the cost of increased memory usage.

Fields

0T

Examples

use forkunion::{CacheAligned, ThreadPool, Topology};

let topology = Topology::new().unwrap();
let mut pool = ThreadPool::spawn(&topology, 4).unwrap();
let data: Vec<usize> = (0..1000).collect();

// Each thread gets its own cache-aligned accumulator
let mut scratch: Vec<CacheAligned<usize>> =
    (0..pool.threads_count()).map(|_| CacheAligned(0)).collect();

// No false sharing during parallel reduction
for value in &data {
    let tid = *value % pool.threads_count();
    scratch[tid].0 += value;
}

let total: usize = scratch.iter().map(|a| a.0).sum();

Implements

Clone, Copy, Debug, Default

Functions

fold_with_scratchFolds every item of iterator into the per-thread slot of scratch that runs it.
scheduling.rs:1214 ↗
pub fn fold_with_scratch<I, S, T, F>(
    pool: &mut ThreadPool,
    iterator: I,
    schedule: S,
    scratch: &mut [T],
    fold: F,
) -> Result<()>
where
    I: ParallelIterator,
    S: ParallelSchedule,
    T: Send,
    F: Fn(&mut T, I::Item, usize, ThreadInDomain) + Sync,