orx-parallel
Performant parallel computations with an expressive iterator API.
The crate focuses on practical parallelization through a convenient iterator API, with support for:
- first-class fallible flows,
- configurable resource usage,
- safe per-thread mutable state,
- recursive traversal on non-linear data,
- WebAssembly support,
- determinism,
- customizable runner strategies to enable experimentation and advanced tuning.
Parallelization with Iterator Ergonomics
In many pipelines, parallelization is as simple as iter → par, into_iter → into_par and iter_mut → par_mut substitutions.
use *;
use *;
;
let num_tours = 1_000_000;
let num_cities = 10;
// sequential
let best_tour =
.map
.filter
.min_by_key;
// parallel
let best_tour =
.par // ← parallelized
.map // ← rest is the same as seq code
.filter
.min_by_key;
What Can Be Parallelized?
1. Direct collection support
Common inputs are directly supported, including:
- vectors and slices
VecDeque- ranges
- draining iterators (
par_drain)
2. Any arbitrary iterator
Any regular iterator can be parallelized with iter_into_par().
use *;
let numbers = vec!;
assert_eq!;
let iter = .filter;
assert_eq!;
This makes it possible to parallelize computations on all iterable collections; on maps or sets, for instance.
use *;
use HashMap;
let mut map: = .map.collect;
map.values_mut
.iter_into_par
.filter
.for_each;
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.
3. Extensible via concurrent iterator abstractions
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.
First-Class Fallible Computation
Fallible parallel flows are a core feature.
into_optional()forOption<T>pipelinesinto_fallible()forResult<T, E>pipelines
After 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 *;
assert_eq!;
assert_eq!;
Configurable Resource Usage
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)
= { = "4.0" }
# transient pool: spawn threads, compute, and join for each computation
= { = "4.0", = ["transient-pool"] }
# rayon-core pool integration
= { = "4.0", = ["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 *;
let result: =
.par // ← can use all threads in the pool
.map
.num_threads // ← limit this computation to use <=4 threads
.collect;
assert_eq!;
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 *;
let runner = adaptive_with_pool;
let sum =
.par
.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 *;
let ingredients = vec!;
global.scope;
// or
let tasks = tasks!;
global.run_all;
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.
Sequential Execution
Every parallel iterator can also run sequentially on the calling thread:
- use
.num_threads(1)to keep the parallel pipeline API while disabling parallel execution; - use
.into_iter()to consume the pipeline as a regular sequential iterator.
Both options avoid spawning worker threads and avoid using the thread pool.
Runner Strategies and Extensibility
Scheduling is abstracted by ParRunner and selected with .runner(...).
Built-in runners:
Runner::adaptive(): adaptive chunking strategy (default withstdfeature)Runner::fixed(): pre-computed fixed chunking strategy (default inno-stdbuilds)
use *; // assume default features used: ["std"]
let sum: usize =
.par
.map
.sum; // ← uses adaptive runner by default
assert_eq!;
let sum: usize =
.par
.runner // ← uses fixed runner
.map
.sum;
assert_eq!;
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: Safe Mutable Per-Thread State
use transformations provide a safe and ergonomic way to use mutable thread-local state in parallel pipelines:
- no unsafe code in application-level iterator logic
- exactly one use-variable per worker thread
- minimized and deterministic allocation behavior for stateful workloads
For example, rather than allocating a new String for every element, we can reuse one scratch buffer per worker thread:
use *;
let words = vec!;
// one reusable scratch buffer per thread, instead of allocating for every element
let mut buffers = new;
let greetings: = words
.par
.use_vec // ← mutably lend it to parallel iterator
.filter_map
.map
.collect;
assert_eq!;
For practical use cases, please see use_transformation.md.
Recursive Iterators for Non-Linear Data
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.
let result = par_recursive // ← initial tasks and how to explore new ones
.map // ← we process nodes as if they were in a linear data structure
.reduce;
For practical examples, see:
WASM Support
orx-parallel supports browser-hosted wasm with dedicated examples and guides.
- live demo: https://orx-parallel-wasm-demo-tsp.pages.dev/
- tutorial: https://orx-parallel-wasm-tutorials.pages.dev/
- demo and tutorial sources: https://github.com/orxfun/orx-parallel-wasm-demos
- wasm guide:
docs/wasm.md - internals:
docs/wasm_internals.md
Performance and Benchmarks
The 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.
- Live benchmark dashboard: https://orx-parallel-benchmarks.pages.dev/ displays results generated from the benchmark repository.
- Benchmark sources: https://github.com/orxfun/orx-parallel-benchmarks
You can also use the benchmark repository as a starting point for measuring your own computations.
Contributing
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.
Experimental Features
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!
Research & Runner Development
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.
New Parallelizable Collection
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.
License
Dual-licensed under Apache 2.0 or MIT.