Performant parallel computations with an expressive iterator API.
Rust
315
1,528 commits
updated Sep 20, 2026
Performant parallel computations with an expressive iterator API.
The crate focuses on practical parallelization through a convenient iterator API, with support for:
In many pipelines, parallelization is as simple as iter → par, into_iter → into_par and iter_mut → par_mut substitutions.
use orx_parallel::*;
use rand::prelude::*;
struct Tour(Vec<usize>);
impl Tour {
fn random(n: usize) -> Self {
let mut cities: Vec<_> = (0..n).collect();
cities.shuffle(&mut rand::rng());
Self(cities)
}
fn starts_at_coffee_shop(&self) -> bool {
self.0.first() == Some(&7)
}
fn duration(&self) -> u64 {
let links = self.0.iter().zip(self.0.iter().skip(1));
links
.map(|(a, b)| (*a as i64 - *b as i64).unsigned_abs())
.sum::<u64>()
}
}
let num_tours = 1_000_000;
let num_cities = 10;
// sequential
let best_tour = (0..num_tours)
.map(|_| Tour::random(num_cities))
.filter(|t| t.starts_at_coffee_shop())
.min_by_key(|t| t.duration());
// parallel
let best_tour = (0..num_tours)
.par() // ← parallelized
.map(|_| Tour::random(num_cities)) // ← rest is the same as seq code
.filter(|t| t.starts_at_coffee_shop())
.min_by_key(|t| t.duration());
Common inputs are directly supported, including:
VecDequepar_drain)Any regular iterator can be parallelized with iter_into_par().
use orx_parallel::*;
fn par_compute(inputs: impl Iterator<Item = u64>) -> u64 {
inputs
.iter_into_par() // ← parallelization over arbitrary iterator
.filter(|x| !x.is_multiple_of(7))
.sum()
}
let numbers = vec![4, 2, 9, 14, 1];
assert_eq!(par_compute(numbers.iter().copied()), 16);
let iter = (0u64..100).filter(|x| !x.is_power_of_two());
assert_eq!(par_compute(iter), 4088);
This makes it possible to parallelize computations on all iterable collections; on maps or sets, for instance.
use orx_parallel::*;
use std::collections::HashMap;
let mut map: HashMap<_, _> = (0..1024).map(|x| (x.to_string(), x)).collect();
map.values_mut()
.iter_into_par()
.filter(|x| **x % 2 == 0)
.for_each(|x| *x *= 2);
This broad path is generic, rather than being optimized for a specific collection. It works across many iterator sources and is especially useful when each task is substantial relative to parallelization overhead.
orx-parallel builds on concurrent iterator traits from orx-concurrent-iter.
If a collection provides a suitable concurrent iterator implementation (for example IntoConcurrentIter / ConcurrentIterable), it can integrate naturally with orx-parallel.
In practice, this means collection-specific parallelization can live in the collection crate itself, where internals are available for optimized implementations. If you need help with a ConcurrentIter implementation, please open an issue.
Fallible parallel flows are a core feature.
into_optional() for Option<T> pipelinesinto_fallible() for Result<T, E> pipelinesAfter the transformation, you continue writing only the success path, similar in spirit to using ? in regular Rust code. Any failure short-circuits with early exit.
use orx_parallel::*;
fn parse_qty_and_price(row: &str) -> Option<(u64, u64)> {
let mut parts = row.split(',');
let qty = parts.next()?.parse::<u64>().ok()?;
let unit_price = parts.next()?.parse::<u64>().ok()?;
Some((qty, unit_price))
}
fn total_price(rows: &[&str]) -> Option<u64> {
rows.par()
.map(|row| parse_qty_and_price(row)) // ← some might return None
.into_optional() // ← ascend
.filter(|(qty, _)| *qty >= 2) // ← focus only on success path
.map(|(qty, unit_price)| qty * unit_price) // ← success path
.sum()
}
assert_eq!(total_price(&["1,2300", "4,499", "5,1100"]), Some(7496));
assert_eq!(total_price(&["1,2300", "4,???", "5,1100"]), None);
orx-parallel is not tied to any specific thread pool; it can work with transient threads or persistent thread pools. By default, the library uses the persistent built-in BasicPool, which reuses its workers across computations.
You can configure the pool by features and the ORX_NUM_THREADS environment variable; if the environment variable is set, it is used as the thread limit, otherwise the pool can use all available threads.
# default: BasicPool (persistent workers, reused across computations)
orx-parallel = { version = "4.0" }
# transient pool: spawn threads, compute, and join for each computation
orx-parallel = { version = "4.0", features = ["transient-pool"] }
# rayon-core pool integration
orx-parallel = { version = "4.0", features = ["persistent-pool-rayon"] }
Pool Selection & Tradeoffs:
The pool's scheduling strategy is usually less important than the work being performed. BasicPool (the default) is suitable for most applications—its workers are created once and kept alive, avoiding the overhead of spawning and joining threads for each parallel computation.
If your application performs only occasional parallel computations and should not retain worker threads between them, enable the transient-pool feature. This selects OncePool, which spawns the required threads just before a computation and joins them immediately after. The tradeoff is the cost of thread creation and cleanup on each parallel operation.
Consider a parallel computation of W tasks to be executed by N threads. The number of thread
spawncalls inOncePoolis N, regardless of how large W is.
In addition, you can conveniently tune the thread count for each individual computation:
use orx_parallel::*;
let result: Vec<_> = (0..1000)
.par() // ← can use all threads in the pool
.map(|x| x * 2)
.num_threads(4) // ← limit this computation to use <=4 threads
.collect();
assert_eq!(result.len(), 1000);
The ThreadPool trait is small and straightforward to implement. Since thread pools are independent of runner strategies, you can plug in a custom pool as follows:
use orx_parallel::*;
let runner = Runner::adaptive_with_pool(MyPool::new());
let sum = (0..1000)
.par()
.runner(runner) // ← using adaptive runner with my pool
.sum();
Please see thread_usage.md for detailed information.
Ad-hoc Parallel Computation:
The thread pool itself is also exposed directly through Pool::global().
use orx_parallel::*;
fn prepare_breakfast(ingredients: &[&str]) {}
fn pack_lunch(ingredients: &[&str]) {}
let ingredients = vec!["apple", "tomato"];
Pool::global().scope(|s| {
s.run(|| prepare_breakfast(&ingredients));
s.run(|| pack_lunch(&ingredients));
});
// or
let tasks = tasks![
|| prepare_breakfast(&ingredients),
|| pack_lunch(&ingredients)
];
Pool::global().run_all(tasks);
Note that the tasks are not boxed. On the other hand, this approach bypasses the concurrent iterator and runner strategy optimizations that parallel iterators rely on, so it is best suited for a few large, independent tasks rather than many small ones.
Every parallel iterator can also run sequentially on the calling thread:
.num_threads(1) to keep the parallel pipeline API while disabling parallel execution;.into_iter() to consume the pipeline as a regular sequential iterator.Both options avoid spawning worker threads and avoid using the thread pool.
Scheduling is abstracted by ParRunner and selected with .runner(...).
Built-in runners:
Runner::adaptive(): adaptive chunking strategy (default with std feature)Runner::fixed(): pre-computed fixed chunking strategy (default in no-std builds)use orx_parallel::*; // assume default features used: ["std"]
let sum: usize = (0..10_000)
.par()
.map(|x| x + 1)
.sum(); // ← uses adaptive runner by default
assert_eq!(sum, (1..=10_000).sum());
let sum: usize = (0..10_000)
.par()
.runner(Runner::fixed()) // ← uses fixed runner
.map(|x| x + 1)
.sum();
assert_eq!(sum, (1..=10_000).sum());
You may also implement your own ParRunner, either to tune a specific workload or to explore different scheduling ideas.
For implementation guidance, see parallel_runner.md.
use transformations provide a safe and ergonomic way to use mutable thread-local state in parallel pipelines:
For example, rather than allocating a new String for every element, we can reuse one scratch buffer per worker thread:
use orx_parallel::*;
let words = vec!["Love Rust ", " Hello WORLD", "?"];
// one reusable scratch buffer per thread, instead of allocating for every element
let mut buffers = UseVec::new(|_th_idx| String::new());
let greetings: Vec<String> = words
.par()
.use_vec(&mut buffers) // ← mutably lend it to parallel iterator
.filter_map(|buf, w| {
buf.clear(); // ← buf: &mut String, reused across elements on this thread
buf.push_str(w.trim());
buf.make_ascii_lowercase();
buf.find(' ')
})
.map(|buf, space_idx| buf.chars().skip(space_idx + 1).collect())
.collect();
assert_eq!(greetings, ["rust", "world"]);
For practical use cases, please see use_transformation.md.
Parallel traversal over recursive structures (such as trees or graphs) is supported out of the box without losing convenient iterator ergonomics.
Even though new work is discovered dynamically, deterministic traversal is still possible: with the default ordered mode, order-sensitive operations follow breadth-first order.
Notice below that after the par_recursive call, we use regular iterator methods without additional complexity.
// provide initial tasks and define how to explore new ones
let result = par_recursive([root], |node| &node.children)
.map(process_node) // ← regular iterator transformations
.reduce(merge_agg);
For practical examples, see:
orx-parallel supports browser-hosted wasm with dedicated examples and guides.
docs/wasm.mddocs/wasm_internals.mdThe crate is benchmarked with the goal of maintaining practical performance and guiding future improvements. The benchmarks live in a separate repository so each benchmark can run in isolation with accurate measurements, especially when comparing different thread pools.
You can also use the benchmark repository as a starting point for measuring your own computations.
Contributions are welcome! If you notice an error, have a question or think something could be improved, please open an issue or create a PR.
The crate provides an experimental feature flag for new capabilities that are actively under development and optimization work. For example, par_experimental_sort is a parallel slice sorting implementation currently undergoing evaluation and tuning. Contributions, alternative algorithm designs, performance optimizations, and benchmarks for experimental features are very welcome!
Parallel runner strategies are open for research and improvement. You can start by looking at the current adaptive and fixed runners, then experiment with a new ParRunner implementation.
A useful workflow is to run the tests in this repository and use the orx-parallel-benchmarks repository to measure the performance impact. Benchmark manifests can point to your own branch; to benchmark your runner as the default, update the DefaultRunner alias and default_runner() wiring in src/runner/mod.rs on that branch. You can also use the benchmark repository as a template for measuring your own specific computation.
If there is an input type or collection you would like to parallelize, please open an issue. Collection-specific support can often be added by implementing the appropriate ConcurrentIter integration in the collection crate.
Dual-licensed under Apache 2.0 or MIT.
Rust
99.9%
Performant parallel computations with an expressive iterator API.
Rust
315
1,528 commits
updated Sep 20, 2026
Performant parallel computations with an expressive iterator API.
The crate focuses on practical parallelization through a convenient iterator API, with support for:
In many pipelines, parallelization is as simple as iter → par, into_iter → into_par and iter_mut → par_mut substitutions.
use orx_parallel::*;
use rand::prelude::*;
struct Tour(Vec<usize>);
impl Tour {
fn random(n: usize) -> Self {
let mut cities: Vec<_> = (0..n).collect();
cities.shuffle(&mut rand::rng());
Self(cities)
}
fn starts_at_coffee_shop(&self) -> bool {
self.0.first() == Some(&7)
}
fn duration(&self) -> u64 {
let links = self.0.iter().zip(self.0.iter().skip(1));
links
.map(|(a, b)| (*a as i64 - *b as i64).unsigned_abs())
.sum::<u64>()
}
}
let num_tours = 1_000_000;
let num_cities = 10;
// sequential
let best_tour = (0..num_tours)
.map(|_| Tour::random(num_cities))
.filter(|t| t.starts_at_coffee_shop())
.min_by_key(|t| t.duration());
// parallel
let best_tour = (0..num_tours)
.par() // ← parallelized
.map(|_| Tour::random(num_cities)) // ← rest is the same as seq code
.filter(|t| t.starts_at_coffee_shop())
.min_by_key(|t| t.duration());
Common inputs are directly supported, including:
VecDequepar_drain)Any regular iterator can be parallelized with iter_into_par().
use orx_parallel::*;
fn par_compute(inputs: impl Iterator<Item = u64>) -> u64 {
inputs
.iter_into_par() // ← parallelization over arbitrary iterator
.filter(|x| !x.is_multiple_of(7))
.sum()
}
let numbers = vec![4, 2, 9, 14, 1];
assert_eq!(par_compute(numbers.iter().copied()), 16);
let iter = (0u64..100).filter(|x| !x.is_power_of_two());
assert_eq!(par_compute(iter), 4088);
This makes it possible to parallelize computations on all iterable collections; on maps or sets, for instance.
use orx_parallel::*;
use std::collections::HashMap;
let mut map: HashMap<_, _> = (0..1024).map(|x| (x.to_string(), x)).collect();
map.values_mut()
.iter_into_par()
.filter(|x| **x % 2 == 0)
.for_each(|x| *x *= 2);
This broad path is generic, rather than being optimized for a specific collection. It works across many iterator sources and is especially useful when each task is substantial relative to parallelization overhead.
orx-parallel builds on concurrent iterator traits from orx-concurrent-iter.
If a collection provides a suitable concurrent iterator implementation (for example IntoConcurrentIter / ConcurrentIterable), it can integrate naturally with orx-parallel.
In practice, this means collection-specific parallelization can live in the collection crate itself, where internals are available for optimized implementations. If you need help with a ConcurrentIter implementation, please open an issue.
Fallible parallel flows are a core feature.
into_optional() for Option<T> pipelinesinto_fallible() for Result<T, E> pipelinesAfter the transformation, you continue writing only the success path, similar in spirit to using ? in regular Rust code. Any failure short-circuits with early exit.
use orx_parallel::*;
fn parse_qty_and_price(row: &str) -> Option<(u64, u64)> {
let mut parts = row.split(',');
let qty = parts.next()?.parse::<u64>().ok()?;
let unit_price = parts.next()?.parse::<u64>().ok()?;
Some((qty, unit_price))
}
fn total_price(rows: &[&str]) -> Option<u64> {
rows.par()
.map(|row| parse_qty_and_price(row)) // ← some might return None
.into_optional() // ← ascend
.filter(|(qty, _)| *qty >= 2) // ← focus only on success path
.map(|(qty, unit_price)| qty * unit_price) // ← success path
.sum()
}
assert_eq!(total_price(&["1,2300", "4,499", "5,1100"]), Some(7496));
assert_eq!(total_price(&["1,2300", "4,???", "5,1100"]), None);
orx-parallel is not tied to any specific thread pool; it can work with transient threads or persistent thread pools. By default, the library uses the persistent built-in BasicPool, which reuses its workers across computations.
You can configure the pool by features and the ORX_NUM_THREADS environment variable; if the environment variable is set, it is used as the thread limit, otherwise the pool can use all available threads.
# default: BasicPool (persistent workers, reused across computations)
orx-parallel = { version = "4.0" }
# transient pool: spawn threads, compute, and join for each computation
orx-parallel = { version = "4.0", features = ["transient-pool"] }
# rayon-core pool integration
orx-parallel = { version = "4.0", features = ["persistent-pool-rayon"] }
Pool Selection & Tradeoffs:
The pool's scheduling strategy is usually less important than the work being performed. BasicPool (the default) is suitable for most applications—its workers are created once and kept alive, avoiding the overhead of spawning and joining threads for each parallel computation.
If your application performs only occasional parallel computations and should not retain worker threads between them, enable the transient-pool feature. This selects OncePool, which spawns the required threads just before a computation and joins them immediately after. The tradeoff is the cost of thread creation and cleanup on each parallel operation.
Consider a parallel computation of W tasks to be executed by N threads. The number of thread
spawncalls inOncePoolis N, regardless of how large W is.
In addition, you can conveniently tune the thread count for each individual computation:
use orx_parallel::*;
let result: Vec<_> = (0..1000)
.par() // ← can use all threads in the pool
.map(|x| x * 2)
.num_threads(4) // ← limit this computation to use <=4 threads
.collect();
assert_eq!(result.len(), 1000);
The ThreadPool trait is small and straightforward to implement. Since thread pools are independent of runner strategies, you can plug in a custom pool as follows:
use orx_parallel::*;
let runner = Runner::adaptive_with_pool(MyPool::new());
let sum = (0..1000)
.par()
.runner(runner) // ← using adaptive runner with my pool
.sum();
Please see thread_usage.md for detailed information.
Ad-hoc Parallel Computation:
The thread pool itself is also exposed directly through Pool::global().
use orx_parallel::*;
fn prepare_breakfast(ingredients: &[&str]) {}
fn pack_lunch(ingredients: &[&str]) {}
let ingredients = vec!["apple", "tomato"];
Pool::global().scope(|s| {
s.run(|| prepare_breakfast(&ingredients));
s.run(|| pack_lunch(&ingredients));
});
// or
let tasks = tasks![
|| prepare_breakfast(&ingredients),
|| pack_lunch(&ingredients)
];
Pool::global().run_all(tasks);
Note that the tasks are not boxed. On the other hand, this approach bypasses the concurrent iterator and runner strategy optimizations that parallel iterators rely on, so it is best suited for a few large, independent tasks rather than many small ones.
Every parallel iterator can also run sequentially on the calling thread:
.num_threads(1) to keep the parallel pipeline API while disabling parallel execution;.into_iter() to consume the pipeline as a regular sequential iterator.Both options avoid spawning worker threads and avoid using the thread pool.
Scheduling is abstracted by ParRunner and selected with .runner(...).
Built-in runners:
Runner::adaptive(): adaptive chunking strategy (default with std feature)Runner::fixed(): pre-computed fixed chunking strategy (default in no-std builds)use orx_parallel::*; // assume default features used: ["std"]
let sum: usize = (0..10_000)
.par()
.map(|x| x + 1)
.sum(); // ← uses adaptive runner by default
assert_eq!(sum, (1..=10_000).sum());
let sum: usize = (0..10_000)
.par()
.runner(Runner::fixed()) // ← uses fixed runner
.map(|x| x + 1)
.sum();
assert_eq!(sum, (1..=10_000).sum());
You may also implement your own ParRunner, either to tune a specific workload or to explore different scheduling ideas.
For implementation guidance, see parallel_runner.md.
use transformations provide a safe and ergonomic way to use mutable thread-local state in parallel pipelines:
For example, rather than allocating a new String for every element, we can reuse one scratch buffer per worker thread:
use orx_parallel::*;
let words = vec!["Love Rust ", " Hello WORLD", "?"];
// one reusable scratch buffer per thread, instead of allocating for every element
let mut buffers = UseVec::new(|_th_idx| String::new());
let greetings: Vec<String> = words
.par()
.use_vec(&mut buffers) // ← mutably lend it to parallel iterator
.filter_map(|buf, w| {
buf.clear(); // ← buf: &mut String, reused across elements on this thread
buf.push_str(w.trim());
buf.make_ascii_lowercase();
buf.find(' ')
})
.map(|buf, space_idx| buf.chars().skip(space_idx + 1).collect())
.collect();
assert_eq!(greetings, ["rust", "world"]);
For practical use cases, please see use_transformation.md.
Parallel traversal over recursive structures (such as trees or graphs) is supported out of the box without losing convenient iterator ergonomics.
Even though new work is discovered dynamically, deterministic traversal is still possible: with the default ordered mode, order-sensitive operations follow breadth-first order.
Notice below that after the par_recursive call, we use regular iterator methods without additional complexity.
// provide initial tasks and define how to explore new ones
let result = par_recursive([root], |node| &node.children)
.map(process_node) // ← regular iterator transformations
.reduce(merge_agg);
For practical examples, see:
orx-parallel supports browser-hosted wasm with dedicated examples and guides.
docs/wasm.mddocs/wasm_internals.mdThe crate is benchmarked with the goal of maintaining practical performance and guiding future improvements. The benchmarks live in a separate repository so each benchmark can run in isolation with accurate measurements, especially when comparing different thread pools.
You can also use the benchmark repository as a starting point for measuring your own computations.
Contributions are welcome! If you notice an error, have a question or think something could be improved, please open an issue or create a PR.
The crate provides an experimental feature flag for new capabilities that are actively under development and optimization work. For example, par_experimental_sort is a parallel slice sorting implementation currently undergoing evaluation and tuning. Contributions, alternative algorithm designs, performance optimizations, and benchmarks for experimental features are very welcome!
Parallel runner strategies are open for research and improvement. You can start by looking at the current adaptive and fixed runners, then experiment with a new ParRunner implementation.
A useful workflow is to run the tests in this repository and use the orx-parallel-benchmarks repository to measure the performance impact. Benchmark manifests can point to your own branch; to benchmark your runner as the default, update the DefaultRunner alias and default_runner() wiring in src/runner/mod.rs on that branch. You can also use the benchmark repository as a template for measuring your own specific computation.
If there is an input type or collection you would like to parallelize, please open an issue. Collection-specific support can often be added by implementing the appropriate ConcurrentIter integration in the collection crate.
Dual-licensed under Apache 2.0 or MIT.
Rust
99.9%