From d41f7ab6aad0ba7b628c9fb4d70abc1cd73545ae Mon Sep 17 00:00:00 2001 From: orxfun Date: Tue, 7 Oct 2025 15:36:06 +0200 Subject: [PATCH 01/96] define IntoParIterRec trait --- src/into_par_iter_rec.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 src/into_par_iter_rec.rs diff --git a/src/into_par_iter_rec.rs b/src/into_par_iter_rec.rs new file mode 100644 index 00000000..3d0ec804 --- /dev/null +++ b/src/into_par_iter_rec.rs @@ -0,0 +1,18 @@ +use crate::{DefaultRunner, computational_variants::Par}; +use orx_concurrent_recursive_iter::ConcurrentRecursiveIter; + +pub trait IntoParIterRec +where + Self: IntoIterator, + ::IntoIter: ExactSizeIterator, + ::Item: Send, +{ + fn into_par_rec( + self, + extend: E, + ) -> Par::Item, E, I>, DefaultRunner> + where + I: IntoIterator::Item>, + I::IntoIter: ExactSizeIterator, + E: Fn(&::Item) -> I + Sync; +} From 1ed526a588e95aad006323c1f1c68335270ba8a3 Mon Sep 17 00:00:00 2001 From: orxfun Date: Tue, 7 Oct 2025 15:37:31 +0200 Subject: [PATCH 02/96] export IntoParIterRec --- Cargo.toml | 15 ++++++++------- examples/par_recursive_iter.rs | 1 + src/lib.rs | 2 ++ 3 files changed, 11 insertions(+), 7 deletions(-) create mode 100644 examples/par_recursive_iter.rs diff --git a/Cargo.toml b/Cargo.toml index 79eb1b69..38db0842 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,16 +11,17 @@ keywords = ["parallel", "concurrency", "performance", "thread", "iterator"] categories = ["concurrency", "algorithms"] [dependencies] -orx-pinned-vec = { version = "3.17.0", default-features = false } -orx-fixed-vec = { version = "3.19.0", default-features = false } -orx-split-vec = { version = "3.19.0", default-features = false } -orx-concurrent-iter = { version = "3.1.0", default-features = false } -orx-concurrent-bag = { version = "3.1.0", default-features = false } -orx-concurrent-ordered-bag = { version = "3.1.0", default-features = false } +orx-pinned-vec = { version = "3.20.0", default-features = false } +orx-fixed-vec = { version = "3.21.0", default-features = false } +orx-split-vec = { version = "3.21.0", default-features = false } +orx-concurrent-iter = { version = "3.2.0", default-features = false } +orx-concurrent-bag = { version = "3.3.0", default-features = false } +orx-concurrent-ordered-bag = { version = "3.3.0", default-features = false } +orx-pinned-concurrent-col = { version = "2.17.0", default-features = false } orx-iterable = { version = "1.3.0", default-features = false } -orx-pinned-concurrent-col = { version = "2.15.0", default-features = false } orx-priority-queue = { version = "1.7.0", default-features = false } orx-pseudo-default = { version = "2.1.0", default-features = false } +orx-concurrent-recursive-iter = { git = "https://github.com/orxfun/orx-concurrent-recursive-iter" } # optional: generic iterator rayon = { version = "1.11.0", optional = true, default-features = false } diff --git a/examples/par_recursive_iter.rs b/examples/par_recursive_iter.rs new file mode 100644 index 00000000..f328e4d9 --- /dev/null +++ b/examples/par_recursive_iter.rs @@ -0,0 +1 @@ +fn main() {} diff --git a/src/lib.rs b/src/lib.rs index 728e05b4..7df05ea1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -27,6 +27,7 @@ pub mod executor; mod generic_values; mod heap_sort; mod into_par_iter; +mod into_par_iter_rec; /// Module for creating special iterators. pub mod iter; mod iter_into_par_iter; @@ -60,6 +61,7 @@ mod test_utils; pub use collect_into::ParCollectInto; pub use executor::{DefaultExecutor, ParallelExecutor, ThreadExecutor}; pub use into_par_iter::IntoParIter; +pub use into_par_iter_rec::IntoParIterRec; pub use iter_into_par_iter::IterIntoParIter; pub use par_iter::ParIter; pub use par_iter_option::ParIterOption; From a4cda01adb3e06ea915537702c740015555e3090 Mon Sep 17 00:00:00 2001 From: orxfun Date: Tue, 7 Oct 2025 15:40:49 +0200 Subject: [PATCH 03/96] relax exact size requirement on the IntoParIterRec supertrait --- src/into_par_iter_rec.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/into_par_iter_rec.rs b/src/into_par_iter_rec.rs index 3d0ec804..2c8a2d38 100644 --- a/src/into_par_iter_rec.rs +++ b/src/into_par_iter_rec.rs @@ -4,7 +4,6 @@ use orx_concurrent_recursive_iter::ConcurrentRecursiveIter; pub trait IntoParIterRec where Self: IntoIterator, - ::IntoIter: ExactSizeIterator, ::Item: Send, { fn into_par_rec( From 78b2d7ca08acfad838ecf21da63fdea2f6d0501f Mon Sep 17 00:00:00 2001 From: orxfun Date: Tue, 7 Oct 2025 15:42:57 +0200 Subject: [PATCH 04/96] IntoParIterRec is implemented for all sequential iterators --- src/into_par_iter_rec.rs | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/src/into_par_iter_rec.rs b/src/into_par_iter_rec.rs index 2c8a2d38..c1d27258 100644 --- a/src/into_par_iter_rec.rs +++ b/src/into_par_iter_rec.rs @@ -1,10 +1,25 @@ -use crate::{DefaultRunner, computational_variants::Par}; +use crate::{DefaultRunner, Params, computational_variants::Par}; use orx_concurrent_recursive_iter::ConcurrentRecursiveIter; pub trait IntoParIterRec where Self: IntoIterator, - ::Item: Send, + Self::Item: Send, +{ + fn into_par_rec( + self, + extend: E, + ) -> Par, DefaultRunner> + where + I: IntoIterator, + I::IntoIter: ExactSizeIterator, + E: Fn(&Self::Item) -> I + Sync; +} + +impl IntoParIterRec for X +where + X: IntoIterator, + X::Item: Send, { fn into_par_rec( self, @@ -13,5 +28,9 @@ where where I: IntoIterator::Item>, I::IntoIter: ExactSizeIterator, - E: Fn(&::Item) -> I + Sync; + E: Fn(&::Item) -> I + Sync, + { + let con_rec_iter = ConcurrentRecursiveIter::new(extend, self); + Par::new(DefaultRunner::default(), Params::default(), con_rec_iter) + } } From f55e5863448c12c40fb131e478378b68de890c3b Mon Sep 17 00:00:00 2001 From: orxfun Date: Tue, 7 Oct 2025 16:29:33 +0200 Subject: [PATCH 05/96] implement par-recursive-iter example --- examples/par_recursive_iter.rs | 61 +++++++++++++++++++++++++++++++++- src/into_par_iter_rec.rs | 6 ++-- 2 files changed, 63 insertions(+), 4 deletions(-) diff --git a/examples/par_recursive_iter.rs b/examples/par_recursive_iter.rs index f328e4d9..a4f40dff 100644 --- a/examples/par_recursive_iter.rs +++ b/examples/par_recursive_iter.rs @@ -1 +1,60 @@ -fn main() {} +use orx_parallel::{IntoParIterRec, ParIter}; +use rand::{Rng, SeedableRng}; +use rand_chacha::ChaCha8Rng; + +struct Node { + value: u64, + children: Vec, +} + +fn fibonacci(n: u64) -> u64 { + let n = n % 42; // let's not overflow + let mut a = 0; + let mut b = 1; + for _ in 0..n { + let c = a + b; + a = b; + b = c; + } + a +} + +impl Node { + fn new(rng: &mut impl Rng, value: u64) -> Self { + let num_children = match value { + 0 => 0, + n => rng.random_range(0..(n as usize)), + }; + let children = (0..num_children) + .map(|i| Self::new(rng, i as u64)) + .collect(); + Self { value, children } + } + + fn num_nodes(&self) -> usize { + 1 + self + .children + .iter() + .map(|node| node.num_nodes()) + .sum::() + } +} + +fn main() { + fn extend<'a, 'b>(node: &'a &'b Node) -> &'b [Node] { + &node.children + } + + let mut rng = ChaCha8Rng::seed_from_u64(42); + let root = Node::new(&mut rng, 300); + + let par = [&root].into_par_rec(extend); + let count = par.count(); + assert_eq!(count, root.num_nodes()); + println!("Tree contains {count} nodes"); + + let par = [&root].into_par_rec(extend); + let sum_fib = par.map(|x| fibonacci(x.value)).sum(); + assert_eq!(sum_fib, 4843403551); + println!("Sum of Fibonacci of node values is {sum_fib}"); +} diff --git a/src/into_par_iter_rec.rs b/src/into_par_iter_rec.rs index c1d27258..445c18b2 100644 --- a/src/into_par_iter_rec.rs +++ b/src/into_par_iter_rec.rs @@ -24,11 +24,11 @@ where fn into_par_rec( self, extend: E, - ) -> Par::Item, E, I>, DefaultRunner> + ) -> Par, DefaultRunner> where - I: IntoIterator::Item>, + I: IntoIterator, I::IntoIter: ExactSizeIterator, - E: Fn(&::Item) -> I + Sync, + E: Fn(&Self::Item) -> I + Sync, { let con_rec_iter = ConcurrentRecursiveIter::new(extend, self); Par::new(DefaultRunner::default(), Params::default(), con_rec_iter) From 6961d6873e262c21c6cad6abb1134f50c32922e1 Mon Sep 17 00:00:00 2001 From: orxfun Date: Fri, 17 Oct 2025 09:29:07 +0200 Subject: [PATCH 06/96] implement into_eager for Par with recursive con iter --- Cargo.toml | 2 +- benches/par_recursive_iter.rs | 172 ++++++++++++++++++ examples/par_recursive_iter.rs | 76 +++++++- src/executor/thread_compute/reduce.rs | 1 + src/iter/mod.rs | 2 + .../recursive/into_par_rec_iter.rs} | 0 src/iter/recursive/mod.rs | 4 + src/iter/recursive/rec_per_iter.rs | 19 ++ src/lib.rs | 5 +- 9 files changed, 267 insertions(+), 14 deletions(-) create mode 100644 benches/par_recursive_iter.rs rename src/{into_par_iter_rec.rs => iter/recursive/into_par_rec_iter.rs} (100%) create mode 100644 src/iter/recursive/mod.rs create mode 100644 src/iter/recursive/rec_per_iter.rs diff --git a/Cargo.toml b/Cargo.toml index 38db0842..931714ad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,7 +46,7 @@ rayon = "1.11.0" test-case = "3.3.1" [[bench]] -name = "find_iter_into_par" +name = "par_recursive_iter" harness = false [package.metadata.docs.rs] diff --git a/benches/par_recursive_iter.rs b/benches/par_recursive_iter.rs new file mode 100644 index 00000000..be07702f --- /dev/null +++ b/benches/par_recursive_iter.rs @@ -0,0 +1,172 @@ +use criterion::{BenchmarkId, Criterion, criterion_group, criterion_main}; +use orx_concurrent_recursive_iter::ConcurrentRecursiveIter; +use orx_parallel::*; +use rand::prelude::*; +use rand_chacha::ChaCha8Rng; +use std::{ + hint::black_box, + sync::atomic::{AtomicU64, Ordering}, +}; + +struct Node { + value: u64, + children: Vec, +} + +fn fibonacci(n: u64) -> u64 { + // let n = n % 42; // let's not overflow + (0..100) + .map(|i| { + let n = i + n; + let mut a = 0; + let mut b = 1; + for _ in 0..n { + let c = a + b; + a = b; + b = c; + } + a + }) + .sum() +} + +impl Node { + fn new(rng: &mut impl Rng, value: u64) -> Self { + let num_children = match value { + 0 => 0, + n => rng.random_range(0..(n as usize)), + }; + let children = (0..num_children) + .map(|i| Self::new(rng, i as u64)) + .collect(); + Self { value, children } + } + + fn seq_num_nodes(&self) -> usize { + 1 + self + .children + .iter() + .map(|node| node.seq_num_nodes()) + .sum::() + } + + fn seq_sum_fib(&self) -> u64 { + fibonacci(self.value) + self.children.iter().map(|x| x.seq_sum_fib()).sum::() + } +} + +fn seq(root: &Node) -> u64 { + root.seq_sum_fib() +} + +fn rayon(root: &Node) -> u64 { + fn process_node<'scope>(sum: &'scope AtomicU64, node: &'scope Node, s: &rayon::Scope<'scope>) { + for child in &node.children { + s.spawn(|s| { + process_node(sum, child, s); + }); + } + let val = fibonacci(node.value); + sum.fetch_add(val, Ordering::Relaxed); + } + + let sum = AtomicU64::new(0); + rayon::in_place_scope(|s| { + process_node(&sum, root, s); + }); + sum.into_inner() +} + +fn orx(root: &Node) -> u64 { + fn extend<'a, 'b>(node: &'a &'b Node) -> &'b [Node] { + &node.children + } + + [root] + .into_par_rec(extend) + .chunk_size(1024 * 64) + .map(|x| fibonacci(x.value)) + .sum() +} + +fn orx_static(root: &Node) -> u64 { + fn add_tasks<'a>(tasks: &mut Vec<&'a Node>, node: &'a Node) { + tasks.push(node); + for child in &node.children { + add_tasks(tasks, child); + } + } + let mut tasks = Vec::with_capacity(root.seq_num_nodes() + 1); + add_tasks(&mut tasks, root); + tasks.par().map(|x| fibonacci(x.value)).sum() +} + +fn iter(root: &Node) -> u64 { + use orx_concurrent_iter::*; + fn extend<'a, 'b>(node: &'a &'b Node) -> &'b [Node] { + &node.children + } + + let num_threads = 32; + let iter = ConcurrentRecursiveIter::new(extend, [root]); + let num_spawned = core::sync::atomic::AtomicUsize::new(0); + std::thread::scope(|s| { + let mut handles = vec![]; + for _ in 0..num_threads { + handles.push(s.spawn(|| { + // allow all threads to be spawned + _ = num_spawned.fetch_add(1, Ordering::Relaxed); + while num_spawned.load(Ordering::Relaxed) < num_threads {} + + // computation: parallel reduction + let mut thread_sum = 0; + let mut puller = iter.chunk_puller(1024); + while let Some(chunk) = puller.pull() { + thread_sum += chunk.into_iter().map(|x| fibonacci(x.value)).sum::(); + } + thread_sum + })); + } + + handles.into_iter().map(|x| x.join().unwrap()).sum() + }) +} + +fn run(c: &mut Criterion) { + let mut rng = ChaCha8Rng::seed_from_u64(42); + let root = Node::new(&mut rng, 250); + let n = &root.seq_num_nodes(); + let expected = root.seq_sum_fib(); + + let mut group = c.benchmark_group("par_recursive_iter"); + + group.bench_with_input(BenchmarkId::new("seq", n), n, |b, _| { + assert_eq!(&expected, &seq(&root)); + b.iter(|| seq(black_box(&root))) + }); + + group.bench_with_input(BenchmarkId::new("rayon", n), n, |b, _| { + assert_eq!(&expected, &rayon(&root)); + b.iter(|| rayon(black_box(&root))) + }); + + group.bench_with_input(BenchmarkId::new("orx", n), n, |b, _| { + assert_eq!(&expected, &orx(&root)); + b.iter(|| orx(black_box(&root))) + }); + + group.bench_with_input(BenchmarkId::new("orx_static", n), n, |b, _| { + assert_eq!(&expected, &orx_static(&root)); + b.iter(|| orx_static(black_box(&root))) + }); + + group.bench_with_input(BenchmarkId::new("iter", n), n, |b, _| { + assert_eq!(&expected, &iter(&root)); + b.iter(|| iter(black_box(&root))) + }); + + group.finish(); +} + +criterion_group!(benches, run); +criterion_main!(benches); diff --git a/examples/par_recursive_iter.rs b/examples/par_recursive_iter.rs index a4f40dff..ec1d6a63 100644 --- a/examples/par_recursive_iter.rs +++ b/examples/par_recursive_iter.rs @@ -1,6 +1,8 @@ +use orx_concurrent_recursive_iter::ConcurrentRecursiveIter; use orx_parallel::{IntoParIterRec, ParIter}; use rand::{Rng, SeedableRng}; use rand_chacha::ChaCha8Rng; +use std::sync::atomic::Ordering; struct Node { value: u64, @@ -31,30 +33,84 @@ impl Node { Self { value, children } } - fn num_nodes(&self) -> usize { + fn seq_num_nodes(&self) -> usize { 1 + self .children .iter() - .map(|node| node.num_nodes()) + .map(|node| node.seq_num_nodes()) .sum::() } + + fn seq_sum_fib(&self) -> u64 { + fibonacci(self.value) + self.children.iter().map(|x| x.seq_sum_fib()).sum::() + } } -fn main() { +fn par_rec(root: &Node) -> u64 { + fn extend<'a, 'b>(node: &'a &'b Node) -> &'b [Node] { + &node.children + } + + [root] + .into_par_rec(extend) + .chunk_size(1024 * 1024) + .num_threads(32) + .map(|x| fibonacci(x.value)) + .sum() +} + +fn iter(root: &Node) -> u64 { + use orx_concurrent_iter::*; fn extend<'a, 'b>(node: &'a &'b Node) -> &'b [Node] { &node.children } + let num_threads = 16; + let chunk_size = 1024; + let iter = ConcurrentRecursiveIter::new(extend, [root]); + let num_spawned = core::sync::atomic::AtomicUsize::new(0); + let num_handled = core::sync::atomic::AtomicUsize::new(0); + + std::thread::scope(|s| { + let mut handles = vec![]; + for _ in 0..num_threads { + handles.push(s.spawn(|| { + // allow all threads to be spawned + _ = num_spawned.fetch_add(1, Ordering::Relaxed); + while num_spawned.load(Ordering::Relaxed) < num_threads {} + + // computation: parallel reduction + let mut thread_sum = 0; + let mut puller = iter.chunk_puller(chunk_size); + while let Some(chunk) = puller.pull() { + thread_sum += chunk.into_iter().map(|x| fibonacci(x.value)).sum::(); + } + + thread_sum + })); + } + + handles.into_iter().map(|x| x.join().unwrap()).sum() + }) +} + +fn main() { let mut rng = ChaCha8Rng::seed_from_u64(42); - let root = Node::new(&mut rng, 300); + let root = Node::new(&mut rng, 550); - let par = [&root].into_par_rec(extend); - let count = par.count(); - assert_eq!(count, root.num_nodes()); + // let par = [&root].into_par_rec(extend); + // let count = par.count(); + // assert_eq!(count, root.seq_num_nodes()); + let count = root.seq_num_nodes(); println!("Tree contains {count} nodes"); - let par = [&root].into_par_rec(extend); - let sum_fib = par.map(|x| fibonacci(x.value)).sum(); - assert_eq!(sum_fib, 4843403551); + let expected = root.seq_sum_fib(); + + let sum_fib = par_rec(&root); + assert_eq!(sum_fib, expected); + println!("Sum of Fibonacci of node values is {sum_fib}"); + + let sum_fib = iter(&root); + assert_eq!(sum_fib, expected); println!("Sum of Fibonacci of node values is {sum_fib}"); } diff --git a/src/executor/thread_compute/reduce.rs b/src/executor/thread_compute/reduce.rs index 9b0b03be..66f03bac 100644 --- a/src/executor/thread_compute/reduce.rs +++ b/src/executor/thread_compute/reduce.rs @@ -49,6 +49,7 @@ where match chunk_puller.pull() { Some(chunk) => { + // println!("chunk = {}", chunk.len()); let res = chunk.map(map1).reduce(reduce); acc = match acc { Some(x) => match res { diff --git a/src/iter/mod.rs b/src/iter/mod.rs index 6a306d98..5cf888b1 100644 --- a/src/iter/mod.rs +++ b/src/iter/mod.rs @@ -1,3 +1,5 @@ +mod recursive; mod special_iterators; +pub use recursive::IntoParIterRec; pub use special_iterators::{ParEmpty, empty}; diff --git a/src/into_par_iter_rec.rs b/src/iter/recursive/into_par_rec_iter.rs similarity index 100% rename from src/into_par_iter_rec.rs rename to src/iter/recursive/into_par_rec_iter.rs diff --git a/src/iter/recursive/mod.rs b/src/iter/recursive/mod.rs new file mode 100644 index 00000000..93e0ce01 --- /dev/null +++ b/src/iter/recursive/mod.rs @@ -0,0 +1,4 @@ +mod into_par_rec_iter; +mod rec_per_iter; + +pub use into_par_rec_iter::IntoParIterRec; diff --git a/src/iter/recursive/rec_per_iter.rs b/src/iter/recursive/rec_per_iter.rs new file mode 100644 index 00000000..816c7c0b --- /dev/null +++ b/src/iter/recursive/rec_per_iter.rs @@ -0,0 +1,19 @@ +use crate::{ParallelRunner, computational_variants::Par}; +use orx_concurrent_iter::{ConcurrentIter, IntoConcurrentIter, implementations::ConIterVec}; +use orx_concurrent_recursive_iter::ConcurrentRecursiveIter; + +impl Par, R> +where + I: IntoIterator, + I::IntoIter: ExactSizeIterator, + I::Item: Send, + E: Fn(&I::Item) -> I + Sync, + R: ParallelRunner, +{ + pub fn into_eager(self) -> Par, R> { + let (orchestrator, params, iter) = self.destruct(); + let items: Vec<_> = iter.into_seq_iter().collect(); + let iter = items.into_con_iter(); + Par::new(orchestrator, params, iter) + } +} diff --git a/src/lib.rs b/src/lib.rs index 7df05ea1..fc299908 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,7 +10,7 @@ clippy::missing_panics_doc, clippy::todo )] -#![no_std] +// #![no_std] extern crate alloc; @@ -27,7 +27,6 @@ pub mod executor; mod generic_values; mod heap_sort; mod into_par_iter; -mod into_par_iter_rec; /// Module for creating special iterators. pub mod iter; mod iter_into_par_iter; @@ -61,7 +60,7 @@ mod test_utils; pub use collect_into::ParCollectInto; pub use executor::{DefaultExecutor, ParallelExecutor, ThreadExecutor}; pub use into_par_iter::IntoParIter; -pub use into_par_iter_rec::IntoParIterRec; +pub use iter::IntoParIterRec; pub use iter_into_par_iter::IterIntoParIter; pub use par_iter::ParIter; pub use par_iter_option::ParIterOption; From 9fb842e8448cfd3d01db9aa5f2942d17ee76ad7d Mon Sep 17 00:00:00 2001 From: orxfun Date: Fri, 17 Oct 2025 09:30:49 +0200 Subject: [PATCH 07/96] type alias to simplify types --- src/iter/recursive/rec_per_iter.rs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/iter/recursive/rec_per_iter.rs b/src/iter/recursive/rec_per_iter.rs index 816c7c0b..675987df 100644 --- a/src/iter/recursive/rec_per_iter.rs +++ b/src/iter/recursive/rec_per_iter.rs @@ -1,8 +1,13 @@ -use crate::{ParallelRunner, computational_variants::Par}; +use crate::{ + ParallelRunner, + computational_variants::{Par, ParMap}, +}; use orx_concurrent_iter::{ConcurrentIter, IntoConcurrentIter, implementations::ConIterVec}; use orx_concurrent_recursive_iter::ConcurrentRecursiveIter; -impl Par, R> +type Rec = ConcurrentRecursiveIter<::Item, E, I>; + +impl Par, R> where I: IntoIterator, I::IntoIter: ExactSizeIterator, @@ -17,3 +22,7 @@ where Par::new(orchestrator, params, iter) } } + +// pub struct ParMap + +// impl ParMap<> From 648e88b64856c38276a44920d17f6099f136ec0e Mon Sep 17 00:00:00 2001 From: orxfun Date: Fri, 17 Oct 2025 09:32:28 +0200 Subject: [PATCH 08/96] ParMap into_eager is implemented for recursive iterators --- src/iter/recursive/rec_per_iter.rs | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/iter/recursive/rec_per_iter.rs b/src/iter/recursive/rec_per_iter.rs index 675987df..1ecaeb98 100644 --- a/src/iter/recursive/rec_per_iter.rs +++ b/src/iter/recursive/rec_per_iter.rs @@ -23,6 +23,19 @@ where } } -// pub struct ParMap - -// impl ParMap<> +impl ParMap, O, M1, R> +where + I: IntoIterator, + I::IntoIter: ExactSizeIterator, + I::Item: Send, + E: Fn(&I::Item) -> I + Sync, + R: ParallelRunner, + M1: Fn(I::Item) -> O + Sync, +{ + pub fn into_eager(self) -> ParMap, O, M1, R> { + let (orchestrator, params, iter, map1) = self.destruct(); + let items: Vec<_> = iter.into_seq_iter().collect(); + let iter = items.into_con_iter(); + ParMap::new(orchestrator, params, iter, map1) + } +} From a3ca99fbe7cd4b055519d732d042914118722e7a Mon Sep 17 00:00:00 2001 From: orxfun Date: Fri, 17 Oct 2025 09:34:02 +0200 Subject: [PATCH 09/96] ParXap::into_eager is implemented for recursive iterators --- src/iter/recursive/rec_per_iter.rs | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/iter/recursive/rec_per_iter.rs b/src/iter/recursive/rec_per_iter.rs index 1ecaeb98..f506d4f4 100644 --- a/src/iter/recursive/rec_per_iter.rs +++ b/src/iter/recursive/rec_per_iter.rs @@ -1,6 +1,7 @@ use crate::{ ParallelRunner, - computational_variants::{Par, ParMap}, + computational_variants::{Par, ParMap, ParXap}, + generic_values::{TransformableValues, runner_results::Infallible}, }; use orx_concurrent_iter::{ConcurrentIter, IntoConcurrentIter, implementations::ConIterVec}; use orx_concurrent_recursive_iter::ConcurrentRecursiveIter; @@ -39,3 +40,21 @@ where ParMap::new(orchestrator, params, iter, map1) } } + +impl ParXap, Vo, X1, R> +where + I: IntoIterator, + I::IntoIter: ExactSizeIterator, + I::Item: Send, + E: Fn(&I::Item) -> I + Sync, + R: ParallelRunner, + X1: Fn(I::Item) -> Vo + Sync, + Vo: TransformableValues, +{ + pub fn into_eager(self) -> ParXap, Vo, X1, R> { + let (orchestrator, params, iter, xap1) = self.destruct(); + let items: Vec<_> = iter.into_seq_iter().collect(); + let iter = items.into_con_iter(); + ParXap::new(orchestrator, params, iter, xap1) + } +} From 4aedb82927c94d134f55eac37d8677d1632e2cf5 Mon Sep 17 00:00:00 2001 From: orxfun Date: Fri, 17 Oct 2025 09:40:05 +0200 Subject: [PATCH 10/96] eager is added to benches --- benches/par_recursive_iter.rs | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/benches/par_recursive_iter.rs b/benches/par_recursive_iter.rs index be07702f..2164323a 100644 --- a/benches/par_recursive_iter.rs +++ b/benches/par_recursive_iter.rs @@ -77,7 +77,7 @@ fn rayon(root: &Node) -> u64 { sum.into_inner() } -fn orx(root: &Node) -> u64 { +fn orx_lazy(root: &Node) -> u64 { fn extend<'a, 'b>(node: &'a &'b Node) -> &'b [Node] { &node.children } @@ -89,6 +89,18 @@ fn orx(root: &Node) -> u64 { .sum() } +fn orx_eager(root: &Node) -> u64 { + fn extend<'a, 'b>(node: &'a &'b Node) -> &'b [Node] { + &node.children + } + + [root] + .into_par_rec(extend) + .into_eager() + .map(|x| fibonacci(x.value)) + .sum() +} + fn orx_static(root: &Node) -> u64 { fn add_tasks<'a>(tasks: &mut Vec<&'a Node>, node: &'a Node) { tasks.push(node); @@ -96,7 +108,8 @@ fn orx_static(root: &Node) -> u64 { add_tasks(tasks, child); } } - let mut tasks = Vec::with_capacity(root.seq_num_nodes() + 1); + // let mut tasks = Vec::with_capacity(root.seq_num_nodes() + 1); + let mut tasks = Vec::new(); add_tasks(&mut tasks, root); tasks.par().map(|x| fibonacci(x.value)).sum() } @@ -150,9 +163,14 @@ fn run(c: &mut Criterion) { b.iter(|| rayon(black_box(&root))) }); - group.bench_with_input(BenchmarkId::new("orx", n), n, |b, _| { - assert_eq!(&expected, &orx(&root)); - b.iter(|| orx(black_box(&root))) + group.bench_with_input(BenchmarkId::new("orx_lazy", n), n, |b, _| { + assert_eq!(&expected, &orx_lazy(&root)); + b.iter(|| orx_lazy(black_box(&root))) + }); + + group.bench_with_input(BenchmarkId::new("orx_eager", n), n, |b, _| { + assert_eq!(&expected, &orx_eager(&root)); + b.iter(|| orx_eager(black_box(&root))) }); group.bench_with_input(BenchmarkId::new("orx_static", n), n, |b, _| { From 9c2a3c44e7433b800f1d7cd55b46dfbec0f31a81 Mon Sep 17 00:00:00 2001 From: orxfun Date: Fri, 17 Oct 2025 11:33:07 +0200 Subject: [PATCH 11/96] define IntoParIterRecExact --- Cargo.toml | 2 +- src/iter/mod.rs | 2 +- src/iter/recursive/into_par_rec_iter.rs | 42 ++++++++++++++++++++++++- src/iter/recursive/mod.rs | 2 +- src/lib.rs | 2 +- 5 files changed, 45 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 931714ad..a7ea9376 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,7 @@ orx-pinned-concurrent-col = { version = "2.17.0", default-features = false } orx-iterable = { version = "1.3.0", default-features = false } orx-priority-queue = { version = "1.7.0", default-features = false } orx-pseudo-default = { version = "2.1.0", default-features = false } -orx-concurrent-recursive-iter = { git = "https://github.com/orxfun/orx-concurrent-recursive-iter" } +orx-concurrent-recursive-iter = { git = "https://github.com/orxfun/orx-concurrent-recursive-iter", branch = "exact-sized-con-iter" } # optional: generic iterator rayon = { version = "1.11.0", optional = true, default-features = false } diff --git a/src/iter/mod.rs b/src/iter/mod.rs index 5cf888b1..b5392a95 100644 --- a/src/iter/mod.rs +++ b/src/iter/mod.rs @@ -1,5 +1,5 @@ mod recursive; mod special_iterators; -pub use recursive::IntoParIterRec; +pub use recursive::{IntoParIterRec, IntoParIterRecExact}; pub use special_iterators::{ParEmpty, empty}; diff --git a/src/iter/recursive/into_par_rec_iter.rs b/src/iter/recursive/into_par_rec_iter.rs index 445c18b2..b7637a39 100644 --- a/src/iter/recursive/into_par_rec_iter.rs +++ b/src/iter/recursive/into_par_rec_iter.rs @@ -1,5 +1,7 @@ use crate::{DefaultRunner, Params, computational_variants::Par}; -use orx_concurrent_recursive_iter::ConcurrentRecursiveIter; +use orx_concurrent_recursive_iter::{ConcurrentRecursiveIter, ConcurrentRecursiveIterExact}; + +// unknown size pub trait IntoParIterRec where @@ -34,3 +36,41 @@ where Par::new(DefaultRunner::default(), Params::default(), con_rec_iter) } } + +// exact size + +pub trait IntoParIterRecExact +where + Self: IntoIterator, + Self::Item: Send, +{ + fn into_par_rec_exact( + self, + extend: E, + exact_len: usize, + ) -> Par, DefaultRunner> + where + I: IntoIterator, + I::IntoIter: ExactSizeIterator, + E: Fn(&Self::Item) -> I + Sync; +} + +impl IntoParIterRecExact for X +where + X: IntoIterator, + X::Item: Send, +{ + fn into_par_rec_exact( + self, + extend: E, + exact_len: usize, + ) -> Par, DefaultRunner> + where + I: IntoIterator, + I::IntoIter: ExactSizeIterator, + E: Fn(&Self::Item) -> I + Sync, + { + let con_rec_iter = ConcurrentRecursiveIterExact::new_exact(extend, self, exact_len); + Par::new(DefaultRunner::default(), Params::default(), con_rec_iter) + } +} diff --git a/src/iter/recursive/mod.rs b/src/iter/recursive/mod.rs index 93e0ce01..2b96eac5 100644 --- a/src/iter/recursive/mod.rs +++ b/src/iter/recursive/mod.rs @@ -1,4 +1,4 @@ mod into_par_rec_iter; mod rec_per_iter; -pub use into_par_rec_iter::IntoParIterRec; +pub use into_par_rec_iter::{IntoParIterRec, IntoParIterRecExact}; diff --git a/src/lib.rs b/src/lib.rs index fc299908..6faef8a1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -60,7 +60,7 @@ mod test_utils; pub use collect_into::ParCollectInto; pub use executor::{DefaultExecutor, ParallelExecutor, ThreadExecutor}; pub use into_par_iter::IntoParIter; -pub use iter::IntoParIterRec; +pub use iter::{IntoParIterRec, IntoParIterRecExact}; pub use iter_into_par_iter::IterIntoParIter; pub use par_iter::ParIter; pub use par_iter_option::ParIterOption; From cf2e979626fe97693eca664db5cc44742e000790 Mon Sep 17 00:00:00 2001 From: orxfun Date: Fri, 17 Oct 2025 11:36:48 +0200 Subject: [PATCH 12/96] update eager calls by utilizing exact length when known --- src/iter/recursive/rec_per_iter.rs | 37 +++++++++++++++++++++++------- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/src/iter/recursive/rec_per_iter.rs b/src/iter/recursive/rec_per_iter.rs index f506d4f4..09c224c1 100644 --- a/src/iter/recursive/rec_per_iter.rs +++ b/src/iter/recursive/rec_per_iter.rs @@ -4,12 +4,13 @@ use crate::{ generic_values::{TransformableValues, runner_results::Infallible}, }; use orx_concurrent_iter::{ConcurrentIter, IntoConcurrentIter, implementations::ConIterVec}; -use orx_concurrent_recursive_iter::ConcurrentRecursiveIter; +use orx_concurrent_recursive_iter::{ConcurrentRecursiveIterCore, Size}; -type Rec = ConcurrentRecursiveIter<::Item, E, I>; +type Rec = ConcurrentRecursiveIterCore::Item, E, I>; -impl Par, R> +impl Par, R> where + S: Size, I: IntoIterator, I::IntoIter: ExactSizeIterator, I::Item: Send, @@ -18,14 +19,15 @@ where { pub fn into_eager(self) -> Par, R> { let (orchestrator, params, iter) = self.destruct(); - let items: Vec<_> = iter.into_seq_iter().collect(); + let items = collect_items(iter); let iter = items.into_con_iter(); Par::new(orchestrator, params, iter) } } -impl ParMap, O, M1, R> +impl ParMap, O, M1, R> where + S: Size, I: IntoIterator, I::IntoIter: ExactSizeIterator, I::Item: Send, @@ -35,14 +37,15 @@ where { pub fn into_eager(self) -> ParMap, O, M1, R> { let (orchestrator, params, iter, map1) = self.destruct(); - let items: Vec<_> = iter.into_seq_iter().collect(); + let items = collect_items(iter); let iter = items.into_con_iter(); ParMap::new(orchestrator, params, iter, map1) } } -impl ParXap, Vo, X1, R> +impl ParXap, Vo, X1, R> where + S: Size, I: IntoIterator, I::IntoIter: ExactSizeIterator, I::Item: Send, @@ -53,8 +56,26 @@ where { pub fn into_eager(self) -> ParXap, Vo, X1, R> { let (orchestrator, params, iter, xap1) = self.destruct(); - let items: Vec<_> = iter.into_seq_iter().collect(); + let items = collect_items(iter); let iter = items.into_con_iter(); ParXap::new(orchestrator, params, iter, xap1) } } + +fn collect_items(iter: Rec) -> Vec +where + S: Size, + I: IntoIterator, + I::IntoIter: ExactSizeIterator, + I::Item: Send, + E: Fn(&I::Item) -> I + Sync, +{ + match iter.try_get_len() { + Some(len) => { + let mut items = Vec::with_capacity(len); + items.extend(iter.into_seq_iter()); + items + } + None => iter.into_seq_iter().collect(), + } +} From 088a9efd75bd889fc9bbaef50a0a03c8087a4bba Mon Sep 17 00:00:00 2001 From: orxfun Date: Fri, 17 Oct 2025 11:41:24 +0200 Subject: [PATCH 13/96] exact tests added --- benches/par_recursive_iter.rs | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/benches/par_recursive_iter.rs b/benches/par_recursive_iter.rs index 2164323a..c3faec05 100644 --- a/benches/par_recursive_iter.rs +++ b/benches/par_recursive_iter.rs @@ -77,13 +77,27 @@ fn rayon(root: &Node) -> u64 { sum.into_inner() } -fn orx_lazy(root: &Node) -> u64 { +fn orx_lazy_unknown(root: &Node) -> u64 { fn extend<'a, 'b>(node: &'a &'b Node) -> &'b [Node] { &node.children } [root] .into_par_rec(extend) + // .chunk_size(1024 * 64) + .map(|x| fibonacci(x.value)) + .sum() +} + +fn orx_lazy_exact(root: &Node) -> u64 { + fn extend<'a, 'b>(node: &'a &'b Node) -> &'b [Node] { + &node.children + } + + let num_nodes = root.seq_num_nodes(); + + [root] + .into_par_rec_exact(extend, num_nodes) .chunk_size(1024 * 64) .map(|x| fibonacci(x.value)) .sum() @@ -163,9 +177,14 @@ fn run(c: &mut Criterion) { b.iter(|| rayon(black_box(&root))) }); - group.bench_with_input(BenchmarkId::new("orx_lazy", n), n, |b, _| { - assert_eq!(&expected, &orx_lazy(&root)); - b.iter(|| orx_lazy(black_box(&root))) + group.bench_with_input(BenchmarkId::new("orx_lazy_unknown", n), n, |b, _| { + assert_eq!(&expected, &orx_lazy_unknown(&root)); + b.iter(|| orx_lazy_unknown(black_box(&root))) + }); + + group.bench_with_input(BenchmarkId::new("orx_lazy_exact", n), n, |b, _| { + assert_eq!(&expected, &orx_lazy_exact(&root)); + b.iter(|| orx_lazy_exact(black_box(&root))) }); group.bench_with_input(BenchmarkId::new("orx_eager", n), n, |b, _| { From 35f97320735d5a38f923a53917e6e5f676dec1b4 Mon Sep 17 00:00:00 2001 From: orxfun Date: Fri, 17 Oct 2025 11:42:34 +0200 Subject: [PATCH 14/96] add exact and unknown benchmarks for recursive iterators --- benches/par_recursive_iter.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/benches/par_recursive_iter.rs b/benches/par_recursive_iter.rs index c3faec05..9cdcf18a 100644 --- a/benches/par_recursive_iter.rs +++ b/benches/par_recursive_iter.rs @@ -98,7 +98,6 @@ fn orx_lazy_exact(root: &Node) -> u64 { [root] .into_par_rec_exact(extend, num_nodes) - .chunk_size(1024 * 64) .map(|x| fibonacci(x.value)) .sum() } From 297e49082cd6edafadd25836c5efcf5f794411e7 Mon Sep 17 00:00:00 2001 From: orxfun Date: Sun, 19 Oct 2025 18:10:35 +0200 Subject: [PATCH 15/96] update recursive example and benches --- benches/par_recursive_iter.rs | 10 +++++----- examples/par_recursive_iter.rs | 10 ++++++---- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/benches/par_recursive_iter.rs b/benches/par_recursive_iter.rs index 9cdcf18a..25327cbe 100644 --- a/benches/par_recursive_iter.rs +++ b/benches/par_recursive_iter.rs @@ -160,7 +160,7 @@ fn iter(root: &Node) -> u64 { fn run(c: &mut Criterion) { let mut rng = ChaCha8Rng::seed_from_u64(42); - let root = Node::new(&mut rng, 250); + let root = Node::new(&mut rng, 500); let n = &root.seq_num_nodes(); let expected = root.seq_sum_fib(); @@ -176,10 +176,10 @@ fn run(c: &mut Criterion) { b.iter(|| rayon(black_box(&root))) }); - group.bench_with_input(BenchmarkId::new("orx_lazy_unknown", n), n, |b, _| { - assert_eq!(&expected, &orx_lazy_unknown(&root)); - b.iter(|| orx_lazy_unknown(black_box(&root))) - }); + // group.bench_with_input(BenchmarkId::new("orx_lazy_unknown", n), n, |b, _| { + // assert_eq!(&expected, &orx_lazy_unknown(&root)); + // b.iter(|| orx_lazy_unknown(black_box(&root))) + // }); group.bench_with_input(BenchmarkId::new("orx_lazy_exact", n), n, |b, _| { assert_eq!(&expected, &orx_lazy_exact(&root)); diff --git a/examples/par_recursive_iter.rs b/examples/par_recursive_iter.rs index ec1d6a63..7836891a 100644 --- a/examples/par_recursive_iter.rs +++ b/examples/par_recursive_iter.rs @@ -1,5 +1,5 @@ use orx_concurrent_recursive_iter::ConcurrentRecursiveIter; -use orx_parallel::{IntoParIterRec, ParIter}; +use orx_parallel::*; use rand::{Rng, SeedableRng}; use rand_chacha::ChaCha8Rng; use std::sync::atomic::Ordering; @@ -50,10 +50,11 @@ fn par_rec(root: &Node) -> u64 { fn extend<'a, 'b>(node: &'a &'b Node) -> &'b [Node] { &node.children } + let count = root.seq_num_nodes(); [root] - .into_par_rec(extend) - .chunk_size(1024 * 1024) + .into_par_rec_exact(extend, count) + // .chunk_size(1024 * 1024) .num_threads(32) .map(|x| fibonacci(x.value)) .sum() @@ -96,7 +97,8 @@ fn iter(root: &Node) -> u64 { fn main() { let mut rng = ChaCha8Rng::seed_from_u64(42); - let root = Node::new(&mut rng, 550); + // let root = Node::new(&mut rng, 550); + let root = Node::new(&mut rng, 250); // let par = [&root].into_par_rec(extend); // let count = par.count(); From 0f1b7523587d7a6a23d16230eacf180b704c898f Mon Sep 17 00:00:00 2001 From: orxfun Date: Sun, 19 Oct 2025 18:14:19 +0200 Subject: [PATCH 16/96] initiate executor with diagnostics --- src/executor/executor_with_diagnostics/mod.rs | 3 +++ .../executor_with_diagnostics/parallel_executor.rs | 8 ++++++++ src/executor/executor_with_diagnostics/shared_state.rs | 3 +++ src/executor/executor_with_diagnostics/thread_executor.rs | 0 src/executor/mod.rs | 1 + 5 files changed, 15 insertions(+) create mode 100644 src/executor/executor_with_diagnostics/mod.rs create mode 100644 src/executor/executor_with_diagnostics/parallel_executor.rs create mode 100644 src/executor/executor_with_diagnostics/shared_state.rs create mode 100644 src/executor/executor_with_diagnostics/thread_executor.rs diff --git a/src/executor/executor_with_diagnostics/mod.rs b/src/executor/executor_with_diagnostics/mod.rs new file mode 100644 index 00000000..82e1d5b4 --- /dev/null +++ b/src/executor/executor_with_diagnostics/mod.rs @@ -0,0 +1,3 @@ +mod parallel_executor; +mod shared_state; +mod thread_executor; diff --git a/src/executor/executor_with_diagnostics/parallel_executor.rs b/src/executor/executor_with_diagnostics/parallel_executor.rs new file mode 100644 index 00000000..214f27de --- /dev/null +++ b/src/executor/executor_with_diagnostics/parallel_executor.rs @@ -0,0 +1,8 @@ +use crate::ParallelExecutor; + +pub struct ParallelExecutorWithDiagnostics +where + E: ParallelExecutor, +{ + executor: E, +} diff --git a/src/executor/executor_with_diagnostics/shared_state.rs b/src/executor/executor_with_diagnostics/shared_state.rs new file mode 100644 index 00000000..91cd376a --- /dev/null +++ b/src/executor/executor_with_diagnostics/shared_state.rs @@ -0,0 +1,3 @@ +pub struct SharedStateWithDiagnostics { + base_state: S, +} diff --git a/src/executor/executor_with_diagnostics/thread_executor.rs b/src/executor/executor_with_diagnostics/thread_executor.rs new file mode 100644 index 00000000..e69de29b diff --git a/src/executor/mod.rs b/src/executor/mod.rs index 8deafde4..b3840383 100644 --- a/src/executor/mod.rs +++ b/src/executor/mod.rs @@ -1,3 +1,4 @@ +mod executor_with_diagnostics; mod fixed_chunk_executor; pub(crate) mod parallel_compute; mod parallel_executor; From e6e4bcf716f724008419cee494897e8ae3cbdab8 Mon Sep 17 00:00:00 2001 From: orxfun Date: Sun, 19 Oct 2025 18:15:15 +0200 Subject: [PATCH 17/96] impl par executor for ParallelExecutorWithDiagnostics --- .../parallel_executor.rs | 44 ++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/src/executor/executor_with_diagnostics/parallel_executor.rs b/src/executor/executor_with_diagnostics/parallel_executor.rs index 214f27de..8a091b97 100644 --- a/src/executor/executor_with_diagnostics/parallel_executor.rs +++ b/src/executor/executor_with_diagnostics/parallel_executor.rs @@ -1,4 +1,8 @@ -use crate::ParallelExecutor; +use crate::{ + ParallelExecutor, + executor::executor_with_diagnostics::shared_state::SharedStateWithDiagnostics, + runner::{ComputationKind, NumSpawned}, +}; pub struct ParallelExecutorWithDiagnostics where @@ -6,3 +10,41 @@ where { executor: E, } + +impl ParallelExecutor for ParallelExecutorWithDiagnostics +where + E: ParallelExecutor, +{ + type SharedState = SharedStateWithDiagnostics; + + type ThreadExecutor = (); + + fn new( + kind: ComputationKind, + params: crate::Params, + initial_input_len: Option, + max_num_threads: std::num::NonZeroUsize, + ) -> Self { + todo!() + } + + fn new_shared_state(&self) -> Self::SharedState { + todo!() + } + + fn do_spawn_new( + &self, + num_spawned: NumSpawned, + shared_state: &Self::SharedState, + iter: &I, + ) -> bool + where + I: orx_concurrent_iter::ConcurrentIter, + { + todo!() + } + + fn new_thread_executor(&self, shared_state: &Self::SharedState) -> Self::ThreadExecutor { + todo!() + } +} From 34d9eb3cc8cbe38b48e0b5ddc1180e94a4e4744c Mon Sep 17 00:00:00 2001 From: orxfun Date: Sun, 19 Oct 2025 18:16:49 +0200 Subject: [PATCH 18/96] define ThreadExecutorWithDiagnostics --- .../parallel_executor.rs | 6 ++- .../thread_executor.rs | 37 +++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/src/executor/executor_with_diagnostics/parallel_executor.rs b/src/executor/executor_with_diagnostics/parallel_executor.rs index 8a091b97..4391e2ba 100644 --- a/src/executor/executor_with_diagnostics/parallel_executor.rs +++ b/src/executor/executor_with_diagnostics/parallel_executor.rs @@ -1,6 +1,8 @@ use crate::{ ParallelExecutor, - executor::executor_with_diagnostics::shared_state::SharedStateWithDiagnostics, + executor::executor_with_diagnostics::{ + shared_state::SharedStateWithDiagnostics, thread_executor::ThreadExecutorWithDiagnostics, + }, runner::{ComputationKind, NumSpawned}, }; @@ -17,7 +19,7 @@ where { type SharedState = SharedStateWithDiagnostics; - type ThreadExecutor = (); + type ThreadExecutor = ThreadExecutorWithDiagnostics; fn new( kind: ComputationKind, diff --git a/src/executor/executor_with_diagnostics/thread_executor.rs b/src/executor/executor_with_diagnostics/thread_executor.rs index e69de29b..6148bfb7 100644 --- a/src/executor/executor_with_diagnostics/thread_executor.rs +++ b/src/executor/executor_with_diagnostics/thread_executor.rs @@ -0,0 +1,37 @@ +use crate::{ + ThreadExecutor, executor::executor_with_diagnostics::shared_state::SharedStateWithDiagnostics, +}; +use orx_concurrent_iter::ConcurrentIter; + +pub struct ThreadExecutorWithDiagnostics +where + E: ThreadExecutor, +{ + executor: E, +} + +impl ThreadExecutor for ThreadExecutorWithDiagnostics +where + E: ThreadExecutor, +{ + type SharedState = SharedStateWithDiagnostics; + + fn next_chunk_size(&self, shared_state: &Self::SharedState, iter: &I) -> usize + where + I: ConcurrentIter, + { + todo!() + } + + fn begin_chunk(&mut self, chunk_size: usize) { + todo!() + } + + fn complete_chunk(&mut self, shared_state: &Self::SharedState, chunk_size: usize) { + todo!() + } + + fn complete_task(&mut self, shared_state: &Self::SharedState) { + todo!() + } +} From d46d051601bc3e04e6b2b3e38201103fc565a770 Mon Sep 17 00:00:00 2001 From: orxfun Date: Sun, 19 Oct 2025 18:18:40 +0200 Subject: [PATCH 19/96] combine thread and parallel executors with diagnostics --- .../executor_with_diagnostics/parallel_executor.rs | 2 +- .../executor_with_diagnostics/thread_executor.rs | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/executor/executor_with_diagnostics/parallel_executor.rs b/src/executor/executor_with_diagnostics/parallel_executor.rs index 4391e2ba..2f6e41bf 100644 --- a/src/executor/executor_with_diagnostics/parallel_executor.rs +++ b/src/executor/executor_with_diagnostics/parallel_executor.rs @@ -19,7 +19,7 @@ where { type SharedState = SharedStateWithDiagnostics; - type ThreadExecutor = ThreadExecutorWithDiagnostics; + type ThreadExecutor = ThreadExecutorWithDiagnostics; fn new( kind: ComputationKind, diff --git a/src/executor/executor_with_diagnostics/thread_executor.rs b/src/executor/executor_with_diagnostics/thread_executor.rs index 6148bfb7..e2252d0c 100644 --- a/src/executor/executor_with_diagnostics/thread_executor.rs +++ b/src/executor/executor_with_diagnostics/thread_executor.rs @@ -1,20 +1,21 @@ use crate::{ - ThreadExecutor, executor::executor_with_diagnostics::shared_state::SharedStateWithDiagnostics, + ParallelExecutor, ThreadExecutor, + executor::executor_with_diagnostics::shared_state::SharedStateWithDiagnostics, }; use orx_concurrent_iter::ConcurrentIter; pub struct ThreadExecutorWithDiagnostics where - E: ThreadExecutor, + E: ParallelExecutor, { - executor: E, + executor: E::ThreadExecutor, } impl ThreadExecutor for ThreadExecutorWithDiagnostics where - E: ThreadExecutor, + E: ParallelExecutor, { - type SharedState = SharedStateWithDiagnostics; + type SharedState = SharedStateWithDiagnostics; fn next_chunk_size(&self, shared_state: &Self::SharedState, iter: &I) -> usize where From d12f0fe26dcfdce31cefe8910b4e14b137342ab3 Mon Sep 17 00:00:00 2001 From: orxfun Date: Sun, 19 Oct 2025 18:20:19 +0200 Subject: [PATCH 20/96] complete thread executor implementation for ThreadExecutorWithDiagnostics without diagnostics yet --- src/executor/executor_with_diagnostics/shared_state.rs | 9 ++++++++- .../executor_with_diagnostics/thread_executor.rs | 9 +++++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/executor/executor_with_diagnostics/shared_state.rs b/src/executor/executor_with_diagnostics/shared_state.rs index 91cd376a..7a91133e 100644 --- a/src/executor/executor_with_diagnostics/shared_state.rs +++ b/src/executor/executor_with_diagnostics/shared_state.rs @@ -1,3 +1,10 @@ pub struct SharedStateWithDiagnostics { - base_state: S, + inner: S, +} + +impl SharedStateWithDiagnostics { + #[inline(always)] + pub fn inner(&self) -> &S { + &self.inner + } } diff --git a/src/executor/executor_with_diagnostics/thread_executor.rs b/src/executor/executor_with_diagnostics/thread_executor.rs index e2252d0c..d4c0010b 100644 --- a/src/executor/executor_with_diagnostics/thread_executor.rs +++ b/src/executor/executor_with_diagnostics/thread_executor.rs @@ -21,18 +21,19 @@ where where I: ConcurrentIter, { - todo!() + self.executor.next_chunk_size(shared_state.inner(), iter) } fn begin_chunk(&mut self, chunk_size: usize) { - todo!() + self.executor.begin_chunk(chunk_size); } fn complete_chunk(&mut self, shared_state: &Self::SharedState, chunk_size: usize) { - todo!() + self.executor + .complete_chunk(shared_state.inner(), chunk_size); } fn complete_task(&mut self, shared_state: &Self::SharedState) { - todo!() + self.executor.complete_task(shared_state.inner()); } } From a076411433bbd7337849ba86c09baaac23abe4a5 Mon Sep 17 00:00:00 2001 From: orxfun Date: Sun, 19 Oct 2025 18:21:31 +0200 Subject: [PATCH 21/96] organize imports --- .../executor_with_diagnostics/parallel_executor.rs | 13 ++++++------- .../executor_with_diagnostics/thread_executor.rs | 6 ++---- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/src/executor/executor_with_diagnostics/parallel_executor.rs b/src/executor/executor_with_diagnostics/parallel_executor.rs index 2f6e41bf..73f501d5 100644 --- a/src/executor/executor_with_diagnostics/parallel_executor.rs +++ b/src/executor/executor_with_diagnostics/parallel_executor.rs @@ -1,10 +1,9 @@ -use crate::{ - ParallelExecutor, - executor::executor_with_diagnostics::{ - shared_state::SharedStateWithDiagnostics, thread_executor::ThreadExecutorWithDiagnostics, - }, - runner::{ComputationKind, NumSpawned}, +use super::{ + shared_state::SharedStateWithDiagnostics, thread_executor::ThreadExecutorWithDiagnostics, }; +use crate::ParallelExecutor; +use crate::runner::{ComputationKind, NumSpawned}; +use std::num::NonZeroUsize; pub struct ParallelExecutorWithDiagnostics where @@ -25,7 +24,7 @@ where kind: ComputationKind, params: crate::Params, initial_input_len: Option, - max_num_threads: std::num::NonZeroUsize, + max_num_threads: NonZeroUsize, ) -> Self { todo!() } diff --git a/src/executor/executor_with_diagnostics/thread_executor.rs b/src/executor/executor_with_diagnostics/thread_executor.rs index d4c0010b..a5352bb8 100644 --- a/src/executor/executor_with_diagnostics/thread_executor.rs +++ b/src/executor/executor_with_diagnostics/thread_executor.rs @@ -1,7 +1,5 @@ -use crate::{ - ParallelExecutor, ThreadExecutor, - executor::executor_with_diagnostics::shared_state::SharedStateWithDiagnostics, -}; +use super::shared_state::SharedStateWithDiagnostics; +use crate::{ParallelExecutor, ThreadExecutor}; use orx_concurrent_iter::ConcurrentIter; pub struct ThreadExecutorWithDiagnostics From f7dbfea578c4e8a8eec3d053ede5d298295b1347 Mon Sep 17 00:00:00 2001 From: orxfun Date: Sun, 19 Oct 2025 18:24:27 +0200 Subject: [PATCH 22/96] parallel executor is implemented for diagnostics --- .../parallel_executor.rs | 16 +++++++++++----- .../executor_with_diagnostics/shared_state.rs | 4 ++++ .../executor_with_diagnostics/thread_executor.rs | 11 ++++++++++- 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/src/executor/executor_with_diagnostics/parallel_executor.rs b/src/executor/executor_with_diagnostics/parallel_executor.rs index 73f501d5..2d138884 100644 --- a/src/executor/executor_with_diagnostics/parallel_executor.rs +++ b/src/executor/executor_with_diagnostics/parallel_executor.rs @@ -1,3 +1,5 @@ +use orx_concurrent_iter::ConcurrentIter; + use super::{ shared_state::SharedStateWithDiagnostics, thread_executor::ThreadExecutorWithDiagnostics, }; @@ -26,11 +28,13 @@ where initial_input_len: Option, max_num_threads: NonZeroUsize, ) -> Self { - todo!() + let executor = E::new(kind, params, initial_input_len, max_num_threads); + Self { executor } } fn new_shared_state(&self) -> Self::SharedState { - todo!() + let inner_state = self.executor.new_shared_state(); + SharedStateWithDiagnostics::new(inner_state) } fn do_spawn_new( @@ -40,12 +44,14 @@ where iter: &I, ) -> bool where - I: orx_concurrent_iter::ConcurrentIter, + I: ConcurrentIter, { - todo!() + self.executor + .do_spawn_new(num_spawned, shared_state.inner(), iter) } fn new_thread_executor(&self, shared_state: &Self::SharedState) -> Self::ThreadExecutor { - todo!() + let executor = self.executor.new_thread_executor(shared_state.inner()); + ThreadExecutorWithDiagnostics::new(executor) } } diff --git a/src/executor/executor_with_diagnostics/shared_state.rs b/src/executor/executor_with_diagnostics/shared_state.rs index 7a91133e..b7f00e22 100644 --- a/src/executor/executor_with_diagnostics/shared_state.rs +++ b/src/executor/executor_with_diagnostics/shared_state.rs @@ -3,6 +3,10 @@ pub struct SharedStateWithDiagnostics { } impl SharedStateWithDiagnostics { + pub fn new(inner: S) -> Self { + Self { inner } + } + #[inline(always)] pub fn inner(&self) -> &S { &self.inner diff --git a/src/executor/executor_with_diagnostics/thread_executor.rs b/src/executor/executor_with_diagnostics/thread_executor.rs index a5352bb8..336e68ca 100644 --- a/src/executor/executor_with_diagnostics/thread_executor.rs +++ b/src/executor/executor_with_diagnostics/thread_executor.rs @@ -1,5 +1,5 @@ use super::shared_state::SharedStateWithDiagnostics; -use crate::{ParallelExecutor, ThreadExecutor}; +use crate::{ParallelExecutor, ThreadExecutor, executor}; use orx_concurrent_iter::ConcurrentIter; pub struct ThreadExecutorWithDiagnostics @@ -9,6 +9,15 @@ where executor: E::ThreadExecutor, } +impl ThreadExecutorWithDiagnostics +where + E: ParallelExecutor, +{ + pub(super) fn new(executor: E::ThreadExecutor) -> Self { + Self { executor } + } +} + impl ThreadExecutor for ThreadExecutorWithDiagnostics where E: ParallelExecutor, From 5882e3a70eb205ede72d97a723a26d1d4e07044d Mon Sep 17 00:00:00 2001 From: orxfun Date: Sun, 19 Oct 2025 18:26:29 +0200 Subject: [PATCH 23/96] prepare diagnostics --- src/executor/executor_with_diagnostics/parallel_executor.rs | 3 +-- src/executor/executor_with_diagnostics/shared_state.rs | 6 +++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/executor/executor_with_diagnostics/parallel_executor.rs b/src/executor/executor_with_diagnostics/parallel_executor.rs index 2d138884..0d64f7a4 100644 --- a/src/executor/executor_with_diagnostics/parallel_executor.rs +++ b/src/executor/executor_with_diagnostics/parallel_executor.rs @@ -1,10 +1,9 @@ -use orx_concurrent_iter::ConcurrentIter; - use super::{ shared_state::SharedStateWithDiagnostics, thread_executor::ThreadExecutorWithDiagnostics, }; use crate::ParallelExecutor; use crate::runner::{ComputationKind, NumSpawned}; +use orx_concurrent_iter::ConcurrentIter; use std::num::NonZeroUsize; pub struct ParallelExecutorWithDiagnostics diff --git a/src/executor/executor_with_diagnostics/shared_state.rs b/src/executor/executor_with_diagnostics/shared_state.rs index b7f00e22..8c7d3b38 100644 --- a/src/executor/executor_with_diagnostics/shared_state.rs +++ b/src/executor/executor_with_diagnostics/shared_state.rs @@ -1,10 +1,14 @@ +use orx_concurrent_bag::ConcurrentBag; + pub struct SharedStateWithDiagnostics { inner: S, + tasks: ConcurrentBag, } impl SharedStateWithDiagnostics { pub fn new(inner: S) -> Self { - Self { inner } + let tasks = ConcurrentBag::new(); + Self { inner, tasks } } #[inline(always)] From 0283160d0cc8f71d35552c002c0f0a771a6040ba Mon Sep 17 00:00:00 2001 From: orxfun Date: Sun, 19 Oct 2025 18:30:49 +0200 Subject: [PATCH 24/96] new_thread_executor receives the thread index --- .../parallel_executor.rs | 10 ++++++++-- .../fixed_chunk_executor/parallel_executor.rs | 2 +- src/executor/parallel_executor.rs | 6 +++++- src/runner/parallel_runner.rs | 15 ++++++++++++--- 4 files changed, 26 insertions(+), 7 deletions(-) diff --git a/src/executor/executor_with_diagnostics/parallel_executor.rs b/src/executor/executor_with_diagnostics/parallel_executor.rs index 0d64f7a4..8957001b 100644 --- a/src/executor/executor_with_diagnostics/parallel_executor.rs +++ b/src/executor/executor_with_diagnostics/parallel_executor.rs @@ -49,8 +49,14 @@ where .do_spawn_new(num_spawned, shared_state.inner(), iter) } - fn new_thread_executor(&self, shared_state: &Self::SharedState) -> Self::ThreadExecutor { - let executor = self.executor.new_thread_executor(shared_state.inner()); + fn new_thread_executor( + &self, + thread_idx: usize, + shared_state: &Self::SharedState, + ) -> Self::ThreadExecutor { + let executor = self + .executor + .new_thread_executor(thread_idx, shared_state.inner()); ThreadExecutorWithDiagnostics::new(executor) } } diff --git a/src/executor/fixed_chunk_executor/parallel_executor.rs b/src/executor/fixed_chunk_executor/parallel_executor.rs index be328a4e..b6316907 100644 --- a/src/executor/fixed_chunk_executor/parallel_executor.rs +++ b/src/executor/fixed_chunk_executor/parallel_executor.rs @@ -111,7 +111,7 @@ impl ParallelExecutor for FixedChunkRunner { self.spawn_new(num_spawned, iter.try_get_len()) } - fn new_thread_executor(&self, _: &Self::SharedState) -> Self::ThreadExecutor { + fn new_thread_executor(&self, _: usize, _: &Self::SharedState) -> Self::ThreadExecutor { Self::ThreadExecutor { chunk_size: self.current_chunk_size.load(Ordering::Relaxed), } diff --git a/src/executor/parallel_executor.rs b/src/executor/parallel_executor.rs index abf434d8..b7780623 100644 --- a/src/executor/parallel_executor.rs +++ b/src/executor/parallel_executor.rs @@ -43,5 +43,9 @@ pub trait ParallelExecutor: Sized + Sync + 'static { /// Creates a new thread executor provided that the current parallel execution state is /// `shared_state`. - fn new_thread_executor(&self, shared_state: &Self::SharedState) -> Self::ThreadExecutor; + fn new_thread_executor( + &self, + thread_idx: usize, + shared_state: &Self::SharedState, + ) -> Self::ThreadExecutor; } diff --git a/src/runner/parallel_runner.rs b/src/runner/parallel_runner.rs index b3b8f09a..16fb1fef 100644 --- a/src/runner/parallel_runner.rs +++ b/src/runner/parallel_runner.rs @@ -50,12 +50,13 @@ pub trait ParallelRunner { let runner = self.new_executor(kind, params, iter.try_get_len()); let state = runner.new_shared_state(); let do_spawn = |num_spawned| runner.do_spawn_new(num_spawned, &state, &iter); - let work = |num_spawned| { + let work = |num_spawned: NumSpawned| { + let thread_idx = num_spawned.into_inner(); thread_do( num_spawned, &iter, &state, - runner.new_thread_executor(&state), + runner.new_thread_executor(thread_idx, &state), ); }; self.thread_pool_mut().run_in_pool(do_spawn, work) @@ -81,7 +82,15 @@ pub trait ParallelRunner { let runner = self.new_executor(kind, params, iter_len); let state = runner.new_shared_state(); let do_spawn = |num_spawned| runner.do_spawn_new(num_spawned, &state, &iter); - let work = |nt| thread_map(nt, &iter, &state, runner.new_thread_executor(&state)); + let work = |num_spawned: NumSpawned| { + let thread_idx = num_spawned.into_inner(); + thread_map( + num_spawned, + &iter, + &state, + runner.new_thread_executor(thread_idx, &state), + ) + }; let max_num_threads = self.max_num_threads_for_computation(params, iter_len); self.thread_pool_mut() .map_in_pool::(do_spawn, work, max_num_threads) From a848ddb0dd26c05e188fed2958eae9cd5a95157e Mon Sep 17 00:00:00 2001 From: orxfun Date: Sun, 19 Oct 2025 18:31:41 +0200 Subject: [PATCH 25/96] ThreadExecutorWithDiagnostics receives its thread index --- .../executor_with_diagnostics/parallel_executor.rs | 2 +- .../executor_with_diagnostics/thread_executor.rs | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/executor/executor_with_diagnostics/parallel_executor.rs b/src/executor/executor_with_diagnostics/parallel_executor.rs index 8957001b..3002053b 100644 --- a/src/executor/executor_with_diagnostics/parallel_executor.rs +++ b/src/executor/executor_with_diagnostics/parallel_executor.rs @@ -57,6 +57,6 @@ where let executor = self .executor .new_thread_executor(thread_idx, shared_state.inner()); - ThreadExecutorWithDiagnostics::new(executor) + ThreadExecutorWithDiagnostics::new(thread_idx, executor) } } diff --git a/src/executor/executor_with_diagnostics/thread_executor.rs b/src/executor/executor_with_diagnostics/thread_executor.rs index 336e68ca..4b2f5a67 100644 --- a/src/executor/executor_with_diagnostics/thread_executor.rs +++ b/src/executor/executor_with_diagnostics/thread_executor.rs @@ -1,11 +1,12 @@ use super::shared_state::SharedStateWithDiagnostics; -use crate::{ParallelExecutor, ThreadExecutor, executor}; +use crate::{ParallelExecutor, ThreadExecutor}; use orx_concurrent_iter::ConcurrentIter; pub struct ThreadExecutorWithDiagnostics where E: ParallelExecutor, { + thread_idx: usize, executor: E::ThreadExecutor, } @@ -13,8 +14,11 @@ impl ThreadExecutorWithDiagnostics where E: ParallelExecutor, { - pub(super) fn new(executor: E::ThreadExecutor) -> Self { - Self { executor } + pub(super) fn new(thread_idx: usize, executor: E::ThreadExecutor) -> Self { + Self { + thread_idx, + executor, + } } } From 4e8d0263898b3799815b549f95091c948e26bc3b Mon Sep 17 00:00:00 2001 From: orxfun Date: Sun, 19 Oct 2025 18:33:14 +0200 Subject: [PATCH 26/96] diagnostics shared state holds task_counts --- .../executor_with_diagnostics/shared_state.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/executor/executor_with_diagnostics/shared_state.rs b/src/executor/executor_with_diagnostics/shared_state.rs index 8c7d3b38..6c5d09fa 100644 --- a/src/executor/executor_with_diagnostics/shared_state.rs +++ b/src/executor/executor_with_diagnostics/shared_state.rs @@ -2,17 +2,24 @@ use orx_concurrent_bag::ConcurrentBag; pub struct SharedStateWithDiagnostics { inner: S, - tasks: ConcurrentBag, + task_counts: ConcurrentBag<(usize, usize)>, // (thread_idx, chunk_size) } impl SharedStateWithDiagnostics { pub fn new(inner: S) -> Self { let tasks = ConcurrentBag::new(); - Self { inner, tasks } + Self { + inner, + task_counts: tasks, + } } #[inline(always)] pub fn inner(&self) -> &S { &self.inner } + + pub fn add_task_count(&self, thread_idx: usize, chunk_size: usize) { + self.task_counts.push((thread_idx, chunk_size)); + } } From e5ab9901297ae695767764489e5137eda2952e02 Mon Sep 17 00:00:00 2001 From: orxfun Date: Sun, 19 Oct 2025 18:34:02 +0200 Subject: [PATCH 27/96] complete_chunk updates diagnostics with task counts --- src/executor/executor_with_diagnostics/thread_executor.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/executor/executor_with_diagnostics/thread_executor.rs b/src/executor/executor_with_diagnostics/thread_executor.rs index 4b2f5a67..764c5ef5 100644 --- a/src/executor/executor_with_diagnostics/thread_executor.rs +++ b/src/executor/executor_with_diagnostics/thread_executor.rs @@ -40,6 +40,7 @@ where } fn complete_chunk(&mut self, shared_state: &Self::SharedState, chunk_size: usize) { + shared_state.add_task_count(self.thread_idx, chunk_size); self.executor .complete_chunk(shared_state.inner(), chunk_size); } From e51cb1a159951bcc6a7a4c69daefe922cd2d2e80 Mon Sep 17 00:00:00 2001 From: orxfun Date: Sun, 19 Oct 2025 18:38:27 +0200 Subject: [PATCH 28/96] shared state is updated only on complete task for diagnostics --- src/executor/executor_with_diagnostics/shared_state.rs | 6 +++--- src/executor/executor_with_diagnostics/thread_executor.rs | 5 ++++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/executor/executor_with_diagnostics/shared_state.rs b/src/executor/executor_with_diagnostics/shared_state.rs index 6c5d09fa..57b5078e 100644 --- a/src/executor/executor_with_diagnostics/shared_state.rs +++ b/src/executor/executor_with_diagnostics/shared_state.rs @@ -2,7 +2,7 @@ use orx_concurrent_bag::ConcurrentBag; pub struct SharedStateWithDiagnostics { inner: S, - task_counts: ConcurrentBag<(usize, usize)>, // (thread_idx, chunk_size) + task_counts: ConcurrentBag<(usize, Vec)>, // (thread_idx, chunk sizes) } impl SharedStateWithDiagnostics { @@ -19,7 +19,7 @@ impl SharedStateWithDiagnostics { &self.inner } - pub fn add_task_count(&self, thread_idx: usize, chunk_size: usize) { - self.task_counts.push((thread_idx, chunk_size)); + pub fn add_task_counts_of_thread(&self, thread_idx: usize, chunk_sizes: Vec) { + self.task_counts.push((thread_idx, chunk_sizes)); } } diff --git a/src/executor/executor_with_diagnostics/thread_executor.rs b/src/executor/executor_with_diagnostics/thread_executor.rs index 764c5ef5..36c47810 100644 --- a/src/executor/executor_with_diagnostics/thread_executor.rs +++ b/src/executor/executor_with_diagnostics/thread_executor.rs @@ -8,6 +8,7 @@ where { thread_idx: usize, executor: E::ThreadExecutor, + task_counts: Vec, // (thread_idx, chunk_size) } impl ThreadExecutorWithDiagnostics @@ -18,6 +19,7 @@ where Self { thread_idx, executor, + task_counts: vec![], } } } @@ -40,12 +42,13 @@ where } fn complete_chunk(&mut self, shared_state: &Self::SharedState, chunk_size: usize) { - shared_state.add_task_count(self.thread_idx, chunk_size); + self.task_counts.push(chunk_size); self.executor .complete_chunk(shared_state.inner(), chunk_size); } fn complete_task(&mut self, shared_state: &Self::SharedState) { self.executor.complete_task(shared_state.inner()); + shared_state.add_task_counts_of_thread(self.thread_idx, self.task_counts.clone()); } } From 65dc8848a3ea77f2a52a13419bef4b5b4ef7d245 Mon Sep 17 00:00:00 2001 From: orxfun Date: Sun, 19 Oct 2025 18:45:45 +0200 Subject: [PATCH 29/96] executor complete task is called --- .../parallel_executor.rs | 5 ++++ .../fixed_chunk_executor/parallel_executor.rs | 2 ++ src/executor/parallel_executor.rs | 3 +++ src/runner/parallel_runner.rs | 27 +++++++++++-------- 4 files changed, 26 insertions(+), 11 deletions(-) diff --git a/src/executor/executor_with_diagnostics/parallel_executor.rs b/src/executor/executor_with_diagnostics/parallel_executor.rs index 3002053b..b70e2313 100644 --- a/src/executor/executor_with_diagnostics/parallel_executor.rs +++ b/src/executor/executor_with_diagnostics/parallel_executor.rs @@ -59,4 +59,9 @@ where .new_thread_executor(thread_idx, shared_state.inner()); ThreadExecutorWithDiagnostics::new(thread_idx, executor) } + + fn complete_task(&mut self, shared_state: Self::SharedState) { + // + todo!() + } } diff --git a/src/executor/fixed_chunk_executor/parallel_executor.rs b/src/executor/fixed_chunk_executor/parallel_executor.rs index b6316907..fd91a293 100644 --- a/src/executor/fixed_chunk_executor/parallel_executor.rs +++ b/src/executor/fixed_chunk_executor/parallel_executor.rs @@ -116,4 +116,6 @@ impl ParallelExecutor for FixedChunkRunner { chunk_size: self.current_chunk_size.load(Ordering::Relaxed), } } + + fn complete_task(&mut self, _: Self::SharedState) {} } diff --git a/src/executor/parallel_executor.rs b/src/executor/parallel_executor.rs index b7780623..6c581413 100644 --- a/src/executor/parallel_executor.rs +++ b/src/executor/parallel_executor.rs @@ -48,4 +48,7 @@ pub trait ParallelExecutor: Sized + Sync + 'static { thread_idx: usize, shared_state: &Self::SharedState, ) -> Self::ThreadExecutor; + + /// Executes the finalization tasks when the entire parallel computation is completed. + fn complete_task(&mut self, shared_state: Self::SharedState); } diff --git a/src/runner/parallel_runner.rs b/src/runner/parallel_runner.rs index 16fb1fef..bc03eba4 100644 --- a/src/runner/parallel_runner.rs +++ b/src/runner/parallel_runner.rs @@ -47,19 +47,21 @@ pub trait ParallelRunner { I: ConcurrentIter, F: Fn(NumSpawned, &I, &SharedStateOf, ThreadRunnerOf) + Sync, { - let runner = self.new_executor(kind, params, iter.try_get_len()); - let state = runner.new_shared_state(); - let do_spawn = |num_spawned| runner.do_spawn_new(num_spawned, &state, &iter); + let mut executor = self.new_executor(kind, params, iter.try_get_len()); + let state = executor.new_shared_state(); + let do_spawn = |num_spawned| executor.do_spawn_new(num_spawned, &state, &iter); let work = |num_spawned: NumSpawned| { let thread_idx = num_spawned.into_inner(); thread_do( num_spawned, &iter, &state, - runner.new_thread_executor(thread_idx, &state), + executor.new_thread_executor(thread_idx, &state), ); }; - self.thread_pool_mut().run_in_pool(do_spawn, work) + let result = self.thread_pool_mut().run_in_pool(do_spawn, work); + executor.complete_task(state); + result } /// Runs `thread_map` using threads provided by the thread pool. @@ -79,21 +81,24 @@ pub trait ParallelRunner { F::Error: Send, { let iter_len = iter.try_get_len(); - let runner = self.new_executor(kind, params, iter_len); - let state = runner.new_shared_state(); - let do_spawn = |num_spawned| runner.do_spawn_new(num_spawned, &state, &iter); + let mut executor = self.new_executor(kind, params, iter_len); + let state = executor.new_shared_state(); + let do_spawn = |num_spawned| executor.do_spawn_new(num_spawned, &state, &iter); let work = |num_spawned: NumSpawned| { let thread_idx = num_spawned.into_inner(); thread_map( num_spawned, &iter, &state, - runner.new_thread_executor(thread_idx, &state), + executor.new_thread_executor(thread_idx, &state), ) }; let max_num_threads = self.max_num_threads_for_computation(params, iter_len); - self.thread_pool_mut() - .map_in_pool::(do_spawn, work, max_num_threads) + let result = + self.thread_pool_mut() + .map_in_pool::(do_spawn, work, max_num_threads); + executor.complete_task(state); + result } /// Runs infallible `thread_map` using threads provided by the thread pool. From d247ee1973f14268bacae1a1f3b6bf86a0a67bfe Mon Sep 17 00:00:00 2001 From: orxfun Date: Sun, 19 Oct 2025 18:49:46 +0200 Subject: [PATCH 30/96] expose executor with diagnostics --- src/executor/executor_with_diagnostics/mod.rs | 2 ++ src/executor/mod.rs | 1 + src/lib.rs | 4 +++- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/executor/executor_with_diagnostics/mod.rs b/src/executor/executor_with_diagnostics/mod.rs index 82e1d5b4..cbf13205 100644 --- a/src/executor/executor_with_diagnostics/mod.rs +++ b/src/executor/executor_with_diagnostics/mod.rs @@ -1,3 +1,5 @@ mod parallel_executor; mod shared_state; mod thread_executor; + +pub use parallel_executor::ParallelExecutorWithDiagnostics; diff --git a/src/executor/mod.rs b/src/executor/mod.rs index b3840383..e38aaa25 100644 --- a/src/executor/mod.rs +++ b/src/executor/mod.rs @@ -5,6 +5,7 @@ mod parallel_executor; mod thread_compute; mod thread_executor; +pub use executor_with_diagnostics::ParallelExecutorWithDiagnostics; pub use parallel_executor::ParallelExecutor; pub use thread_executor::ThreadExecutor; diff --git a/src/lib.rs b/src/lib.rs index 6faef8a1..2148ea6f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -58,7 +58,9 @@ pub mod generic_iterator; mod test_utils; pub use collect_into::ParCollectInto; -pub use executor::{DefaultExecutor, ParallelExecutor, ThreadExecutor}; +pub use executor::{ + DefaultExecutor, ParallelExecutor, ParallelExecutorWithDiagnostics, ThreadExecutor, +}; pub use into_par_iter::IntoParIter; pub use iter::{IntoParIterRec, IntoParIterRecExact}; pub use iter_into_par_iter::IterIntoParIter; From 3cc889b8641dd690e3303f66c1b77ff29490cb82 Mon Sep 17 00:00:00 2001 From: orxfun Date: Sun, 19 Oct 2025 19:02:56 +0200 Subject: [PATCH 31/96] complete task consumes the parallel executor --- examples/par_recursive_iter.rs | 13 ++++++++----- .../executor_with_diagnostics/parallel_executor.rs | 2 +- .../fixed_chunk_executor/parallel_executor.rs | 2 +- src/executor/parallel_executor.rs | 2 +- src/runner/parallel_runner.rs | 4 ++-- 5 files changed, 13 insertions(+), 10 deletions(-) diff --git a/examples/par_recursive_iter.rs b/examples/par_recursive_iter.rs index 7836891a..973fbd3d 100644 --- a/examples/par_recursive_iter.rs +++ b/examples/par_recursive_iter.rs @@ -96,6 +96,7 @@ fn iter(root: &Node) -> u64 { } fn main() { + println!("\n\n"); let mut rng = ChaCha8Rng::seed_from_u64(42); // let root = Node::new(&mut rng, 550); let root = Node::new(&mut rng, 250); @@ -106,13 +107,15 @@ fn main() { let count = root.seq_num_nodes(); println!("Tree contains {count} nodes"); - let expected = root.seq_sum_fib(); + // let expected = root.seq_sum_fib(); - let sum_fib = par_rec(&root); - assert_eq!(sum_fib, expected); - println!("Sum of Fibonacci of node values is {sum_fib}"); + // let sum_fib = par_rec(&root); + // assert_eq!(sum_fib, expected); + // println!("Sum of Fibonacci of node values is {sum_fib}"); let sum_fib = iter(&root); - assert_eq!(sum_fib, expected); + // assert_eq!(sum_fib, expected); println!("Sum of Fibonacci of node values is {sum_fib}"); + + println!("\n\n"); } diff --git a/src/executor/executor_with_diagnostics/parallel_executor.rs b/src/executor/executor_with_diagnostics/parallel_executor.rs index b70e2313..ffa75314 100644 --- a/src/executor/executor_with_diagnostics/parallel_executor.rs +++ b/src/executor/executor_with_diagnostics/parallel_executor.rs @@ -60,7 +60,7 @@ where ThreadExecutorWithDiagnostics::new(thread_idx, executor) } - fn complete_task(&mut self, shared_state: Self::SharedState) { + fn complete_task(self, shared_state: Self::SharedState) { // todo!() } diff --git a/src/executor/fixed_chunk_executor/parallel_executor.rs b/src/executor/fixed_chunk_executor/parallel_executor.rs index fd91a293..e0c4df8b 100644 --- a/src/executor/fixed_chunk_executor/parallel_executor.rs +++ b/src/executor/fixed_chunk_executor/parallel_executor.rs @@ -117,5 +117,5 @@ impl ParallelExecutor for FixedChunkRunner { } } - fn complete_task(&mut self, _: Self::SharedState) {} + fn complete_task(self, _: Self::SharedState) {} } diff --git a/src/executor/parallel_executor.rs b/src/executor/parallel_executor.rs index 6c581413..62e7ad57 100644 --- a/src/executor/parallel_executor.rs +++ b/src/executor/parallel_executor.rs @@ -50,5 +50,5 @@ pub trait ParallelExecutor: Sized + Sync + 'static { ) -> Self::ThreadExecutor; /// Executes the finalization tasks when the entire parallel computation is completed. - fn complete_task(&mut self, shared_state: Self::SharedState); + fn complete_task(self, shared_state: Self::SharedState); } diff --git a/src/runner/parallel_runner.rs b/src/runner/parallel_runner.rs index bc03eba4..6cc9fff7 100644 --- a/src/runner/parallel_runner.rs +++ b/src/runner/parallel_runner.rs @@ -47,7 +47,7 @@ pub trait ParallelRunner { I: ConcurrentIter, F: Fn(NumSpawned, &I, &SharedStateOf, ThreadRunnerOf) + Sync, { - let mut executor = self.new_executor(kind, params, iter.try_get_len()); + let executor = self.new_executor(kind, params, iter.try_get_len()); let state = executor.new_shared_state(); let do_spawn = |num_spawned| executor.do_spawn_new(num_spawned, &state, &iter); let work = |num_spawned: NumSpawned| { @@ -81,7 +81,7 @@ pub trait ParallelRunner { F::Error: Send, { let iter_len = iter.try_get_len(); - let mut executor = self.new_executor(kind, params, iter_len); + let executor = self.new_executor(kind, params, iter_len); let state = executor.new_shared_state(); let do_spawn = |num_spawned| executor.do_spawn_new(num_spawned, &state, &iter); let work = |num_spawned: NumSpawned| { From ac1f3d90178199159a967909b90cb0edd9659067 Mon Sep 17 00:00:00 2001 From: orxfun Date: Sun, 19 Oct 2025 19:11:09 +0200 Subject: [PATCH 32/96] complete task with diagnostics prints the stats --- .../parallel_executor.rs | 3 +-- .../executor_with_diagnostics/shared_state.rs | 26 +++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/executor/executor_with_diagnostics/parallel_executor.rs b/src/executor/executor_with_diagnostics/parallel_executor.rs index ffa75314..700f06c3 100644 --- a/src/executor/executor_with_diagnostics/parallel_executor.rs +++ b/src/executor/executor_with_diagnostics/parallel_executor.rs @@ -61,7 +61,6 @@ where } fn complete_task(self, shared_state: Self::SharedState) { - // - todo!() + shared_state.display(); } } diff --git a/src/executor/executor_with_diagnostics/shared_state.rs b/src/executor/executor_with_diagnostics/shared_state.rs index 57b5078e..4c256443 100644 --- a/src/executor/executor_with_diagnostics/shared_state.rs +++ b/src/executor/executor_with_diagnostics/shared_state.rs @@ -22,4 +22,30 @@ impl SharedStateWithDiagnostics { pub fn add_task_counts_of_thread(&self, thread_idx: usize, chunk_sizes: Vec) { self.task_counts.push((thread_idx, chunk_sizes)); } + + pub fn display(self) { + let mut task_counts = self.task_counts.into_inner().to_vec(); + task_counts.sort_by_key(|x| x.0); + + println!("\n# Parallel Executor Diagnostics"); + println!("\n- Number of threads used = {}", task_counts.len()); + println!(); + + println!("\n- Threads"); + + for (thread_idx, task_counts) in task_counts { + let total: usize = task_counts.iter().sum(); + let num_calls = task_counts.len(); + let avg_chunk_size = match num_calls { + 0 => 0, + n => total / n, + }; + let first_chunks: Vec<_> = task_counts.iter().copied().take(10).collect(); + println!("\n- Thread # {}", thread_idx); + println!(" - total number of calls = {}", num_calls); + println!(" - total number of tasks = {}", total); + println!(" - average chunk size = {}", avg_chunk_size); + println!(" - first chunks sizes = {:?}", first_chunks); + } + } } From 20de751ae59bea04ed3dcdd77bbf4072c4a8843b Mon Sep 17 00:00:00 2001 From: orxfun Date: Sun, 19 Oct 2025 19:16:21 +0200 Subject: [PATCH 33/96] with_diagnostics is added for default runner --- examples/par_recursive_iter.rs | 12 ++++++++---- src/runner/implementations/runner_with_pool.rs | 12 +++++++++++- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/examples/par_recursive_iter.rs b/examples/par_recursive_iter.rs index 973fbd3d..7d67af7a 100644 --- a/examples/par_recursive_iter.rs +++ b/examples/par_recursive_iter.rs @@ -52,8 +52,12 @@ fn par_rec(root: &Node) -> u64 { } let count = root.seq_num_nodes(); + let runner = DefaultRunner::default().with_diagnostics(); + [root] .into_par_rec_exact(extend, count) + .with_runner(runner) + // .with_runner(DefaultRunner::with_executor(self)) // .chunk_size(1024 * 1024) .num_threads(32) .map(|x| fibonacci(x.value)) @@ -109,13 +113,13 @@ fn main() { // let expected = root.seq_sum_fib(); - // let sum_fib = par_rec(&root); + let sum_fib = par_rec(&root); // assert_eq!(sum_fib, expected); - // println!("Sum of Fibonacci of node values is {sum_fib}"); + println!("Sum of Fibonacci of node values is {sum_fib}"); - let sum_fib = iter(&root); + // let sum_fib = iter(&root); // assert_eq!(sum_fib, expected); - println!("Sum of Fibonacci of node values is {sum_fib}"); + // println!("Sum of Fibonacci of node values is {sum_fib}"); println!("\n\n"); } diff --git a/src/runner/implementations/runner_with_pool.rs b/src/runner/implementations/runner_with_pool.rs index a96585d8..d809bbc2 100644 --- a/src/runner/implementations/runner_with_pool.rs +++ b/src/runner/implementations/runner_with_pool.rs @@ -1,4 +1,7 @@ -use crate::{DefaultExecutor, ParThreadPool, ParallelExecutor, runner::ParallelRunner}; +use crate::{ + DefaultExecutor, ParThreadPool, ParallelExecutor, ParallelExecutorWithDiagnostics, + runner::ParallelRunner, +}; use core::marker::PhantomData; /// Parallel runner with a given pool of type `P` and parallel executor of `R`. @@ -181,6 +184,13 @@ where runner: PhantomData, } } + + pub fn with_diagnostics(self) -> RunnerWithPool> { + RunnerWithPool { + pool: self.pool, + runner: PhantomData, + } + } } impl ParallelRunner for RunnerWithPool From e8d596bf78d7fbee2e95dcd663da03157626e7a6 Mon Sep 17 00:00:00 2001 From: orxfun Date: Sun, 19 Oct 2025 19:20:54 +0200 Subject: [PATCH 34/96] update diagnostics display --- .../executor_with_diagnostics/shared_state.rs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/executor/executor_with_diagnostics/shared_state.rs b/src/executor/executor_with_diagnostics/shared_state.rs index 4c256443..b5bc58a1 100644 --- a/src/executor/executor_with_diagnostics/shared_state.rs +++ b/src/executor/executor_with_diagnostics/shared_state.rs @@ -29,9 +29,8 @@ impl SharedStateWithDiagnostics { println!("\n# Parallel Executor Diagnostics"); println!("\n- Number of threads used = {}", task_counts.len()); - println!(); - println!("\n- Threads"); + println!("\n- [Thread idx]: num_calls, num_tasks, avg_chunk_size, first_chunk_sizes"); for (thread_idx, task_counts) in task_counts { let total: usize = task_counts.iter().sum(); @@ -41,11 +40,10 @@ impl SharedStateWithDiagnostics { n => total / n, }; let first_chunks: Vec<_> = task_counts.iter().copied().take(10).collect(); - println!("\n- Thread # {}", thread_idx); - println!(" - total number of calls = {}", num_calls); - println!(" - total number of tasks = {}", total); - println!(" - average chunk size = {}", avg_chunk_size); - println!(" - first chunks sizes = {:?}", first_chunks); + + println!( + " - [{thread_idx}]: {num_calls}, {total}, {avg_chunk_size}, {first_chunks:?}", + ); } } } From 8835bbceed32db652ab89188088913f1e0cf0d3a Mon Sep 17 00:00:00 2001 From: orxfun Date: Sun, 19 Oct 2025 19:23:05 +0200 Subject: [PATCH 35/96] diagnostics reveals the problem with recursive parallel iterator, some threads early exit --- examples/par_recursive_iter.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/par_recursive_iter.rs b/examples/par_recursive_iter.rs index 7d67af7a..0b6613b1 100644 --- a/examples/par_recursive_iter.rs +++ b/examples/par_recursive_iter.rs @@ -102,8 +102,8 @@ fn iter(root: &Node) -> u64 { fn main() { println!("\n\n"); let mut rng = ChaCha8Rng::seed_from_u64(42); - // let root = Node::new(&mut rng, 550); - let root = Node::new(&mut rng, 250); + let root = Node::new(&mut rng, 550); + // let root = Node::new(&mut rng, 250); // let par = [&root].into_par_rec(extend); // let count = par.count(); From b5eecd5c8c6d3859101dd07a37e9aebde92175b1 Mon Sep 17 00:00:00 2001 From: orxfun Date: Mon, 20 Oct 2025 11:05:39 +0200 Subject: [PATCH 36/96] wip --- Cargo.toml | 3 +- benches/par_recursive_iter.rs | 2 +- examples/par_recursive_iter.rs | 50 ++++++++++++++++++++++++++-------- 3 files changed, 42 insertions(+), 13 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a7ea9376..cacf4471 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,8 @@ orx-pinned-concurrent-col = { version = "2.17.0", default-features = false } orx-iterable = { version = "1.3.0", default-features = false } orx-priority-queue = { version = "1.7.0", default-features = false } orx-pseudo-default = { version = "2.1.0", default-features = false } -orx-concurrent-recursive-iter = { git = "https://github.com/orxfun/orx-concurrent-recursive-iter", branch = "exact-sized-con-iter" } +# orx-concurrent-recursive-iter = { git = "https://github.com/orxfun/orx-concurrent-recursive-iter", branch = "exact-sized-con-iter" } +orx-concurrent-recursive-iter = { path = "../orx-concurrent-recursive-iter" } # optional: generic iterator rayon = { version = "1.11.0", optional = true, default-features = false } diff --git a/benches/par_recursive_iter.rs b/benches/par_recursive_iter.rs index 25327cbe..27d9c57d 100644 --- a/benches/par_recursive_iter.rs +++ b/benches/par_recursive_iter.rs @@ -160,7 +160,7 @@ fn iter(root: &Node) -> u64 { fn run(c: &mut Criterion) { let mut rng = ChaCha8Rng::seed_from_u64(42); - let root = Node::new(&mut rng, 500); + let root = Node::new(&mut rng, 300); let n = &root.seq_num_nodes(); let expected = root.seq_sum_fib(); diff --git a/examples/par_recursive_iter.rs b/examples/par_recursive_iter.rs index 0b6613b1..da041ddd 100644 --- a/examples/par_recursive_iter.rs +++ b/examples/par_recursive_iter.rs @@ -10,15 +10,20 @@ struct Node { } fn fibonacci(n: u64) -> u64 { - let n = n % 42; // let's not overflow - let mut a = 0; - let mut b = 1; - for _ in 0..n { - let c = a + b; - a = b; - b = c; - } - a + (0..100) + .map(|j| { + let n = n + j; + // let n = n % 42; // let's not overflow + let mut a = 0; + let mut b = 1; + for _ in 0..n { + let c = a + b; + a = b; + b = c; + } + a + }) + .sum() } impl Node { @@ -58,12 +63,31 @@ fn par_rec(root: &Node) -> u64 { .into_par_rec_exact(extend, count) .with_runner(runner) // .with_runner(DefaultRunner::with_executor(self)) - // .chunk_size(1024 * 1024) + // .chunk_size(64) .num_threads(32) .map(|x| fibonacci(x.value)) .sum() } +fn par_rec_eager(root: &Node) -> u64 { + fn extend<'a, 'b>(node: &'a &'b Node) -> &'b [Node] { + &node.children + } + let count = root.seq_num_nodes(); + + let runner = DefaultRunner::default().with_diagnostics(); + + [root] + .into_par_rec_exact(extend, count) + .into_eager() + .with_runner(runner) + // .with_runner(DefaultRunner::with_executor(self)) + // .chunk_size(1024 * 1024) + // .num_threads(1024) + .map(|x| fibonacci(x.value)) + .sum() +} + fn iter(root: &Node) -> u64 { use orx_concurrent_iter::*; fn extend<'a, 'b>(node: &'a &'b Node) -> &'b [Node] { @@ -102,7 +126,7 @@ fn iter(root: &Node) -> u64 { fn main() { println!("\n\n"); let mut rng = ChaCha8Rng::seed_from_u64(42); - let root = Node::new(&mut rng, 550); + let root = Node::new(&mut rng, 100); // let root = Node::new(&mut rng, 250); // let par = [&root].into_par_rec(extend); @@ -113,6 +137,10 @@ fn main() { // let expected = root.seq_sum_fib(); + // let sum_fib = par_rec_eager(&root); + // // assert_eq!(sum_fib, expected); + // println!("Sum of Fibonacci of node values is {sum_fib}"); + let sum_fib = par_rec(&root); // assert_eq!(sum_fib, expected); println!("Sum of Fibonacci of node values is {sum_fib}"); From 5db891399af6075c077abca2ca82587876022336 Mon Sep 17 00:00:00 2001 From: orxfun Date: Mon, 20 Oct 2025 11:16:15 +0200 Subject: [PATCH 37/96] example uses stopping condition --- examples/par_recursive_iter.rs | 18 +++++++++++++----- src/lib.rs | 2 +- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/examples/par_recursive_iter.rs b/examples/par_recursive_iter.rs index da041ddd..e05dde48 100644 --- a/examples/par_recursive_iter.rs +++ b/examples/par_recursive_iter.rs @@ -98,7 +98,6 @@ fn iter(root: &Node) -> u64 { let chunk_size = 1024; let iter = ConcurrentRecursiveIter::new(extend, [root]); let num_spawned = core::sync::atomic::AtomicUsize::new(0); - let num_handled = core::sync::atomic::AtomicUsize::new(0); std::thread::scope(|s| { let mut handles = vec![]; @@ -111,8 +110,17 @@ fn iter(root: &Node) -> u64 { // computation: parallel reduction let mut thread_sum = 0; let mut puller = iter.chunk_puller(chunk_size); - while let Some(chunk) = puller.pull() { - thread_sum += chunk.into_iter().map(|x| fibonacci(x.value)).sum::(); + loop { + match puller.pull() { + Some(chunk) => { + thread_sum += chunk.into_iter().map(|x| fibonacci(x.value)).sum::() + } + None => { + if iter.is_completed() { + break; + } + } + } } thread_sum @@ -145,9 +153,9 @@ fn main() { // assert_eq!(sum_fib, expected); println!("Sum of Fibonacci of node values is {sum_fib}"); - // let sum_fib = iter(&root); + let sum_fib = iter(&root); // assert_eq!(sum_fib, expected); - // println!("Sum of Fibonacci of node values is {sum_fib}"); + println!("Sum of Fibonacci of node values is {sum_fib}"); println!("\n\n"); } diff --git a/src/lib.rs b/src/lib.rs index 2148ea6f..64f9e031 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,7 +14,7 @@ extern crate alloc; -#[cfg(any(test, feature = "std"))] +// #[cfg(any(test, feature = "std"))] extern crate std; mod collect_into; From 76d939384376aa012c48212c819b7d8fd2e192b8 Mon Sep 17 00:00:00 2001 From: orxfun Date: Mon, 20 Oct 2025 11:21:26 +0200 Subject: [PATCH 38/96] temporarily opt out pinned vec con iter trait bound tests --- tests/trait_bounds.rs | 92 +++++++++++++++++++++---------------------- 1 file changed, 46 insertions(+), 46 deletions(-) diff --git a/tests/trait_bounds.rs b/tests/trait_bounds.rs index 5ac87c42..81a45b1a 100644 --- a/tests/trait_bounds.rs +++ b/tests/trait_bounds.rs @@ -1,53 +1,53 @@ -use orx_fixed_vec::FixedVec; -use orx_split_vec::SplitVec; -use std::collections::VecDeque; +// use orx_fixed_vec::FixedVec; +// use orx_split_vec::SplitVec; +// use std::collections::VecDeque; -#[test] -fn trait_bounds_parallelizable() { - use orx_parallel::Parallelizable; - fn fun(source: impl Parallelizable) { - let _iter = source.par(); - } +// #[test] +// fn trait_bounds_parallelizable() { +// use orx_parallel::Parallelizable; +// fn fun(source: impl Parallelizable) { +// let _iter = source.par(); +// } - fun(vec![1, 2, 3].as_slice()); - fun(&vec![1, 2, 3]); - fun(&VecDeque::::new()); - fun(0..9); - fun(&FixedVec::::new(3)); - fun(&SplitVec::::new()); -} +// fun(vec![1, 2, 3].as_slice()); +// fun(&vec![1, 2, 3]); +// fun(&VecDeque::::new()); +// fun(0..9); +// fun(&FixedVec::::new(3)); +// fun(&SplitVec::::new()); +// } -#[test] -fn trait_bounds_parallelizable_collection() { - use orx_parallel::ParallelizableCollection; - fn fun(source: impl ParallelizableCollection) { - let _iter = source.par(); - } +// #[test] +// fn trait_bounds_parallelizable_collection() { +// use orx_parallel::ParallelizableCollection; +// fn fun(source: impl ParallelizableCollection) { +// let _iter = source.par(); +// } - fun(vec![1, 2, 3]); - fun(VecDeque::::new()); - fun(FixedVec::::new(3)); - fun(SplitVec::::new()); -} +// fun(vec![1, 2, 3]); +// fun(VecDeque::::new()); +// fun(FixedVec::::new(3)); +// fun(SplitVec::::new()); +// } -#[test] -fn trait_bounds_into_par_iter() { - use orx_parallel::IntoParIter; - fn fun(source: impl IntoParIter) { - let _iter = source.into_par(); - } +// #[test] +// fn trait_bounds_into_par_iter() { +// use orx_parallel::IntoParIter; +// fn fun(source: impl IntoParIter) { +// let _iter = source.into_par(); +// } - // owned - fun(vec![1, 2, 3]); - fun(VecDeque::::new()); - fun(FixedVec::::new(3)); - fun(SplitVec::::new()); +// // owned +// fun(vec![1, 2, 3]); +// fun(VecDeque::::new()); +// fun(FixedVec::::new(3)); +// fun(SplitVec::::new()); - // ref - fun(vec![1, 2, 3].as_slice()); - fun(&vec![1, 2, 3]); - fun(&VecDeque::::new()); - fun(0..9); - fun(&FixedVec::::new(3)); - fun(&SplitVec::::new()); -} +// // ref +// fun(vec![1, 2, 3].as_slice()); +// fun(&vec![1, 2, 3]); +// fun(&VecDeque::::new()); +// fun(0..9); +// fun(&FixedVec::::new(3)); +// fun(&SplitVec::::new()); +// } From 711a469c02c4128303d2121def03142a89cc1e04 Mon Sep 17 00:00:00 2001 From: orxfun Date: Mon, 20 Oct 2025 11:41:10 +0200 Subject: [PATCH 39/96] revise example and benches on recursive iteration --- Cargo.toml | 5 +-- ...{par_recursive_iter.rs => rec_iter_sum.rs} | 25 ++++++------- examples/par_recursive_iter.rs | 35 ++++++++----------- src/executor/thread_compute/reduce.rs | 12 +++++-- 4 files changed, 38 insertions(+), 39 deletions(-) rename benches/{par_recursive_iter.rs => rec_iter_sum.rs} (93%) diff --git a/Cargo.toml b/Cargo.toml index cacf4471..82c594c9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,8 @@ categories = ["concurrency", "algorithms"] orx-pinned-vec = { version = "3.20.0", default-features = false } orx-fixed-vec = { version = "3.21.0", default-features = false } orx-split-vec = { version = "3.21.0", default-features = false } -orx-concurrent-iter = { version = "3.2.0", default-features = false } +# orx-concurrent-iter = { version = "3.2.0", default-features = false } +orx-concurrent-iter = { path = "../orx-concurrent-iter", default-features = false } orx-concurrent-bag = { version = "3.3.0", default-features = false } orx-concurrent-ordered-bag = { version = "3.3.0", default-features = false } orx-pinned-concurrent-col = { version = "2.17.0", default-features = false } @@ -47,7 +48,7 @@ rayon = "1.11.0" test-case = "3.3.1" [[bench]] -name = "par_recursive_iter" +name = "rec_iter_sum" harness = false [package.metadata.docs.rs] diff --git a/benches/par_recursive_iter.rs b/benches/rec_iter_sum.rs similarity index 93% rename from benches/par_recursive_iter.rs rename to benches/rec_iter_sum.rs index 27d9c57d..99cfc9f8 100644 --- a/benches/par_recursive_iter.rs +++ b/benches/rec_iter_sum.rs @@ -14,20 +14,15 @@ struct Node { } fn fibonacci(n: u64) -> u64 { - // let n = n % 42; // let's not overflow - (0..100) - .map(|i| { - let n = i + n; - let mut a = 0; - let mut b = 1; - for _ in 0..n { - let c = a + b; - a = b; - b = c; - } - a - }) - .sum() + let n = black_box(n % 100); + let mut a = 0; + let mut b = 1; + for _ in 0..n { + let c = a + b; + a = b; + b = c; + } + a } impl Node { @@ -164,7 +159,7 @@ fn run(c: &mut Criterion) { let n = &root.seq_num_nodes(); let expected = root.seq_sum_fib(); - let mut group = c.benchmark_group("par_recursive_iter"); + let mut group = c.benchmark_group("rec_iter_sum"); group.bench_with_input(BenchmarkId::new("seq", n), n, |b, _| { assert_eq!(&expected, &seq(&root)); diff --git a/examples/par_recursive_iter.rs b/examples/par_recursive_iter.rs index e05dde48..54a3b7f7 100644 --- a/examples/par_recursive_iter.rs +++ b/examples/par_recursive_iter.rs @@ -2,7 +2,7 @@ use orx_concurrent_recursive_iter::ConcurrentRecursiveIter; use orx_parallel::*; use rand::{Rng, SeedableRng}; use rand_chacha::ChaCha8Rng; -use std::sync::atomic::Ordering; +use std::{hint::black_box, sync::atomic::Ordering}; struct Node { value: u64, @@ -10,20 +10,15 @@ struct Node { } fn fibonacci(n: u64) -> u64 { - (0..100) - .map(|j| { - let n = n + j; - // let n = n % 42; // let's not overflow - let mut a = 0; - let mut b = 1; - for _ in 0..n { - let c = a + b; - a = b; - b = c; - } - a - }) - .sum() + let n = black_box(n % 100); + let mut a = 0; + let mut b = 1; + for _ in 0..n { + let c = a + b; + a = b; + b = c; + } + a } impl Node { @@ -116,7 +111,7 @@ fn iter(root: &Node) -> u64 { thread_sum += chunk.into_iter().map(|x| fibonacci(x.value)).sum::() } None => { - if iter.is_completed() { + if iter.is_completed_when_none_returned() { break; } } @@ -134,7 +129,7 @@ fn iter(root: &Node) -> u64 { fn main() { println!("\n\n"); let mut rng = ChaCha8Rng::seed_from_u64(42); - let root = Node::new(&mut rng, 100); + let root = Node::new(&mut rng, 300); // let root = Node::new(&mut rng, 250); // let par = [&root].into_par_rec(extend); @@ -153,9 +148,9 @@ fn main() { // assert_eq!(sum_fib, expected); println!("Sum of Fibonacci of node values is {sum_fib}"); - let sum_fib = iter(&root); - // assert_eq!(sum_fib, expected); - println!("Sum of Fibonacci of node values is {sum_fib}"); + // let sum_fib = iter(&root); + // // assert_eq!(sum_fib, expected); + // println!("Sum of Fibonacci of node values is {sum_fib}"); println!("\n\n"); } diff --git a/src/executor/thread_compute/reduce.rs b/src/executor/thread_compute/reduce.rs index 66f03bac..81b70bd9 100644 --- a/src/executor/thread_compute/reduce.rs +++ b/src/executor/thread_compute/reduce.rs @@ -40,7 +40,11 @@ where None => Some(y), }; } - None => break, + None => { + if iter.is_completed_when_none_returned() { + break; + } + } }, c => { if c > chunk_puller.chunk_size() { @@ -59,7 +63,11 @@ where None => res, }; } - None => break, + None => { + if iter.is_completed_when_none_returned() { + break; + } + } } } } From 1c2215ff086d6f1378436c6377560638ff95655b Mon Sep 17 00:00:00 2001 From: orxfun Date: Mon, 20 Oct 2025 11:43:19 +0200 Subject: [PATCH 40/96] all breaks made conditional on the iterator to be completed even if none is returned --- .../thread_compute/collect_arbitrary.rs | 24 +++++++++++++++---- .../thread_compute/collect_ordered.rs | 24 +++++++++++++++---- src/executor/thread_compute/next.rs | 24 +++++++++++++++---- src/executor/thread_compute/next_any.rs | 24 +++++++++++++++---- src/executor/thread_compute/reduce.rs | 12 ++++++++-- 5 files changed, 90 insertions(+), 18 deletions(-) diff --git a/src/executor/thread_compute/collect_arbitrary.rs b/src/executor/thread_compute/collect_arbitrary.rs index 0b632e35..7e62a608 100644 --- a/src/executor/thread_compute/collect_arbitrary.rs +++ b/src/executor/thread_compute/collect_arbitrary.rs @@ -32,7 +32,11 @@ pub fn m( match chunk_size { 0 | 1 => match item_puller.next() { Some(value) => _ = bag.push(map1(value)), - None => break, + None => { + if iter.is_completed_when_none_returned() { + break; + } + } }, c => { if c > chunk_puller.chunk_size() { @@ -41,7 +45,11 @@ pub fn m( match chunk_puller.pull() { Some(chunk) => _ = bag.extend(chunk.map(&map1)), - None => break, + None => { + if iter.is_completed_when_none_returned() { + break; + } + } } } } @@ -98,7 +106,11 @@ where } } } - None => break, + None => { + if iter.is_completed_when_none_returned() { + break; + } + } }, c => { if c > chunk_puller.chunk_size() { @@ -126,7 +138,11 @@ where } } } - None => break, + None => { + if iter.is_completed_when_none_returned() { + break; + } + } } } } diff --git a/src/executor/thread_compute/collect_ordered.rs b/src/executor/thread_compute/collect_ordered.rs index 10984a02..ba60456b 100644 --- a/src/executor/thread_compute/collect_ordered.rs +++ b/src/executor/thread_compute/collect_ordered.rs @@ -30,7 +30,11 @@ pub fn m( match chunk_size { 0 | 1 => match item_puller.next() { Some((idx, value)) => unsafe { o_bag.set_value(offset + idx, map1(value)) }, - None => break, + None => { + if iter.is_completed_when_none_returned() { + break; + } + } }, c => { if c > chunk_puller.chunk_size() { @@ -42,7 +46,11 @@ pub fn m( let values = chunk.map(map1); unsafe { o_bag.set_values(offset + begin_idx, values) }; } - None => break, + None => { + if iter.is_completed_when_none_returned() { + break; + } + } } } } @@ -98,7 +106,11 @@ where } } } - None => break, + None => { + if iter.is_completed_when_none_returned() { + break; + } + } }, c => { if c > chunk_puller.chunk_size() { @@ -128,7 +140,11 @@ where } } } - None => break, + None => { + if iter.is_completed_when_none_returned() { + break; + } + } } } } diff --git a/src/executor/thread_compute/next.rs b/src/executor/thread_compute/next.rs index d6d6fac8..15ee2072 100644 --- a/src/executor/thread_compute/next.rs +++ b/src/executor/thread_compute/next.rs @@ -33,7 +33,11 @@ where runner.complete_task(shared_state); return Some((idx, first)); } - None => break, + None => { + if iter.is_completed_when_none_returned() { + break; + } + } }, c => { if c > chunk_puller.chunk_size() { @@ -50,7 +54,11 @@ where return Some((idx, first)); } } - None => break, + None => { + if iter.is_completed_when_none_returned() { + break; + } + } } } } @@ -109,7 +117,11 @@ where } } } - None => break, + None => { + if iter.is_completed_when_none_returned() { + break; + } + } }, c => { if c > chunk_puller.chunk_size() { @@ -144,7 +156,11 @@ where } } } - None => break, + None => { + if iter.is_completed_when_none_returned() { + break; + } + } } } } diff --git a/src/executor/thread_compute/next_any.rs b/src/executor/thread_compute/next_any.rs index a09926f7..f369d75f 100644 --- a/src/executor/thread_compute/next_any.rs +++ b/src/executor/thread_compute/next_any.rs @@ -34,7 +34,11 @@ where runner.complete_task(shared_state); return Some(first); } - None => break, + None => { + if iter.is_completed_when_none_returned() { + break; + } + } }, c => { if c > chunk_puller.chunk_size() { @@ -51,7 +55,11 @@ where return Some(first); } } - None => break, + None => { + if iter.is_completed_when_none_returned() { + break; + } + } } } } @@ -111,7 +119,11 @@ where } } } - None => break, + None => { + if iter.is_completed_when_none_returned() { + break; + } + } }, c => { if c > chunk_puller.chunk_size() { @@ -146,7 +158,11 @@ where } } } - None => break, + None => { + if iter.is_completed_when_none_returned() { + break; + } + } } } } diff --git a/src/executor/thread_compute/reduce.rs b/src/executor/thread_compute/reduce.rs index 81b70bd9..b65f1fd9 100644 --- a/src/executor/thread_compute/reduce.rs +++ b/src/executor/thread_compute/reduce.rs @@ -126,7 +126,11 @@ where } }; } - None => break, + None => { + if iter.is_completed_when_none_returned() { + break; + } + } }, c => { if c > chunk_puller.chunk_size() { @@ -156,7 +160,11 @@ where }; } } - None => break, + None => { + if iter.is_completed_when_none_returned() { + break; + } + } } } } From b1c0534c12c2e3bf463039d8bb9aaf804877effe Mon Sep 17 00:00:00 2001 From: orxfun Date: Mon, 20 Oct 2025 15:35:20 +0200 Subject: [PATCH 41/96] update recursive iter examples and benches --- Cargo.toml | 2 +- benches/rec_iter_map_collect.rs | 201 ++++++++++++++++++++++++++++++ benches/rec_iter_map_sum.rs | 187 ++++++++++++++++++++++++++++ benches/rec_iter_sum.rs | 203 ------------------------------- examples/par_recursive_iter.rs | 156 ------------------------ examples/rec_iter_map_collect.rs | 129 ++++++++++++++++++++ examples/rec_iter_map_sum.rs | 120 ++++++++++++++++++ 7 files changed, 638 insertions(+), 360 deletions(-) create mode 100644 benches/rec_iter_map_collect.rs create mode 100644 benches/rec_iter_map_sum.rs delete mode 100644 benches/rec_iter_sum.rs delete mode 100644 examples/par_recursive_iter.rs create mode 100644 examples/rec_iter_map_collect.rs create mode 100644 examples/rec_iter_map_sum.rs diff --git a/Cargo.toml b/Cargo.toml index 82c594c9..f3bc98ce 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,7 +48,7 @@ rayon = "1.11.0" test-case = "3.3.1" [[bench]] -name = "rec_iter_sum" +name = "rec_iter_map_collect" harness = false [package.metadata.docs.rs] diff --git a/benches/rec_iter_map_collect.rs b/benches/rec_iter_map_collect.rs new file mode 100644 index 00000000..addd0ec7 --- /dev/null +++ b/benches/rec_iter_map_collect.rs @@ -0,0 +1,201 @@ +use criterion::{BenchmarkId, Criterion, criterion_group, criterion_main}; +use orx_concurrent_bag::ConcurrentBag; +use orx_parallel::*; +use orx_split_vec::SplitVec; +use rand::prelude::*; +use rand_chacha::ChaCha8Rng; +use std::{hint::black_box, sync::atomic::AtomicU64}; + +fn fibonacci(n: u64, work: usize) -> u64 { + (7..(work + 7)) + .map(|j| { + let n = black_box((n + j as u64) % 100); + let mut a = 0; + let mut b = 1; + for _ in 0..n { + let c = a + b; + a = b; + b = c; + } + a + }) + .sum() +} + +struct Node { + value: Vec, + children: Vec, +} + +impl Node { + fn new(mut n: u32, rng: &mut impl Rng) -> Self { + let mut children = Vec::new(); + if n < 5 { + for _ in 0..n { + children.push(Node::new(0, rng)); + } + } else { + while n > 0 { + let n2 = rng.random_range(0..=n); + children.push(Node::new(n2, rng)); + n -= n2; + } + } + Self { + value: (0..rng.random_range(1..500)) + .map(|_| rng.random_range(0..40)) + .collect(), + children, + } + } + + fn seq_num_nodes(&self) -> usize { + 1 + self + .children + .iter() + .map(|node| node.seq_num_nodes()) + .sum::() + } + + fn seq(&self, work: usize, numbers: &mut Vec) { + numbers.extend(self.value.iter().map(|x| fibonacci(*x, work))); + for c in &self.children { + c.seq(work, numbers); + } + } +} + +// alternatives + +fn seq(roots: &[Node], work: usize) -> Vec { + let mut result = vec![]; + for root in roots { + root.seq(work, &mut result); + } + result +} + +fn rayon(roots: &[Node], work: usize) -> SplitVec { + use rayon::iter::*; + fn process_node<'scope>( + work: usize, + sum: &'scope AtomicU64, + node: &'scope Node, + s: &rayon::Scope<'scope>, + result: &'scope ConcurrentBag, + ) { + for child in &node.children { + s.spawn(move |s| { + process_node(work, sum, child, s, result); + }); + } + let x: Vec<_> = node.value.par_iter().map(|x| fibonacci(*x, work)).collect(); + result.extend(x); + } + + let sum = AtomicU64::new(0); + let result = ConcurrentBag::new(); + rayon::in_place_scope(|s| { + for root in roots { + process_node(work, &sum, root, s, &result); + } + }); + result.into_inner() +} + +fn orx_lazy_unknown_chunk1024(roots: &[Node], work: usize) -> SplitVec { + fn extend<'a, 'b>(node: &'a &'b Node) -> &'b [Node] { + &node.children + } + + roots + .into_par_rec(extend) + .chunk_size(1024) + .flat_map(|x| x.value.iter().map(|x| fibonacci(*x, work))) + .collect() +} + +fn orx_lazy_exact(roots: &[Node], work: usize, num_nodes: usize) -> SplitVec { + fn extend<'a, 'b>(node: &'a &'b Node) -> &'b [Node] { + &node.children + } + + roots + .into_par_rec_exact(extend, num_nodes) + .flat_map(|x| x.value.iter().map(|x| fibonacci(*x, work))) + .collect() +} + +fn orx_eager(roots: &[Node], work: usize) -> SplitVec { + fn extend<'a, 'b>(node: &'a &'b Node) -> &'b [Node] { + &node.children + } + + roots + .into_par_rec(extend) + .into_eager() + .flat_map(|x| x.value.iter().map(|x| fibonacci(*x, work))) + .collect() +} + +fn run(c: &mut Criterion) { + let treatments = [1, 10, 25]; + let mut group = c.benchmark_group("rec_iter_map_collect"); + let mut rng = ChaCha8Rng::seed_from_u64(42); + let roots = vec![ + Node::new(5000, &mut rng), + Node::new(2000, &mut rng), + Node::new(4000, &mut rng), + ]; + + let num_nodes: usize = roots.iter().map(|x| x.seq_num_nodes()).sum(); + + for work in &treatments { + let mut expected = seq(&roots, *work); + expected.sort(); + + group.bench_with_input(BenchmarkId::new("seq", work), work, |b, _| { + let mut result = seq(&roots, *work); + result.sort(); + assert_eq!(&expected, &result); + b.iter(|| seq(&roots, *work)) + }); + + group.bench_with_input(BenchmarkId::new("rayon", work), work, |b, _| { + let mut result = rayon(&roots, *work).to_vec(); + result.sort(); + assert_eq!(&expected, &result); + b.iter(|| rayon(&roots, *work)) + }); + + group.bench_with_input( + BenchmarkId::new("orx_lazy_unknown_chunk1024", work), + work, + |b, _| { + let mut result = orx_lazy_unknown_chunk1024(&roots, *work).to_vec(); + result.sort(); + assert_eq!(&expected, &result); + b.iter(|| orx_lazy_unknown_chunk1024(&roots, *work)) + }, + ); + + group.bench_with_input(BenchmarkId::new("orx_lazy_exact", work), work, |b, _| { + let mut result = orx_lazy_exact(&roots, *work, num_nodes).to_vec(); + result.sort(); + assert_eq!(&expected, &result); + b.iter(|| orx_lazy_exact(&roots, *work, num_nodes)) + }); + + group.bench_with_input(BenchmarkId::new("orx_eager", work), work, |b, _| { + let mut result = orx_eager(&roots, *work).to_vec(); + result.sort(); + assert_eq!(&expected, &result); + b.iter(|| orx_eager(&roots, *work)) + }); + } + + group.finish(); +} + +criterion_group!(benches, run); +criterion_main!(benches); diff --git a/benches/rec_iter_map_sum.rs b/benches/rec_iter_map_sum.rs new file mode 100644 index 00000000..a846eaad --- /dev/null +++ b/benches/rec_iter_map_sum.rs @@ -0,0 +1,187 @@ +use criterion::{BenchmarkId, Criterion, criterion_group, criterion_main}; +use orx_parallel::*; +use rand::prelude::*; +use rand_chacha::ChaCha8Rng; +use std::{ + hint::black_box, + sync::atomic::{AtomicU64, Ordering}, +}; + +fn fibonacci(n: u64, work: usize) -> u64 { + (7..(work + 7)) + .map(|j| { + let n = black_box((n + j as u64) % 100); + let mut a = 0; + let mut b = 1; + for _ in 0..n { + let c = a + b; + a = b; + b = c; + } + a + }) + .sum() +} + +struct Node { + value: Vec, + children: Vec, +} + +impl Node { + fn new(mut n: u32, rng: &mut impl Rng) -> Self { + let mut children = Vec::new(); + if n < 5 { + for _ in 0..n { + children.push(Node::new(0, rng)); + } + } else { + while n > 0 { + let n2 = rng.random_range(0..=n); + children.push(Node::new(n2, rng)); + n -= n2; + } + } + Self { + value: (0..rng.random_range(1..500)) + .map(|_| rng.random_range(0..40)) + .collect(), + children, + } + } + + fn seq_num_nodes(&self) -> usize { + 1 + self + .children + .iter() + .map(|node| node.seq_num_nodes()) + .sum::() + } + + fn seq_sum_fib(&self, work: usize) -> u64 { + self.value.iter().map(|x| fibonacci(*x, work)).sum::() + + self + .children + .iter() + .map(|x| x.seq_sum_fib(work)) + .sum::() + } +} + +// alternatives + +fn seq(roots: &[Node], work: usize) -> u64 { + roots.iter().map(|x| x.seq_sum_fib(work)).sum() +} + +fn rayon(roots: &[Node], work: usize) -> u64 { + use rayon::iter::*; + fn process_node<'scope>( + work: usize, + sum: &'scope AtomicU64, + node: &'scope Node, + s: &rayon::Scope<'scope>, + ) { + for child in &node.children { + s.spawn(move |s| { + process_node(work, sum, child, s); + }); + } + let val = node.value.par_iter().map(|x| fibonacci(*x, work)).sum(); + sum.fetch_add(val, Ordering::Relaxed); + } + + let sum = AtomicU64::new(0); + rayon::in_place_scope(|s| { + for root in roots { + process_node(work, &sum, root, s); + } + }); + sum.into_inner() +} + +fn orx_lazy_unknown_chunk1024(roots: &[Node], work: usize) -> u64 { + fn extend<'a, 'b>(node: &'a &'b Node) -> &'b [Node] { + &node.children + } + + roots + .into_par_rec(extend) + .chunk_size(1024) + .map(|x| x.value.iter().map(|x| fibonacci(*x, work)).sum::()) + .sum() +} + +fn orx_lazy_exact(roots: &[Node], work: usize, num_nodes: usize) -> u64 { + fn extend<'a, 'b>(node: &'a &'b Node) -> &'b [Node] { + &node.children + } + + roots + .into_par_rec_exact(extend, num_nodes) + .map(|x| x.value.iter().map(|x| fibonacci(*x, work)).sum::()) + .sum() +} + +fn orx_eager(roots: &[Node], work: usize) -> u64 { + fn extend<'a, 'b>(node: &'a &'b Node) -> &'b [Node] { + &node.children + } + + roots + .into_par_rec(extend) + .into_eager() + .map(|x| x.value.iter().map(|x| fibonacci(*x, work)).sum::()) + .sum() +} + +fn run(c: &mut Criterion) { + let treatments = [1, 10, 25]; + let mut group = c.benchmark_group("rec_iter_map_sum"); + let mut rng = ChaCha8Rng::seed_from_u64(42); + let roots = vec![ + Node::new(5000, &mut rng), + Node::new(2000, &mut rng), + Node::new(4000, &mut rng), + ]; + + let num_nodes: usize = roots.iter().map(|x| x.seq_num_nodes()).sum(); + + for work in &treatments { + let expected = seq(&roots, *work); + + group.bench_with_input(BenchmarkId::new("seq", work), work, |b, _| { + assert_eq!(&expected, &seq(&roots, *work)); + b.iter(|| seq(&roots, *work)) + }); + + group.bench_with_input(BenchmarkId::new("rayon", work), work, |b, _| { + assert_eq!(&expected, &rayon(&roots, *work)); + b.iter(|| rayon(&roots, *work)) + }); + + group.bench_with_input( + BenchmarkId::new("orx_lazy_unknown_chunk1024", work), + work, + |b, _| { + assert_eq!(&expected, &orx_lazy_unknown_chunk1024(&roots, *work)); + b.iter(|| orx_lazy_unknown_chunk1024(&roots, *work)) + }, + ); + + group.bench_with_input(BenchmarkId::new("orx_lazy_exact", work), work, |b, _| { + assert_eq!(&expected, &orx_lazy_exact(&roots, *work, num_nodes)); + b.iter(|| orx_lazy_exact(&roots, *work, num_nodes)) + }); + + group.bench_with_input(BenchmarkId::new("orx_eager", work), work, |b, _| { + assert_eq!(&expected, &orx_eager(&roots, *work)); + b.iter(|| orx_eager(&roots, *work)) + }); + } + + group.finish(); +} + +criterion_group!(benches, run); +criterion_main!(benches); diff --git a/benches/rec_iter_sum.rs b/benches/rec_iter_sum.rs deleted file mode 100644 index 99cfc9f8..00000000 --- a/benches/rec_iter_sum.rs +++ /dev/null @@ -1,203 +0,0 @@ -use criterion::{BenchmarkId, Criterion, criterion_group, criterion_main}; -use orx_concurrent_recursive_iter::ConcurrentRecursiveIter; -use orx_parallel::*; -use rand::prelude::*; -use rand_chacha::ChaCha8Rng; -use std::{ - hint::black_box, - sync::atomic::{AtomicU64, Ordering}, -}; - -struct Node { - value: u64, - children: Vec, -} - -fn fibonacci(n: u64) -> u64 { - let n = black_box(n % 100); - let mut a = 0; - let mut b = 1; - for _ in 0..n { - let c = a + b; - a = b; - b = c; - } - a -} - -impl Node { - fn new(rng: &mut impl Rng, value: u64) -> Self { - let num_children = match value { - 0 => 0, - n => rng.random_range(0..(n as usize)), - }; - let children = (0..num_children) - .map(|i| Self::new(rng, i as u64)) - .collect(); - Self { value, children } - } - - fn seq_num_nodes(&self) -> usize { - 1 + self - .children - .iter() - .map(|node| node.seq_num_nodes()) - .sum::() - } - - fn seq_sum_fib(&self) -> u64 { - fibonacci(self.value) + self.children.iter().map(|x| x.seq_sum_fib()).sum::() - } -} - -fn seq(root: &Node) -> u64 { - root.seq_sum_fib() -} - -fn rayon(root: &Node) -> u64 { - fn process_node<'scope>(sum: &'scope AtomicU64, node: &'scope Node, s: &rayon::Scope<'scope>) { - for child in &node.children { - s.spawn(|s| { - process_node(sum, child, s); - }); - } - let val = fibonacci(node.value); - sum.fetch_add(val, Ordering::Relaxed); - } - - let sum = AtomicU64::new(0); - rayon::in_place_scope(|s| { - process_node(&sum, root, s); - }); - sum.into_inner() -} - -fn orx_lazy_unknown(root: &Node) -> u64 { - fn extend<'a, 'b>(node: &'a &'b Node) -> &'b [Node] { - &node.children - } - - [root] - .into_par_rec(extend) - // .chunk_size(1024 * 64) - .map(|x| fibonacci(x.value)) - .sum() -} - -fn orx_lazy_exact(root: &Node) -> u64 { - fn extend<'a, 'b>(node: &'a &'b Node) -> &'b [Node] { - &node.children - } - - let num_nodes = root.seq_num_nodes(); - - [root] - .into_par_rec_exact(extend, num_nodes) - .map(|x| fibonacci(x.value)) - .sum() -} - -fn orx_eager(root: &Node) -> u64 { - fn extend<'a, 'b>(node: &'a &'b Node) -> &'b [Node] { - &node.children - } - - [root] - .into_par_rec(extend) - .into_eager() - .map(|x| fibonacci(x.value)) - .sum() -} - -fn orx_static(root: &Node) -> u64 { - fn add_tasks<'a>(tasks: &mut Vec<&'a Node>, node: &'a Node) { - tasks.push(node); - for child in &node.children { - add_tasks(tasks, child); - } - } - // let mut tasks = Vec::with_capacity(root.seq_num_nodes() + 1); - let mut tasks = Vec::new(); - add_tasks(&mut tasks, root); - tasks.par().map(|x| fibonacci(x.value)).sum() -} - -fn iter(root: &Node) -> u64 { - use orx_concurrent_iter::*; - fn extend<'a, 'b>(node: &'a &'b Node) -> &'b [Node] { - &node.children - } - - let num_threads = 32; - let iter = ConcurrentRecursiveIter::new(extend, [root]); - let num_spawned = core::sync::atomic::AtomicUsize::new(0); - std::thread::scope(|s| { - let mut handles = vec![]; - for _ in 0..num_threads { - handles.push(s.spawn(|| { - // allow all threads to be spawned - _ = num_spawned.fetch_add(1, Ordering::Relaxed); - while num_spawned.load(Ordering::Relaxed) < num_threads {} - - // computation: parallel reduction - let mut thread_sum = 0; - let mut puller = iter.chunk_puller(1024); - while let Some(chunk) = puller.pull() { - thread_sum += chunk.into_iter().map(|x| fibonacci(x.value)).sum::(); - } - thread_sum - })); - } - - handles.into_iter().map(|x| x.join().unwrap()).sum() - }) -} - -fn run(c: &mut Criterion) { - let mut rng = ChaCha8Rng::seed_from_u64(42); - let root = Node::new(&mut rng, 300); - let n = &root.seq_num_nodes(); - let expected = root.seq_sum_fib(); - - let mut group = c.benchmark_group("rec_iter_sum"); - - group.bench_with_input(BenchmarkId::new("seq", n), n, |b, _| { - assert_eq!(&expected, &seq(&root)); - b.iter(|| seq(black_box(&root))) - }); - - group.bench_with_input(BenchmarkId::new("rayon", n), n, |b, _| { - assert_eq!(&expected, &rayon(&root)); - b.iter(|| rayon(black_box(&root))) - }); - - // group.bench_with_input(BenchmarkId::new("orx_lazy_unknown", n), n, |b, _| { - // assert_eq!(&expected, &orx_lazy_unknown(&root)); - // b.iter(|| orx_lazy_unknown(black_box(&root))) - // }); - - group.bench_with_input(BenchmarkId::new("orx_lazy_exact", n), n, |b, _| { - assert_eq!(&expected, &orx_lazy_exact(&root)); - b.iter(|| orx_lazy_exact(black_box(&root))) - }); - - group.bench_with_input(BenchmarkId::new("orx_eager", n), n, |b, _| { - assert_eq!(&expected, &orx_eager(&root)); - b.iter(|| orx_eager(black_box(&root))) - }); - - group.bench_with_input(BenchmarkId::new("orx_static", n), n, |b, _| { - assert_eq!(&expected, &orx_static(&root)); - b.iter(|| orx_static(black_box(&root))) - }); - - group.bench_with_input(BenchmarkId::new("iter", n), n, |b, _| { - assert_eq!(&expected, &iter(&root)); - b.iter(|| iter(black_box(&root))) - }); - - group.finish(); -} - -criterion_group!(benches, run); -criterion_main!(benches); diff --git a/examples/par_recursive_iter.rs b/examples/par_recursive_iter.rs deleted file mode 100644 index 54a3b7f7..00000000 --- a/examples/par_recursive_iter.rs +++ /dev/null @@ -1,156 +0,0 @@ -use orx_concurrent_recursive_iter::ConcurrentRecursiveIter; -use orx_parallel::*; -use rand::{Rng, SeedableRng}; -use rand_chacha::ChaCha8Rng; -use std::{hint::black_box, sync::atomic::Ordering}; - -struct Node { - value: u64, - children: Vec, -} - -fn fibonacci(n: u64) -> u64 { - let n = black_box(n % 100); - let mut a = 0; - let mut b = 1; - for _ in 0..n { - let c = a + b; - a = b; - b = c; - } - a -} - -impl Node { - fn new(rng: &mut impl Rng, value: u64) -> Self { - let num_children = match value { - 0 => 0, - n => rng.random_range(0..(n as usize)), - }; - let children = (0..num_children) - .map(|i| Self::new(rng, i as u64)) - .collect(); - Self { value, children } - } - - fn seq_num_nodes(&self) -> usize { - 1 + self - .children - .iter() - .map(|node| node.seq_num_nodes()) - .sum::() - } - - fn seq_sum_fib(&self) -> u64 { - fibonacci(self.value) + self.children.iter().map(|x| x.seq_sum_fib()).sum::() - } -} - -fn par_rec(root: &Node) -> u64 { - fn extend<'a, 'b>(node: &'a &'b Node) -> &'b [Node] { - &node.children - } - let count = root.seq_num_nodes(); - - let runner = DefaultRunner::default().with_diagnostics(); - - [root] - .into_par_rec_exact(extend, count) - .with_runner(runner) - // .with_runner(DefaultRunner::with_executor(self)) - // .chunk_size(64) - .num_threads(32) - .map(|x| fibonacci(x.value)) - .sum() -} - -fn par_rec_eager(root: &Node) -> u64 { - fn extend<'a, 'b>(node: &'a &'b Node) -> &'b [Node] { - &node.children - } - let count = root.seq_num_nodes(); - - let runner = DefaultRunner::default().with_diagnostics(); - - [root] - .into_par_rec_exact(extend, count) - .into_eager() - .with_runner(runner) - // .with_runner(DefaultRunner::with_executor(self)) - // .chunk_size(1024 * 1024) - // .num_threads(1024) - .map(|x| fibonacci(x.value)) - .sum() -} - -fn iter(root: &Node) -> u64 { - use orx_concurrent_iter::*; - fn extend<'a, 'b>(node: &'a &'b Node) -> &'b [Node] { - &node.children - } - - let num_threads = 16; - let chunk_size = 1024; - let iter = ConcurrentRecursiveIter::new(extend, [root]); - let num_spawned = core::sync::atomic::AtomicUsize::new(0); - - std::thread::scope(|s| { - let mut handles = vec![]; - for _ in 0..num_threads { - handles.push(s.spawn(|| { - // allow all threads to be spawned - _ = num_spawned.fetch_add(1, Ordering::Relaxed); - while num_spawned.load(Ordering::Relaxed) < num_threads {} - - // computation: parallel reduction - let mut thread_sum = 0; - let mut puller = iter.chunk_puller(chunk_size); - loop { - match puller.pull() { - Some(chunk) => { - thread_sum += chunk.into_iter().map(|x| fibonacci(x.value)).sum::() - } - None => { - if iter.is_completed_when_none_returned() { - break; - } - } - } - } - - thread_sum - })); - } - - handles.into_iter().map(|x| x.join().unwrap()).sum() - }) -} - -fn main() { - println!("\n\n"); - let mut rng = ChaCha8Rng::seed_from_u64(42); - let root = Node::new(&mut rng, 300); - // let root = Node::new(&mut rng, 250); - - // let par = [&root].into_par_rec(extend); - // let count = par.count(); - // assert_eq!(count, root.seq_num_nodes()); - let count = root.seq_num_nodes(); - println!("Tree contains {count} nodes"); - - // let expected = root.seq_sum_fib(); - - // let sum_fib = par_rec_eager(&root); - // // assert_eq!(sum_fib, expected); - // println!("Sum of Fibonacci of node values is {sum_fib}"); - - let sum_fib = par_rec(&root); - // assert_eq!(sum_fib, expected); - println!("Sum of Fibonacci of node values is {sum_fib}"); - - // let sum_fib = iter(&root); - // // assert_eq!(sum_fib, expected); - // println!("Sum of Fibonacci of node values is {sum_fib}"); - - println!("\n\n"); -} diff --git a/examples/rec_iter_map_collect.rs b/examples/rec_iter_map_collect.rs new file mode 100644 index 00000000..cdf2e35e --- /dev/null +++ b/examples/rec_iter_map_collect.rs @@ -0,0 +1,129 @@ +use orx_parallel::*; +use orx_split_vec::{PinnedVec, SplitVec}; +use rand::{Rng, SeedableRng}; +use rand_chacha::ChaCha8Rng; + +fn fibonacci(n: u64) -> u64 { + let mut a = 0; + let mut b = 1; + for _ in 0..n { + let c = a + b; + a = b; + b = c; + } + a +} + +struct Node { + value: Vec, + children: Vec, +} + +impl Node { + fn new(mut n: u32, rng: &mut impl Rng) -> Self { + let mut children = Vec::new(); + if n < 5 { + for _ in 0..n { + children.push(Node::new(0, rng)); + } + } else { + while n > 0 { + let n2 = rng.random_range(0..=n); + children.push(Node::new(n2, rng)); + n -= n2; + } + } + Self { + value: (0..rng.random_range(1..500)) + .map(|_| rng.random_range(0..40)) + .collect(), + children, + } + } + + fn seq_num_nodes(&self) -> usize { + 1 + self + .children + .iter() + .map(|node| node.seq_num_nodes()) + .sum::() + } + + fn seq(&self, numbers: &mut Vec) { + numbers.extend(self.value.iter().map(|x| fibonacci(*x))); + for c in &self.children { + c.seq(numbers); + } + } +} + +fn par_rec(roots: &[Node]) -> SplitVec { + fn extend<'a, 'b>(node: &'a &'b Node) -> &'b [Node] { + &node.children + } + let count: usize = roots.iter().map(|x| x.seq_num_nodes()).sum(); + + let runner = DefaultRunner::default().with_diagnostics(); + + roots + .into_par_rec_exact(extend, count) + .with_runner(runner) + .flat_map(|x| x.value.iter().map(|x| fibonacci(*x))) + .collect() +} + +fn par_rec_eager(roots: &[Node]) -> SplitVec { + fn extend<'a, 'b>(node: &'a &'b Node) -> &'b [Node] { + &node.children + } + let count: usize = roots.iter().map(|x| x.seq_num_nodes()).sum(); + + let runner = DefaultRunner::default().with_diagnostics(); + + roots + .into_par_rec_exact(extend, count) + .into_eager() + .with_runner(runner) + .flat_map(|x| x.value.iter().map(|x| fibonacci(*x))) + .collect() +} + +fn main() { + println!("\n\n"); + let mut rng = ChaCha8Rng::seed_from_u64(42); + let roots = vec![ + Node::new(5000, &mut rng), + Node::new(2000, &mut rng), + Node::new(4000, &mut rng), + ]; + + // let root = Node::new(&mut rng, 250); + + // let par = [&root].into_par_rec(extend); + // let count = par.count(); + // assert_eq!(count, root.seq_num_nodes()); + let count: usize = roots.iter().map(|x| x.seq_num_nodes()).sum(); + println!("Tree contains {count} nodes"); + + let mut expected = vec![]; + for root in &roots { + root.seq(&mut expected); + } + expected.sort(); + + println!("\n\n# par_rec"); + let mut result = par_rec(&roots); + // result.sort(); + // assert_eq!(result, expected); + + println!("\n\n# par_rec_eager"); + let mut result = par_rec_eager(&roots); + // result.sort(); + // assert_eq!(result, expected); + + // // let sum_fib = iter(&root); + // // // assert_eq!(sum_fib, expected); + // // println!("Sum of Fibonacci of node values is {sum_fib}"); + + // println!("\n\n"); +} diff --git a/examples/rec_iter_map_sum.rs b/examples/rec_iter_map_sum.rs new file mode 100644 index 00000000..4202e5a6 --- /dev/null +++ b/examples/rec_iter_map_sum.rs @@ -0,0 +1,120 @@ +use orx_parallel::*; +use rand::{Rng, SeedableRng}; +use rand_chacha::ChaCha8Rng; + +fn fibonacci(n: u64) -> u64 { + let mut a = 0; + let mut b = 1; + for _ in 0..n { + let c = a + b; + a = b; + b = c; + } + a +} + +struct Node { + value: Vec, + children: Vec, +} + +impl Node { + fn new(mut n: u32, rng: &mut impl Rng) -> Self { + let mut children = Vec::new(); + if n < 5 { + for _ in 0..n { + children.push(Node::new(0, rng)); + } + } else { + while n > 0 { + let n2 = rng.random_range(0..=n); + children.push(Node::new(n2, rng)); + n -= n2; + } + } + Self { + value: (0..rng.random_range(1..500)) + .map(|_| rng.random_range(0..40)) + .collect(), + children, + } + } + + fn seq_num_nodes(&self) -> usize { + 1 + self + .children + .iter() + .map(|node| node.seq_num_nodes()) + .sum::() + } + + fn seq_sum_fib(&self) -> u64 { + self.value.iter().map(|x| fibonacci(*x)).sum::() + + self.children.iter().map(|x| x.seq_sum_fib()).sum::() + } +} + +fn par_rec(roots: &[Node]) -> u64 { + fn extend<'a, 'b>(node: &'a &'b Node) -> &'b [Node] { + &node.children + } + let count: usize = roots.iter().map(|x| x.seq_num_nodes()).sum(); + + let runner = DefaultRunner::default().with_diagnostics(); + + roots + .into_par_rec_exact(extend, count) + .with_runner(runner) + .map(|x| x.value.iter().map(|x| fibonacci(*x)).sum::()) + .sum() +} + +fn par_rec_eager(roots: &[Node]) -> u64 { + fn extend<'a, 'b>(node: &'a &'b Node) -> &'b [Node] { + &node.children + } + let count: usize = roots.iter().map(|x| x.seq_num_nodes()).sum(); + + let runner = DefaultRunner::default().with_diagnostics(); + + roots + .into_par_rec_exact(extend, count) + .into_eager() + .with_runner(runner) + .map(|x| x.value.iter().map(|x| fibonacci(*x)).sum::()) + .sum() +} + +fn main() { + println!("\n\n"); + let mut rng = ChaCha8Rng::seed_from_u64(42); + let roots = vec![ + Node::new(500, &mut rng), + Node::new(200, &mut rng), + Node::new(400, &mut rng), + ]; + + // let root = Node::new(&mut rng, 250); + + // let par = [&root].into_par_rec(extend); + // let count = par.count(); + // assert_eq!(count, root.seq_num_nodes()); + let count: usize = roots.iter().map(|x| x.seq_num_nodes()).sum(); + println!("Tree contains {count} nodes"); + + let expected: u64 = roots.iter().map(|x| x.seq_sum_fib()).sum(); + + let sum_fib = par_rec_eager(&roots); + assert_eq!(sum_fib, expected); + println!("Sum of Fibonacci of node values is {sum_fib}"); + + let sum_fib = par_rec(&roots); + assert_eq!(sum_fib, expected); + println!("Sum of Fibonacci of node values is {sum_fib}"); + + // // let sum_fib = iter(&root); + // // // assert_eq!(sum_fib, expected); + // // println!("Sum of Fibonacci of node values is {sum_fib}"); + + // println!("\n\n"); +} From 0c263518387e384fb9e4b830f8b1405bcaa26ba0 Mon Sep 17 00:00:00 2001 From: orxfun Date: Mon, 20 Oct 2025 22:24:51 +0200 Subject: [PATCH 42/96] update bench --- benches/rec_iter_map_collect.rs | 43 +++------------------------------ 1 file changed, 4 insertions(+), 39 deletions(-) diff --git a/benches/rec_iter_map_collect.rs b/benches/rec_iter_map_collect.rs index addd0ec7..23766b14 100644 --- a/benches/rec_iter_map_collect.rs +++ b/benches/rec_iter_map_collect.rs @@ -1,4 +1,4 @@ -use criterion::{BenchmarkId, Criterion, criterion_group, criterion_main}; +use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; use orx_concurrent_bag::ConcurrentBag; use orx_parallel::*; use orx_split_vec::SplitVec; @@ -75,34 +75,6 @@ fn seq(roots: &[Node], work: usize) -> Vec { result } -fn rayon(roots: &[Node], work: usize) -> SplitVec { - use rayon::iter::*; - fn process_node<'scope>( - work: usize, - sum: &'scope AtomicU64, - node: &'scope Node, - s: &rayon::Scope<'scope>, - result: &'scope ConcurrentBag, - ) { - for child in &node.children { - s.spawn(move |s| { - process_node(work, sum, child, s, result); - }); - } - let x: Vec<_> = node.value.par_iter().map(|x| fibonacci(*x, work)).collect(); - result.extend(x); - } - - let sum = AtomicU64::new(0); - let result = ConcurrentBag::new(); - rayon::in_place_scope(|s| { - for root in roots { - process_node(work, &sum, root, s, &result); - } - }); - result.into_inner() -} - fn orx_lazy_unknown_chunk1024(roots: &[Node], work: usize) -> SplitVec { fn extend<'a, 'b>(node: &'a &'b Node) -> &'b [Node] { &node.children @@ -161,11 +133,11 @@ fn run(c: &mut Criterion) { b.iter(|| seq(&roots, *work)) }); - group.bench_with_input(BenchmarkId::new("rayon", work), work, |b, _| { - let mut result = rayon(&roots, *work).to_vec(); + group.bench_with_input(BenchmarkId::new("orx_lazy_exact", work), work, |b, _| { + let mut result = orx_lazy_exact(&roots, *work, num_nodes).to_vec(); result.sort(); assert_eq!(&expected, &result); - b.iter(|| rayon(&roots, *work)) + b.iter(|| orx_lazy_exact(&roots, *work, num_nodes)) }); group.bench_with_input( @@ -179,13 +151,6 @@ fn run(c: &mut Criterion) { }, ); - group.bench_with_input(BenchmarkId::new("orx_lazy_exact", work), work, |b, _| { - let mut result = orx_lazy_exact(&roots, *work, num_nodes).to_vec(); - result.sort(); - assert_eq!(&expected, &result); - b.iter(|| orx_lazy_exact(&roots, *work, num_nodes)) - }); - group.bench_with_input(BenchmarkId::new("orx_eager", work), work, |b, _| { let mut result = orx_eager(&roots, *work).to_vec(); result.sort(); From 2c52e9e8b4dd20ac0e53bc84338dadae0d320fa7 Mon Sep 17 00:00:00 2001 From: orxfun Date: Mon, 20 Oct 2025 22:28:54 +0200 Subject: [PATCH 43/96] upgrade dependencies --- Cargo.toml | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index f3bc98ce..84c3f191 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,19 +11,17 @@ keywords = ["parallel", "concurrency", "performance", "thread", "iterator"] categories = ["concurrency", "algorithms"] [dependencies] -orx-pinned-vec = { version = "3.20.0", default-features = false } -orx-fixed-vec = { version = "3.21.0", default-features = false } -orx-split-vec = { version = "3.21.0", default-features = false } -# orx-concurrent-iter = { version = "3.2.0", default-features = false } -orx-concurrent-iter = { path = "../orx-concurrent-iter", default-features = false } -orx-concurrent-bag = { version = "3.3.0", default-features = false } -orx-concurrent-ordered-bag = { version = "3.3.0", default-features = false } -orx-pinned-concurrent-col = { version = "2.17.0", default-features = false } +orx-pinned-vec = { version = "3.21.0", default-features = false } +orx-fixed-vec = { version = "3.22.0", default-features = false } +orx-split-vec = { version = "3.22.0", default-features = false } +orx-concurrent-iter = { version = "3.3.0", default-features = false } +orx-concurrent-bag = { version = "3.4.0", default-features = false } +orx-concurrent-ordered-bag = { version = "3.4.0", default-features = false } +orx-pinned-concurrent-col = { version = "2.18.0", default-features = false } orx-iterable = { version = "1.3.0", default-features = false } orx-priority-queue = { version = "1.7.0", default-features = false } orx-pseudo-default = { version = "2.1.0", default-features = false } -# orx-concurrent-recursive-iter = { git = "https://github.com/orxfun/orx-concurrent-recursive-iter", branch = "exact-sized-con-iter" } -orx-concurrent-recursive-iter = { path = "../orx-concurrent-recursive-iter" } +orx-concurrent-recursive-iter = { version = "1.1.0", default-features = false } # optional: generic iterator rayon = { version = "1.11.0", optional = true, default-features = false } @@ -38,10 +36,10 @@ yastl = { version = "0.1.2", optional = true, default-features = false } [dev-dependencies] chrono = "0.4.42" -clap = { version = "4.5.47", features = ["derive"] } +clap = { version = "4.5.50", features = ["derive"] } criterion = "0.7.0" orx-concurrent-option = { version = "1.5.0", default-features = false } -orx-concurrent-vec = "3.8.0" +orx-concurrent-vec = "3.10.0" rand = "0.9.2" rand_chacha = "0.9" rayon = "1.11.0" From 04266064c79dfc15d45bc11d3fdba7b519cdca2d Mon Sep 17 00:00:00 2001 From: orxfun Date: Mon, 20 Oct 2025 22:29:20 +0200 Subject: [PATCH 44/96] increment version --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 84c3f191..5bb406aa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "orx-parallel" -version = "3.3.0" +version = "3.4.0" edition = "2024" authors = ["orxfun "] readme = "README.md" From dc6399972f38edf75d80a883b954281266c53d7d Mon Sep 17 00:00:00 2001 From: orxfun Date: Mon, 20 Oct 2025 22:38:44 +0200 Subject: [PATCH 45/96] with_diagnostics is documented --- .../implementations/runner_with_pool.rs | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/src/runner/implementations/runner_with_pool.rs b/src/runner/implementations/runner_with_pool.rs index d809bbc2..22173726 100644 --- a/src/runner/implementations/runner_with_pool.rs +++ b/src/runner/implementations/runner_with_pool.rs @@ -185,6 +185,53 @@ where } } + /// Converts executor of this runner `R` into one with diagnostics; i.e.,`ParallelExecutorWithDiagnostics`. + /// + /// Note that [`ParallelExecutorWithDiagnostics`] prints the diagnostics on the stdout. Therefore, it must + /// only be used while testing a program, not in production. + /// + /// # Examples + /// + /// ``` + /// use orx_parallel::*; + /// + /// // normal execution + /// + /// let range = 0..64 * 1024; + /// let sum = range + /// .par() + /// .map(|x| x + 1) + /// .filter(|x| x.is_multiple_of(2)) + /// .sum(); + /// assert_eq!(sum, 1073774592); + /// + /// // execution with diagnostics + /// + /// let range = 0..64 * 1024; + /// let sum = range + /// .par() + /// .with_runner(DefaultRunner::default().with_diagnostics()) + /// .map(|x| x + 1) + /// .filter(|x| x.is_multiple_of(2)) + /// .sum(); + /// assert_eq!(sum, 1073774592); + /// + /// // prints diagnostics, which looks something like the following: + /// // + /// // - Number of threads used = 15 + /// // + /// // - [Thread idx]: num_calls, num_tasks, avg_chunk_size, first_chunk_sizes + /// // - [0]: 32, 16384, 512, [512, 512, 512, 512, 512, 512, 512, 512, 512, 512] + /// // - [1]: 26, 13312, 512, [512, 512, 512, 512, 512, 512, 512, 512, 512, 512] + /// // - [2]: 2, 2048, 1024, [1024, 1024] + /// // - [3]: 8, 8192, 1024, [1024, 1024, 1024, 1024, 1024, 1024, 1024, 1024] + /// // - [4]: 0, 0, 0, [] + /// // - [5]: 20, 10240, 512, [512, 512, 512, 512, 512, 512, 512, 512, 512, 512] + /// // - [6]: 9, 9216, 1024, [1024, 1024, 1024, 1024, 1024, 1024, 1024, 1024, 1024] + /// // - [7]: 6, 6144, 1024, [1024, 1024, 1024, 1024, 1024, 1024] + /// // - [8]: 0, 0, 0, [] + /// // - [9]: 0, 0, 0, [] + /// ``` pub fn with_diagnostics(self) -> RunnerWithPool> { RunnerWithPool { pool: self.pool, From 73418c1582ea29bc1ed7fa9ac2520fea17425b27 Mon Sep 17 00:00:00 2001 From: orxfun Date: Mon, 20 Oct 2025 22:42:23 +0200 Subject: [PATCH 46/96] update doc test --- .../implementations/runner_with_pool.rs | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/src/runner/implementations/runner_with_pool.rs b/src/runner/implementations/runner_with_pool.rs index 22173726..d0fe740f 100644 --- a/src/runner/implementations/runner_with_pool.rs +++ b/src/runner/implementations/runner_with_pool.rs @@ -197,7 +197,7 @@ where /// /// // normal execution /// - /// let range = 0..64 * 1024; + /// let range = 0..4096; /// let sum = range /// .par() /// .map(|x| x + 1) @@ -207,7 +207,7 @@ where /// /// // execution with diagnostics /// - /// let range = 0..64 * 1024; + /// let range = 0..4096; /// let sum = range /// .par() /// .with_runner(DefaultRunner::default().with_diagnostics()) @@ -218,19 +218,14 @@ where /// /// // prints diagnostics, which looks something like the following: /// // - /// // - Number of threads used = 15 + /// // - Number of threads used = 5 /// // /// // - [Thread idx]: num_calls, num_tasks, avg_chunk_size, first_chunk_sizes - /// // - [0]: 32, 16384, 512, [512, 512, 512, 512, 512, 512, 512, 512, 512, 512] - /// // - [1]: 26, 13312, 512, [512, 512, 512, 512, 512, 512, 512, 512, 512, 512] - /// // - [2]: 2, 2048, 1024, [1024, 1024] - /// // - [3]: 8, 8192, 1024, [1024, 1024, 1024, 1024, 1024, 1024, 1024, 1024] + /// // - [0]: 25, 1600, 64, [64, 64, 64, 64, 64, 64, 64, 64, 64, 64] + /// // - [1]: 26, 1664, 64, [64, 64, 64, 64, 64, 64, 64, 64, 64, 64] + /// // - [2]: 13, 832, 64, [64, 64, 64, 64, 64, 64, 64, 64, 64, 64] + /// // - [3]: 0, 0, 0, [] /// // - [4]: 0, 0, 0, [] - /// // - [5]: 20, 10240, 512, [512, 512, 512, 512, 512, 512, 512, 512, 512, 512] - /// // - [6]: 9, 9216, 1024, [1024, 1024, 1024, 1024, 1024, 1024, 1024, 1024, 1024] - /// // - [7]: 6, 6144, 1024, [1024, 1024, 1024, 1024, 1024, 1024] - /// // - [8]: 0, 0, 0, [] - /// // - [9]: 0, 0, 0, [] /// ``` pub fn with_diagnostics(self) -> RunnerWithPool> { RunnerWithPool { From 0037eb78a65502c11a2815d217aa2aa2b76111ad Mon Sep 17 00:00:00 2001 From: orxfun Date: Mon, 20 Oct 2025 22:43:51 +0200 Subject: [PATCH 47/96] clean up recursive examples --- examples/rec_iter_map_collect.rs | 28 ++++++++-------------------- examples/rec_iter_map_sum.rs | 14 +------------- 2 files changed, 9 insertions(+), 33 deletions(-) diff --git a/examples/rec_iter_map_collect.rs b/examples/rec_iter_map_collect.rs index cdf2e35e..c26a0a78 100644 --- a/examples/rec_iter_map_collect.rs +++ b/examples/rec_iter_map_collect.rs @@ -1,5 +1,5 @@ use orx_parallel::*; -use orx_split_vec::{PinnedVec, SplitVec}; +use orx_split_vec::SplitVec; use rand::{Rng, SeedableRng}; use rand_chacha::ChaCha8Rng; @@ -76,12 +76,11 @@ fn par_rec_eager(roots: &[Node]) -> SplitVec { fn extend<'a, 'b>(node: &'a &'b Node) -> &'b [Node] { &node.children } - let count: usize = roots.iter().map(|x| x.seq_num_nodes()).sum(); let runner = DefaultRunner::default().with_diagnostics(); roots - .into_par_rec_exact(extend, count) + .into_par_rec(extend) .into_eager() .with_runner(runner) .flat_map(|x| x.value.iter().map(|x| fibonacci(*x))) @@ -97,11 +96,6 @@ fn main() { Node::new(4000, &mut rng), ]; - // let root = Node::new(&mut rng, 250); - - // let par = [&root].into_par_rec(extend); - // let count = par.count(); - // assert_eq!(count, root.seq_num_nodes()); let count: usize = roots.iter().map(|x| x.seq_num_nodes()).sum(); println!("Tree contains {count} nodes"); @@ -112,18 +106,12 @@ fn main() { expected.sort(); println!("\n\n# par_rec"); - let mut result = par_rec(&roots); - // result.sort(); - // assert_eq!(result, expected); + let mut result = par_rec(&roots).to_vec(); + result.sort(); + assert_eq!(result, expected); println!("\n\n# par_rec_eager"); - let mut result = par_rec_eager(&roots); - // result.sort(); - // assert_eq!(result, expected); - - // // let sum_fib = iter(&root); - // // // assert_eq!(sum_fib, expected); - // // println!("Sum of Fibonacci of node values is {sum_fib}"); - - // println!("\n\n"); + let mut result = par_rec_eager(&roots).to_vec(); + result.sort(); + assert_eq!(result, expected); } diff --git a/examples/rec_iter_map_sum.rs b/examples/rec_iter_map_sum.rs index 4202e5a6..9fffc9a2 100644 --- a/examples/rec_iter_map_sum.rs +++ b/examples/rec_iter_map_sum.rs @@ -73,12 +73,11 @@ fn par_rec_eager(roots: &[Node]) -> u64 { fn extend<'a, 'b>(node: &'a &'b Node) -> &'b [Node] { &node.children } - let count: usize = roots.iter().map(|x| x.seq_num_nodes()).sum(); let runner = DefaultRunner::default().with_diagnostics(); roots - .into_par_rec_exact(extend, count) + .into_par_rec(extend) .into_eager() .with_runner(runner) .map(|x| x.value.iter().map(|x| fibonacci(*x)).sum::()) @@ -94,11 +93,6 @@ fn main() { Node::new(400, &mut rng), ]; - // let root = Node::new(&mut rng, 250); - - // let par = [&root].into_par_rec(extend); - // let count = par.count(); - // assert_eq!(count, root.seq_num_nodes()); let count: usize = roots.iter().map(|x| x.seq_num_nodes()).sum(); println!("Tree contains {count} nodes"); @@ -111,10 +105,4 @@ fn main() { let sum_fib = par_rec(&roots); assert_eq!(sum_fib, expected); println!("Sum of Fibonacci of node values is {sum_fib}"); - - // // let sum_fib = iter(&root); - // // // assert_eq!(sum_fib, expected); - // // println!("Sum of Fibonacci of node values is {sum_fib}"); - - // println!("\n\n"); } From fd39c3e4526e7aea90f89f7f4b54ae1296a56c55 Mon Sep 17 00:00:00 2001 From: orxfun Date: Mon, 20 Oct 2025 22:48:30 +0200 Subject: [PATCH 48/96] document ParallelExecutorWithDiagnostics --- .../parallel_executor.rs | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/src/executor/executor_with_diagnostics/parallel_executor.rs b/src/executor/executor_with_diagnostics/parallel_executor.rs index 700f06c3..5b0b6ab6 100644 --- a/src/executor/executor_with_diagnostics/parallel_executor.rs +++ b/src/executor/executor_with_diagnostics/parallel_executor.rs @@ -6,6 +6,57 @@ use crate::runner::{ComputationKind, NumSpawned}; use orx_concurrent_iter::ConcurrentIter; use std::num::NonZeroUsize; +/// A parallel executor which wraps another parallel executor `E` and collects diagnostics about: +/// +/// * how many threads are used for the parallel computation +/// * how many times each thread received a tasks +/// * average chunk size; i.e., average number of tasks, that each thread received +/// * and finally, explicit chunk sizes for the first task assignments. +/// +/// The diagnostics are printed on the stdout once the parallel computation is completed. +/// Therefore, this executor is suitable only for test purposed, but not for production. +/// +/// Any executor can be converted into executor with diagnostics. +/// In the example below, executor of the default runner is converted to executor with diagnostics. +/// +/// +/// # Examples +/// +/// ``` +/// use orx_parallel::*; +/// +/// // normal execution +/// +/// let range = 0..4096; +/// let sum = range +/// .par() +/// .map(|x| x + 1) +/// .filter(|x| x.is_multiple_of(2)) +/// .sum(); +/// assert_eq!(sum, 1073774592); +/// +/// // execution with diagnostics +/// +/// let range = 0..4096; +/// let sum = range +/// .par() +/// .with_runner(DefaultRunner::default().with_diagnostics()) +/// .map(|x| x + 1) +/// .filter(|x| x.is_multiple_of(2)) +/// .sum(); +/// assert_eq!(sum, 1073774592); +/// +/// // prints diagnostics, which looks something like the following: +/// // +/// // - Number of threads used = 5 +/// // +/// // - [Thread idx]: num_calls, num_tasks, avg_chunk_size, first_chunk_sizes +/// // - [0]: 25, 1600, 64, [64, 64, 64, 64, 64, 64, 64, 64, 64, 64] +/// // - [1]: 26, 1664, 64, [64, 64, 64, 64, 64, 64, 64, 64, 64, 64] +/// // - [2]: 13, 832, 64, [64, 64, 64, 64, 64, 64, 64, 64, 64, 64] +/// // - [3]: 0, 0, 0, [] +/// // - [4]: 0, 0, 0, [] +/// ``` pub struct ParallelExecutorWithDiagnostics where E: ParallelExecutor, From 0b93a997ce432fad3837a789e19632622679a6e3 Mon Sep 17 00:00:00 2001 From: orxfun Date: Mon, 20 Oct 2025 22:49:20 +0200 Subject: [PATCH 49/96] benchmark clean up --- benches/rec_iter_map_collect.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/benches/rec_iter_map_collect.rs b/benches/rec_iter_map_collect.rs index 23766b14..baffda78 100644 --- a/benches/rec_iter_map_collect.rs +++ b/benches/rec_iter_map_collect.rs @@ -1,10 +1,9 @@ -use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; -use orx_concurrent_bag::ConcurrentBag; +use criterion::{BenchmarkId, Criterion, criterion_group, criterion_main}; use orx_parallel::*; use orx_split_vec::SplitVec; use rand::prelude::*; use rand_chacha::ChaCha8Rng; -use std::{hint::black_box, sync::atomic::AtomicU64}; +use std::hint::black_box; fn fibonacci(n: u64, work: usize) -> u64 { (7..(work + 7)) From cc3bfdaffdd0034e77eb692dbe29799b9bcfa7cb Mon Sep 17 00:00:00 2001 From: orxfun Date: Mon, 20 Oct 2025 23:23:57 +0200 Subject: [PATCH 50/96] document IntoParIterRec trait --- src/iter/recursive/into_par_rec_iter.rs | 179 ++++++++++++++++++++++++ 1 file changed, 179 insertions(+) diff --git a/src/iter/recursive/into_par_rec_iter.rs b/src/iter/recursive/into_par_rec_iter.rs index b7637a39..3dd3645d 100644 --- a/src/iter/recursive/into_par_rec_iter.rs +++ b/src/iter/recursive/into_par_rec_iter.rs @@ -3,6 +3,185 @@ use orx_concurrent_recursive_iter::{ConcurrentRecursiveIter, ConcurrentRecursive // unknown size +/// Trait to convert an iterator with an extend method into a recursive parallel iterator. +/// +/// A recursive parallel iterator is defined as follows: +/// * it has an initial set of elements, +/// * each element can generate new elements, these elements are added to the parallel iterator +/// as well, +/// * computation continues until all initial elements and recursively generated elements +/// are used. +/// +/// # Examples +/// +/// The following example has some code to set up until the `# usage` line. Notice that the `Node` +/// is a recursive data structure with children being other nodes. +/// +/// We have three initial elements `roots`. +/// +/// We want to compute is the sum of Fibonacci numbers of values of all nodes descending from the +/// roots. +/// +/// The `expand` function defines the recursive expansion behavior: +/// * every process node first adds its children to the end of the iterator, +/// * then, once they are process, we will create the children of these children as well, +/// * this process will recursively continue until there is no unprocessed node left. +/// +/// This crate makes use of the [`ConcurrentRecursiveIter`] and [`ConcurrentRecursiveIterExact`] +/// for this computation and provides three ways to execute this computation in parallel. +/// +/// ## A. Recursive Iterator with Exact Length +/// +/// If we know, or if it is possible and sufficiently cheap to find out, the exact length of the iterator, +/// it is recommended to work with exact length recursive iterator. Note that the exact length of an +/// iterator is the total of all elements that will be created. This gives the parallel executor +/// opportunity to optimize the chunk sizes. +/// +/// In the following example, we first calculate this number with `roots.iter().map(|x| x.seq_num_nodes()).sum();` +/// and then use `(&roots).into_par_rec_exact(extend, count)` to create our exact length recursive parallel +/// iterator. +/// +/// Note that, once we create the recursive parallel iterator, it is just another [`ParIter`]. In other words, +/// we have access to all parallel iterator features. +/// +/// ## B. Recursive Iterator with Unknown Length +/// +/// If we cannot know or it is expensive to know the exact length of the iterator ahead of time, we can +/// still create a recursive parallel iterator. In these cases; however, it is recommended to provide +/// chunk size explicitly depending on the number of threads that will be used and any estimate on the exact +/// length. +/// +/// In the following example, we directly create the parallel iterator with `(&roots).into_par_rec(extend)` +/// without providing any length information. Then, we ask the parallel executor to pull tasks in chunks of +/// 1024 with `.chunk_size(1024)`. Recall the general rule-of-thumb on chunk size parameter: +/// * the longer each individual computation, the smaller the chunks can be, +/// * when it is too small, we might suffer from parallelization overhead, +/// * when it is too large, we might suffer from heterogeneity of tasks which might lead to imbalance of +/// load of threads, +/// * we might try to set it to a large enough value to reduce parallelization overhead without causing +/// imbalance. +/// +/// ## C. Into Eager Transformation +/// +/// Even with exact length, a recursive parallel iterator is much more dynamic than a flat parallel +/// iterator. This dynamic nature of shrinking and growing concurrently requires a greater parallelization +/// overhead. An alternative approach is to flatten the tasks and then perform the parallel computation. +/// +/// This might increase performance in certain cases; however, requires storing the flattened tasks. +/// Therefore, it fits best to situations where the input elements are not very large. +/// In the following example, for instance, elements are of type `&Node` which is a pointer size +/// which makes it suitable for this approach. +/// +/// Note that exact size will be obtained during flattening; and hence, we do not need to provide the +/// `count`. +/// +/// In the example, we create eagerly flattened parallel iterator with the +/// `(&roots).into_par_rec(extend).into_eager()` call. +/// +/// [`ParIter`]: crate::ParIter +/// +/// ## Example with all three approaches +/// +/// ``` +/// use orx_parallel::*; +/// use rand::{Rng, SeedableRng}; +/// use rand_chacha::ChaCha8Rng; +/// +/// struct Node { +/// value: Vec, +/// children: Vec, +/// } +/// +/// impl Node { +/// fn new(mut n: u32, rng: &mut impl Rng) -> Self { +/// let mut children = Vec::new(); +/// if n < 5 { +/// for _ in 0..n { +/// children.push(Node::new(0, rng)); +/// } +/// } else { +/// while n > 0 { +/// let n2 = rng.random_range(0..=n); +/// children.push(Node::new(n2, rng)); +/// n -= n2; +/// } +/// } +/// Self { +/// value: (0..rng.random_range(1..500)) +/// .map(|_| rng.random_range(0..40)) +/// .collect(), +/// children, +/// } +/// } +/// +/// fn seq_num_nodes(&self) -> usize { +/// 1 + self +/// .children +/// .iter() +/// .map(|node| node.seq_num_nodes()) +/// .sum::() +/// } +/// +/// fn seq_sum_fib(&self) -> u64 { +/// self.value.iter().map(|x| fibonacci(*x)).sum::() +/// + self.children.iter().map(|x| x.seq_sum_fib()).sum::() +/// } +/// } +/// +/// fn fibonacci(n: u64) -> u64 { +/// let mut a = 0; +/// let mut b = 1; +/// for _ in 0..n { +/// let c = a + b; +/// a = b; +/// b = c; +/// } +/// a +/// } +/// +/// // # usage +/// +/// fn extend<'a, 'b>(node: &'a &'b Node) -> &'b [Node] { +/// &node.children +/// } +/// +/// let mut rng = ChaCha8Rng::seed_from_u64(42); +/// let roots = vec![ +/// Node::new(50, &mut rng), +/// Node::new(20, &mut rng), +/// Node::new(40, &mut rng), +/// ]; +/// +/// let seq_sum: u64 = roots.iter().map(|x| x.seq_sum_fib()).sum(); +/// +/// // A. exact length, recommended when possible +/// +/// let count: usize = roots.iter().map(|x| x.seq_num_nodes()).sum(); +/// +/// let sum = (&roots) +/// .into_par_rec_exact(extend, count) +/// .map(|x| x.value.iter().map(|x| fibonacci(*x)).sum::()) +/// .sum(); +/// assert_eq!(sum, seq_sum); +/// +/// // B. guide the computation with chunk size, when length is unknown +/// +/// let sum = (&roots) +/// .into_par_rec(extend) +/// .chunk_size(1024) +/// .map(|x| x.value.iter().map(|x| fibonacci(*x)).sum::()) +/// .sum(); +/// assert_eq!(sum, seq_sum); +/// +/// // C. eagerly convert to a flat iterator +/// +/// let sum = (&roots) +/// .into_par_rec(extend) +/// .into_eager() +/// .map(|x| x.value.iter().map(|x| fibonacci(*x)).sum::()) +/// .sum(); +/// assert_eq!(sum, seq_sum); +/// ``` pub trait IntoParIterRec where Self: IntoIterator, From 50fb1e59cfc6eb6d1c6066ba78e0083a457a872e Mon Sep 17 00:00:00 2001 From: orxfun Date: Mon, 20 Oct 2025 23:24:11 +0200 Subject: [PATCH 51/96] fix doc test expected numbers --- src/executor/executor_with_diagnostics/parallel_executor.rs | 4 ++-- src/runner/implementations/runner_with_pool.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/executor/executor_with_diagnostics/parallel_executor.rs b/src/executor/executor_with_diagnostics/parallel_executor.rs index 5b0b6ab6..3d0db78d 100644 --- a/src/executor/executor_with_diagnostics/parallel_executor.rs +++ b/src/executor/executor_with_diagnostics/parallel_executor.rs @@ -33,7 +33,7 @@ use std::num::NonZeroUsize; /// .map(|x| x + 1) /// .filter(|x| x.is_multiple_of(2)) /// .sum(); -/// assert_eq!(sum, 1073774592); +/// assert_eq!(sum, 4196352); /// /// // execution with diagnostics /// @@ -44,7 +44,7 @@ use std::num::NonZeroUsize; /// .map(|x| x + 1) /// .filter(|x| x.is_multiple_of(2)) /// .sum(); -/// assert_eq!(sum, 1073774592); +/// assert_eq!(sum, 4196352); /// /// // prints diagnostics, which looks something like the following: /// // diff --git a/src/runner/implementations/runner_with_pool.rs b/src/runner/implementations/runner_with_pool.rs index d0fe740f..38cf15da 100644 --- a/src/runner/implementations/runner_with_pool.rs +++ b/src/runner/implementations/runner_with_pool.rs @@ -203,7 +203,7 @@ where /// .map(|x| x + 1) /// .filter(|x| x.is_multiple_of(2)) /// .sum(); - /// assert_eq!(sum, 1073774592); + /// assert_eq!(sum, 4196352); /// /// // execution with diagnostics /// @@ -214,7 +214,7 @@ where /// .map(|x| x + 1) /// .filter(|x| x.is_multiple_of(2)) /// .sum(); - /// assert_eq!(sum, 1073774592); + /// assert_eq!(sum, 4196352); /// /// // prints diagnostics, which looks something like the following: /// // From d33200adcde198e53aa4d85639c6f93e7dc07889 Mon Sep 17 00:00:00 2001 From: orxfun Date: Mon, 20 Oct 2025 23:32:46 +0200 Subject: [PATCH 52/96] into par iter methods documented --- src/iter/recursive/into_par_rec_iter.rs | 576 +++++++++++++++++------- 1 file changed, 401 insertions(+), 175 deletions(-) diff --git a/src/iter/recursive/into_par_rec_iter.rs b/src/iter/recursive/into_par_rec_iter.rs index 3dd3645d..161b2e27 100644 --- a/src/iter/recursive/into_par_rec_iter.rs +++ b/src/iter/recursive/into_par_rec_iter.rs @@ -3,190 +3,214 @@ use orx_concurrent_recursive_iter::{ConcurrentRecursiveIter, ConcurrentRecursive // unknown size -/// Trait to convert an iterator with an extend method into a recursive parallel iterator. +/// Trait to convert an iterator into a recursive parallel iterator together with the `extend` method. /// -/// A recursive parallel iterator is defined as follows: -/// * it has an initial set of elements, -/// * each element can generate new elements, these elements are added to the parallel iterator -/// as well, -/// * computation continues until all initial elements and recursively generated elements -/// are used. +/// Created parallel iterator is a regular parallel iterator; i.e., we have access to +/// all [`ParIter`] features. /// -/// # Examples +/// It is recursive due to the extension. The recursive parallel iterator will yield +/// * all initial elements contained in this iterator, +/// * all elements created by calling `extend` on each of the initial elements, let's call these depth-1 elements, +/// * all elements created by calling `extend` on each of the depth-1 elements, let's call these depth-2 elements, +/// * ..., and so on. /// -/// The following example has some code to set up until the `# usage` line. Notice that the `Node` -/// is a recursive data structure with children being other nodes. +/// You may read more about the [`ConcurrentRecursiveIterCore`]. /// -/// We have three initial elements `roots`. -/// -/// We want to compute is the sum of Fibonacci numbers of values of all nodes descending from the -/// roots. -/// -/// The `expand` function defines the recursive expansion behavior: -/// * every process node first adds its children to the end of the iterator, -/// * then, once they are process, we will create the children of these children as well, -/// * this process will recursively continue until there is no unprocessed node left. -/// -/// This crate makes use of the [`ConcurrentRecursiveIter`] and [`ConcurrentRecursiveIterExact`] -/// for this computation and provides three ways to execute this computation in parallel. -/// -/// ## A. Recursive Iterator with Exact Length -/// -/// If we know, or if it is possible and sufficiently cheap to find out, the exact length of the iterator, -/// it is recommended to work with exact length recursive iterator. Note that the exact length of an -/// iterator is the total of all elements that will be created. This gives the parallel executor -/// opportunity to optimize the chunk sizes. -/// -/// In the following example, we first calculate this number with `roots.iter().map(|x| x.seq_num_nodes()).sum();` -/// and then use `(&roots).into_par_rec_exact(extend, count)` to create our exact length recursive parallel -/// iterator. -/// -/// Note that, once we create the recursive parallel iterator, it is just another [`ParIter`]. In other words, -/// we have access to all parallel iterator features. -/// -/// ## B. Recursive Iterator with Unknown Length -/// -/// If we cannot know or it is expensive to know the exact length of the iterator ahead of time, we can -/// still create a recursive parallel iterator. In these cases; however, it is recommended to provide -/// chunk size explicitly depending on the number of threads that will be used and any estimate on the exact -/// length. -/// -/// In the following example, we directly create the parallel iterator with `(&roots).into_par_rec(extend)` -/// without providing any length information. Then, we ask the parallel executor to pull tasks in chunks of -/// 1024 with `.chunk_size(1024)`. Recall the general rule-of-thumb on chunk size parameter: -/// * the longer each individual computation, the smaller the chunks can be, -/// * when it is too small, we might suffer from parallelization overhead, -/// * when it is too large, we might suffer from heterogeneity of tasks which might lead to imbalance of -/// load of threads, -/// * we might try to set it to a large enough value to reduce parallelization overhead without causing -/// imbalance. -/// -/// ## C. Into Eager Transformation -/// -/// Even with exact length, a recursive parallel iterator is much more dynamic than a flat parallel -/// iterator. This dynamic nature of shrinking and growing concurrently requires a greater parallelization -/// overhead. An alternative approach is to flatten the tasks and then perform the parallel computation. -/// -/// This might increase performance in certain cases; however, requires storing the flattened tasks. -/// Therefore, it fits best to situations where the input elements are not very large. -/// In the following example, for instance, elements are of type `&Node` which is a pointer size -/// which makes it suitable for this approach. -/// -/// Note that exact size will be obtained during flattening; and hence, we do not need to provide the -/// `count`. -/// -/// In the example, we create eagerly flattened parallel iterator with the -/// `(&roots).into_par_rec(extend).into_eager()` call. -/// -/// [`ParIter`]: crate::ParIter -/// -/// ## Example with all three approaches -/// -/// ``` -/// use orx_parallel::*; -/// use rand::{Rng, SeedableRng}; -/// use rand_chacha::ChaCha8Rng; -/// -/// struct Node { -/// value: Vec, -/// children: Vec, -/// } -/// -/// impl Node { -/// fn new(mut n: u32, rng: &mut impl Rng) -> Self { -/// let mut children = Vec::new(); -/// if n < 5 { -/// for _ in 0..n { -/// children.push(Node::new(0, rng)); -/// } -/// } else { -/// while n > 0 { -/// let n2 = rng.random_range(0..=n); -/// children.push(Node::new(n2, rng)); -/// n -= n2; -/// } -/// } -/// Self { -/// value: (0..rng.random_range(1..500)) -/// .map(|_| rng.random_range(0..40)) -/// .collect(), -/// children, -/// } -/// } -/// -/// fn seq_num_nodes(&self) -> usize { -/// 1 + self -/// .children -/// .iter() -/// .map(|node| node.seq_num_nodes()) -/// .sum::() -/// } -/// -/// fn seq_sum_fib(&self) -> u64 { -/// self.value.iter().map(|x| fibonacci(*x)).sum::() -/// + self.children.iter().map(|x| x.seq_sum_fib()).sum::() -/// } -/// } -/// -/// fn fibonacci(n: u64) -> u64 { -/// let mut a = 0; -/// let mut b = 1; -/// for _ in 0..n { -/// let c = a + b; -/// a = b; -/// b = c; -/// } -/// a -/// } -/// -/// // # usage -/// -/// fn extend<'a, 'b>(node: &'a &'b Node) -> &'b [Node] { -/// &node.children -/// } -/// -/// let mut rng = ChaCha8Rng::seed_from_u64(42); -/// let roots = vec![ -/// Node::new(50, &mut rng), -/// Node::new(20, &mut rng), -/// Node::new(40, &mut rng), -/// ]; -/// -/// let seq_sum: u64 = roots.iter().map(|x| x.seq_sum_fib()).sum(); -/// -/// // A. exact length, recommended when possible -/// -/// let count: usize = roots.iter().map(|x| x.seq_num_nodes()).sum(); -/// -/// let sum = (&roots) -/// .into_par_rec_exact(extend, count) -/// .map(|x| x.value.iter().map(|x| fibonacci(*x)).sum::()) -/// .sum(); -/// assert_eq!(sum, seq_sum); -/// -/// // B. guide the computation with chunk size, when length is unknown -/// -/// let sum = (&roots) -/// .into_par_rec(extend) -/// .chunk_size(1024) -/// .map(|x| x.value.iter().map(|x| fibonacci(*x)).sum::()) -/// .sum(); -/// assert_eq!(sum, seq_sum); -/// -/// // C. eagerly convert to a flat iterator -/// -/// let sum = (&roots) -/// .into_par_rec(extend) -/// .into_eager() -/// .map(|x| x.value.iter().map(|x| fibonacci(*x)).sum::()) -/// .sum(); -/// assert_eq!(sum, seq_sum); -/// ``` +/// See also [`IntoParIterRecExact`] pub trait IntoParIterRec where Self: IntoIterator, Self::Item: Send, { + /// Converts this iterator into a recursive parallel iterator together with the `extend` + /// method. + /// + /// Created parallel iterator is a regular parallel iterator; i.e., we have access to + /// all [`ParIter`] features. + /// + /// It is recursive due to the extension. The recursive parallel iterator will yield + /// * all initial elements contained in this iterator, + /// * all elements created by calling `extend` on each of the initial elements, let's call these depth-1 elements, + /// * all elements created by calling `extend` on each of the depth-1 elements, let's call these depth-2 elements, + /// * ..., and so on. + /// + /// You may read more about the [`ConcurrentRecursiveIterCore`]. + /// + /// See also [`IntoParIterRecExact`] + /// + /// # Examples + /// + /// The following example has some code to set up until the `# usage` line. Notice that the `Node` + /// is a recursive data structure with children being other nodes. + /// + /// We have three initial elements `roots`. + /// + /// We want to compute is the sum of Fibonacci numbers of values of all nodes descending from the + /// roots. + /// + /// The `expand` function defines the recursive expansion behavior: + /// * every process node first adds its children to the end of the iterator, + /// * then, once they are process, we will create the children of these children as well, + /// * this process will recursively continue until there is no unprocessed node left. + /// + /// This crate makes use of the [`ConcurrentRecursiveIter`] and [`ConcurrentRecursiveIterExact`] + /// for this computation and provides three ways to execute this computation in parallel. + /// + /// ## A. Recursive Iterator with Exact Length + /// + /// If we know, or if it is possible and sufficiently cheap to find out, the exact length of the iterator, + /// it is recommended to work with exact length recursive iterator. Note that the exact length of an + /// iterator is the total of all elements that will be created. This gives the parallel executor + /// opportunity to optimize the chunk sizes. + /// + /// In the following example, we first calculate this number with `roots.iter().map(|x| x.seq_num_nodes()).sum();` + /// and then use `(&roots).into_par_rec_exact(extend, count)` to create our exact length recursive parallel + /// iterator. + /// + /// Note that, once we create the recursive parallel iterator, it is just another [`ParIter`]. In other words, + /// we have access to all parallel iterator features. + /// + /// ## B. Recursive Iterator with Unknown Length + /// + /// If we cannot know or it is expensive to know the exact length of the iterator ahead of time, we can + /// still create a recursive parallel iterator. In these cases; however, it is recommended to provide + /// chunk size explicitly depending on the number of threads that will be used and any estimate on the exact + /// length. + /// + /// In the following example, we directly create the parallel iterator with `(&roots).into_par_rec(extend)` + /// without providing any length information. Then, we ask the parallel executor to pull tasks in chunks of + /// 1024 with `.chunk_size(1024)`. Recall the general rule-of-thumb on chunk size parameter: + /// * the longer each individual computation, the smaller the chunks can be, + /// * when it is too small, we might suffer from parallelization overhead, + /// * when it is too large, we might suffer from heterogeneity of tasks which might lead to imbalance of + /// load of threads, + /// * we might try to set it to a large enough value to reduce parallelization overhead without causing + /// imbalance. + /// + /// ## C. Into Eager Transformation + /// + /// Even with exact length, a recursive parallel iterator is much more dynamic than a flat parallel + /// iterator. This dynamic nature of shrinking and growing concurrently requires a greater parallelization + /// overhead. An alternative approach is to flatten the tasks and then perform the parallel computation. + /// + /// This might increase performance in certain cases; however, requires storing the flattened tasks. + /// Therefore, it fits best to situations where the input elements are not very large. + /// In the following example, for instance, elements are of type `&Node` which is a pointer size + /// which makes it suitable for this approach. + /// + /// Note that exact size will be obtained during flattening; and hence, we do not need to provide the + /// `count`. + /// + /// In the example, we create eagerly flattened parallel iterator with the + /// `(&roots).into_par_rec(extend).into_eager()` call. + /// + /// [`ParIter`]: crate::ParIter + /// [`ConcurrentRecursiveIterCore`]: orx_concurrent_recursive_iter::ConcurrentRecursiveIterCore + /// + /// ## Example with all three approaches + /// + /// ``` + /// use orx_parallel::*; + /// use rand::{Rng, SeedableRng}; + /// use rand_chacha::ChaCha8Rng; + /// + /// struct Node { + /// value: Vec, + /// children: Vec, + /// } + /// + /// impl Node { + /// fn new(mut n: u32, rng: &mut impl Rng) -> Self { + /// let mut children = Vec::new(); + /// if n < 5 { + /// for _ in 0..n { + /// children.push(Node::new(0, rng)); + /// } + /// } else { + /// while n > 0 { + /// let n2 = rng.random_range(0..=n); + /// children.push(Node::new(n2, rng)); + /// n -= n2; + /// } + /// } + /// Self { + /// value: (0..rng.random_range(1..500)) + /// .map(|_| rng.random_range(0..40)) + /// .collect(), + /// children, + /// } + /// } + /// + /// fn seq_num_nodes(&self) -> usize { + /// 1 + self + /// .children + /// .iter() + /// .map(|node| node.seq_num_nodes()) + /// .sum::() + /// } + /// + /// fn seq_sum_fib(&self) -> u64 { + /// self.value.iter().map(|x| fibonacci(*x)).sum::() + /// + self.children.iter().map(|x| x.seq_sum_fib()).sum::() + /// } + /// } + /// + /// fn fibonacci(n: u64) -> u64 { + /// let mut a = 0; + /// let mut b = 1; + /// for _ in 0..n { + /// let c = a + b; + /// a = b; + /// b = c; + /// } + /// a + /// } + /// + /// // # usage + /// + /// // this defines how the iterator must extend: + /// // each node drawn from the iterator adds its children to the end of the iterator + /// fn extend<'a, 'b>(node: &'a &'b Node) -> &'b [Node] { + /// &node.children + /// } + /// + /// let mut rng = ChaCha8Rng::seed_from_u64(42); + /// let roots = vec![ + /// Node::new(50, &mut rng), + /// Node::new(20, &mut rng), + /// Node::new(40, &mut rng), + /// ]; + /// + /// let seq_sum: u64 = roots.iter().map(|x| x.seq_sum_fib()).sum(); + /// + /// // A. exact length, recommended when possible + /// + /// let count: usize = roots.iter().map(|x| x.seq_num_nodes()).sum(); + /// + /// let sum = (&roots) + /// .into_par_rec_exact(extend, count) + /// .map(|x| x.value.iter().map(|x| fibonacci(*x)).sum::()) + /// .sum(); + /// assert_eq!(sum, seq_sum); + /// + /// // B. guide the computation with chunk size, when length is unknown + /// + /// let sum = (&roots) + /// .into_par_rec(extend) + /// .chunk_size(1024) + /// .map(|x| x.value.iter().map(|x| fibonacci(*x)).sum::()) + /// .sum(); + /// assert_eq!(sum, seq_sum); + /// + /// // C. eagerly convert to a flat iterator + /// + /// let sum = (&roots) + /// .into_par_rec(extend) + /// .into_eager() + /// .map(|x| x.value.iter().map(|x| fibonacci(*x)).sum::()) + /// .sum(); + /// assert_eq!(sum, seq_sum); + /// ``` fn into_par_rec( self, extend: E, @@ -218,11 +242,213 @@ where // exact size +/// Trait to convert an iterator into an exact-sized recursive parallel iterator together with the `extend` method +/// and `exact_len`, +/// +/// Created parallel iterator is a regular parallel iterator; i.e., we have access to +/// all [`ParIter`] features. +/// +/// It is recursive due to the extension. The recursive parallel iterator will yield +/// * all initial elements contained in this iterator, +/// * all elements created by calling `extend` on each of the initial elements, let's call these depth-1 elements, +/// * all elements created by calling `extend` on each of the depth-1 elements, let's call these depth-2 elements, +/// * ..., and so on. +/// +/// You may read more about the [`ConcurrentRecursiveIterCore`]. +/// +/// See also [`IntoParIterRec`] pub trait IntoParIterRecExact where Self: IntoIterator, Self::Item: Send, { + /// Converts this iterator into a recursive parallel iterator together with the `extend` method and `exact_len`. + /// + /// Created parallel iterator is a regular parallel iterator; i.e., we have access to all [`ParIter`] features. + /// + /// It is recursive due to the extension. The recursive parallel iterator will yield + /// * all initial elements contained in this iterator, + /// * all elements created by calling `extend` on each of the initial elements, let's call these depth-1 elements, + /// * all elements created by calling `extend` on each of the depth-1 elements, let's call these depth-2 elements, + /// * ..., and so on. + /// + /// You may read more about the [`ConcurrentRecursiveIterCore`]. + /// + /// See also [`IntoParIterRec`] + /// + /// # Examples + /// + /// The following example has some code to set up until the `# usage` line. Notice that the `Node` + /// is a recursive data structure with children being other nodes. + /// + /// We have three initial elements `roots`. + /// + /// We want to compute is the sum of Fibonacci numbers of values of all nodes descending from the + /// roots. + /// + /// The `expand` function defines the recursive expansion behavior: + /// * every process node first adds its children to the end of the iterator, + /// * then, once they are process, we will create the children of these children as well, + /// * this process will recursively continue until there is no unprocessed node left. + /// + /// This crate makes use of the [`ConcurrentRecursiveIter`] and [`ConcurrentRecursiveIterExact`] + /// for this computation and provides three ways to execute this computation in parallel. + /// + /// ## A. Recursive Iterator with Exact Length + /// + /// If we know, or if it is possible and sufficiently cheap to find out, the exact length of the iterator, + /// it is recommended to work with exact length recursive iterator. Note that the exact length of an + /// iterator is the total of all elements that will be created. This gives the parallel executor + /// opportunity to optimize the chunk sizes. + /// + /// In the following example, we first calculate this number with `roots.iter().map(|x| x.seq_num_nodes()).sum();` + /// and then use `(&roots).into_par_rec_exact(extend, count)` to create our exact length recursive parallel + /// iterator. + /// + /// Note that, once we create the recursive parallel iterator, it is just another [`ParIter`]. In other words, + /// we have access to all parallel iterator features. + /// + /// ## B. Recursive Iterator with Unknown Length + /// + /// If we cannot know or it is expensive to know the exact length of the iterator ahead of time, we can + /// still create a recursive parallel iterator. In these cases; however, it is recommended to provide + /// chunk size explicitly depending on the number of threads that will be used and any estimate on the exact + /// length. + /// + /// In the following example, we directly create the parallel iterator with `(&roots).into_par_rec(extend)` + /// without providing any length information. Then, we ask the parallel executor to pull tasks in chunks of + /// 1024 with `.chunk_size(1024)`. Recall the general rule-of-thumb on chunk size parameter: + /// * the longer each individual computation, the smaller the chunks can be, + /// * when it is too small, we might suffer from parallelization overhead, + /// * when it is too large, we might suffer from heterogeneity of tasks which might lead to imbalance of + /// load of threads, + /// * we might try to set it to a large enough value to reduce parallelization overhead without causing + /// imbalance. + /// + /// ## C. Into Eager Transformation + /// + /// Even with exact length, a recursive parallel iterator is much more dynamic than a flat parallel + /// iterator. This dynamic nature of shrinking and growing concurrently requires a greater parallelization + /// overhead. An alternative approach is to flatten the tasks and then perform the parallel computation. + /// + /// This might increase performance in certain cases; however, requires storing the flattened tasks. + /// Therefore, it fits best to situations where the input elements are not very large. + /// In the following example, for instance, elements are of type `&Node` which is a pointer size + /// which makes it suitable for this approach. + /// + /// Note that exact size will be obtained during flattening; and hence, we do not need to provide the + /// `count`. + /// + /// In the example, we create eagerly flattened parallel iterator with the + /// `(&roots).into_par_rec(extend).into_eager()` call. + /// + /// [`ParIter`]: crate::ParIter + /// [`ConcurrentRecursiveIterCore`]: orx_concurrent_recursive_iter::ConcurrentRecursiveIterCore + /// + /// ## Example with all three approaches + /// + /// ``` + /// use orx_parallel::*; + /// use rand::{Rng, SeedableRng}; + /// use rand_chacha::ChaCha8Rng; + /// + /// struct Node { + /// value: Vec, + /// children: Vec, + /// } + /// + /// impl Node { + /// fn new(mut n: u32, rng: &mut impl Rng) -> Self { + /// let mut children = Vec::new(); + /// if n < 5 { + /// for _ in 0..n { + /// children.push(Node::new(0, rng)); + /// } + /// } else { + /// while n > 0 { + /// let n2 = rng.random_range(0..=n); + /// children.push(Node::new(n2, rng)); + /// n -= n2; + /// } + /// } + /// Self { + /// value: (0..rng.random_range(1..500)) + /// .map(|_| rng.random_range(0..40)) + /// .collect(), + /// children, + /// } + /// } + /// + /// fn seq_num_nodes(&self) -> usize { + /// 1 + self + /// .children + /// .iter() + /// .map(|node| node.seq_num_nodes()) + /// .sum::() + /// } + /// + /// fn seq_sum_fib(&self) -> u64 { + /// self.value.iter().map(|x| fibonacci(*x)).sum::() + /// + self.children.iter().map(|x| x.seq_sum_fib()).sum::() + /// } + /// } + /// + /// fn fibonacci(n: u64) -> u64 { + /// let mut a = 0; + /// let mut b = 1; + /// for _ in 0..n { + /// let c = a + b; + /// a = b; + /// b = c; + /// } + /// a + /// } + /// + /// // # usage + /// + /// // this defines how the iterator must extend: + /// // each node drawn from the iterator adds its children to the end of the iterator + /// fn extend<'a, 'b>(node: &'a &'b Node) -> &'b [Node] { + /// &node.children + /// } + /// + /// let mut rng = ChaCha8Rng::seed_from_u64(42); + /// let roots = vec![ + /// Node::new(50, &mut rng), + /// Node::new(20, &mut rng), + /// Node::new(40, &mut rng), + /// ]; + /// + /// let seq_sum: u64 = roots.iter().map(|x| x.seq_sum_fib()).sum(); + /// + /// // A. exact length, recommended when possible + /// + /// let count: usize = roots.iter().map(|x| x.seq_num_nodes()).sum(); + /// + /// let sum = (&roots) + /// .into_par_rec_exact(extend, count) + /// .map(|x| x.value.iter().map(|x| fibonacci(*x)).sum::()) + /// .sum(); + /// assert_eq!(sum, seq_sum); + /// + /// // B. guide the computation with chunk size, when length is unknown + /// + /// let sum = (&roots) + /// .into_par_rec(extend) + /// .chunk_size(1024) + /// .map(|x| x.value.iter().map(|x| fibonacci(*x)).sum::()) + /// .sum(); + /// assert_eq!(sum, seq_sum); + /// + /// // C. eagerly convert to a flat iterator + /// + /// let sum = (&roots) + /// .into_par_rec(extend) + /// .into_eager() + /// .map(|x| x.value.iter().map(|x| fibonacci(*x)).sum::()) + /// .sum(); + /// assert_eq!(sum, seq_sum); + /// ``` fn into_par_rec_exact( self, extend: E, From 9338dfd66a33dbaffad4555571faddd165303906 Mon Sep 17 00:00:00 2001 From: orxfun Date: Mon, 20 Oct 2025 23:38:39 +0200 Subject: [PATCH 53/96] document into_eager --- src/iter/recursive/into_par_rec_iter.rs | 6 +++-- src/iter/recursive/rec_per_iter.rs | 36 +++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/src/iter/recursive/into_par_rec_iter.rs b/src/iter/recursive/into_par_rec_iter.rs index 161b2e27..67ff9cc6 100644 --- a/src/iter/recursive/into_par_rec_iter.rs +++ b/src/iter/recursive/into_par_rec_iter.rs @@ -91,7 +91,8 @@ where /// /// Even with exact length, a recursive parallel iterator is much more dynamic than a flat parallel /// iterator. This dynamic nature of shrinking and growing concurrently requires a greater parallelization - /// overhead. An alternative approach is to flatten the tasks and then perform the parallel computation. + /// overhead. An alternative approach is to eagerly discover all tasks and then perform the parallel + /// computation over the flattened input of tasks. /// /// This might increase performance in certain cases; however, requires storing the flattened tasks. /// Therefore, it fits best to situations where the input elements are not very large. @@ -329,7 +330,8 @@ where /// /// Even with exact length, a recursive parallel iterator is much more dynamic than a flat parallel /// iterator. This dynamic nature of shrinking and growing concurrently requires a greater parallelization - /// overhead. An alternative approach is to flatten the tasks and then perform the parallel computation. + /// overhead. An alternative approach is to eagerly discover all tasks and then perform the parallel + /// computation over the flattened input of tasks. /// /// This might increase performance in certain cases; however, requires storing the flattened tasks. /// Therefore, it fits best to situations where the input elements are not very large. diff --git a/src/iter/recursive/rec_per_iter.rs b/src/iter/recursive/rec_per_iter.rs index 09c224c1..adfd5cbc 100644 --- a/src/iter/recursive/rec_per_iter.rs +++ b/src/iter/recursive/rec_per_iter.rs @@ -17,6 +17,18 @@ where E: Fn(&I::Item) -> I + Sync, R: ParallelRunner, { + /// Even with exact length, a recursive parallel iterator is much more dynamic than a flat parallel + /// iterator. This dynamic nature of shrinking and growing concurrently requires a greater parallelization + /// overhead. An alternative approach is to eagerly discover all tasks and then perform the parallel + /// computation over the flattened input of tasks. + /// + /// This might increase performance in certain cases; however, requires storing the flattened tasks. + /// Therefore, it fits best to situations where the input elements are not very large. + /// + /// See [`into_par_rec`] and [`into_par_rec_exact`] for examples. + /// + /// [`into_par_rec`]: crate::IntoParIterRec::into_par_rec + /// [`into_par_rec_exact`]: crate::IntoParIterRecExact::into_par_rec_exact pub fn into_eager(self) -> Par, R> { let (orchestrator, params, iter) = self.destruct(); let items = collect_items(iter); @@ -35,6 +47,18 @@ where R: ParallelRunner, M1: Fn(I::Item) -> O + Sync, { + /// Even with exact length, a recursive parallel iterator is much more dynamic than a flat parallel + /// iterator. This dynamic nature of shrinking and growing concurrently requires a greater parallelization + /// overhead. An alternative approach is to eagerly discover all tasks and then perform the parallel + /// computation over the flattened input of tasks. + /// + /// This might increase performance in certain cases; however, requires storing the flattened tasks. + /// Therefore, it fits best to situations where the input elements are not very large. + /// + /// See [`into_par_rec`] and [`into_par_rec_exact`] for examples. + /// + /// [`into_par_rec`]: crate::IntoParIterRec::into_par_rec + /// [`into_par_rec_exact`]: crate::IntoParIterRecExact::into_par_rec_exact pub fn into_eager(self) -> ParMap, O, M1, R> { let (orchestrator, params, iter, map1) = self.destruct(); let items = collect_items(iter); @@ -54,6 +78,18 @@ where X1: Fn(I::Item) -> Vo + Sync, Vo: TransformableValues, { + /// Even with exact length, a recursive parallel iterator is much more dynamic than a flat parallel + /// iterator. This dynamic nature of shrinking and growing concurrently requires a greater parallelization + /// overhead. An alternative approach is to eagerly discover all tasks and then perform the parallel + /// computation over the flattened input of tasks. + /// + /// This might increase performance in certain cases; however, requires storing the flattened tasks. + /// Therefore, it fits best to situations where the input elements are not very large. + /// + /// See [`into_par_rec`] and [`into_par_rec_exact`] for examples. + /// + /// [`into_par_rec`]: crate::IntoParIterRec::into_par_rec + /// [`into_par_rec_exact`]: crate::IntoParIterRecExact::into_par_rec_exact pub fn into_eager(self) -> ParXap, Vo, X1, R> { let (orchestrator, params, iter, xap1) = self.destruct(); let items = collect_items(iter); From 39d621459fd5f17fe559c03a6b3cc2f7fee2870f Mon Sep 17 00:00:00 2001 From: orxfun Date: Mon, 20 Oct 2025 23:42:33 +0200 Subject: [PATCH 54/96] update bench --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 5bb406aa..97ac89fc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,7 +46,7 @@ rayon = "1.11.0" test-case = "3.3.1" [[bench]] -name = "rec_iter_map_collect" +name = "find_iter_into_par" harness = false [package.metadata.docs.rs] From 85c37f4a110191a11a692bf1f4c9bb498158eb31 Mon Sep 17 00:00:00 2001 From: orxfun Date: Mon, 20 Oct 2025 23:47:27 +0200 Subject: [PATCH 55/96] add trait bounds tests for pinned vectors --- tests/trait_bounds.rs | 92 +++++++++++++++++++++---------------------- 1 file changed, 46 insertions(+), 46 deletions(-) diff --git a/tests/trait_bounds.rs b/tests/trait_bounds.rs index 81a45b1a..5ac87c42 100644 --- a/tests/trait_bounds.rs +++ b/tests/trait_bounds.rs @@ -1,53 +1,53 @@ -// use orx_fixed_vec::FixedVec; -// use orx_split_vec::SplitVec; -// use std::collections::VecDeque; +use orx_fixed_vec::FixedVec; +use orx_split_vec::SplitVec; +use std::collections::VecDeque; -// #[test] -// fn trait_bounds_parallelizable() { -// use orx_parallel::Parallelizable; -// fn fun(source: impl Parallelizable) { -// let _iter = source.par(); -// } +#[test] +fn trait_bounds_parallelizable() { + use orx_parallel::Parallelizable; + fn fun(source: impl Parallelizable) { + let _iter = source.par(); + } -// fun(vec![1, 2, 3].as_slice()); -// fun(&vec![1, 2, 3]); -// fun(&VecDeque::::new()); -// fun(0..9); -// fun(&FixedVec::::new(3)); -// fun(&SplitVec::::new()); -// } + fun(vec![1, 2, 3].as_slice()); + fun(&vec![1, 2, 3]); + fun(&VecDeque::::new()); + fun(0..9); + fun(&FixedVec::::new(3)); + fun(&SplitVec::::new()); +} -// #[test] -// fn trait_bounds_parallelizable_collection() { -// use orx_parallel::ParallelizableCollection; -// fn fun(source: impl ParallelizableCollection) { -// let _iter = source.par(); -// } +#[test] +fn trait_bounds_parallelizable_collection() { + use orx_parallel::ParallelizableCollection; + fn fun(source: impl ParallelizableCollection) { + let _iter = source.par(); + } -// fun(vec![1, 2, 3]); -// fun(VecDeque::::new()); -// fun(FixedVec::::new(3)); -// fun(SplitVec::::new()); -// } + fun(vec![1, 2, 3]); + fun(VecDeque::::new()); + fun(FixedVec::::new(3)); + fun(SplitVec::::new()); +} -// #[test] -// fn trait_bounds_into_par_iter() { -// use orx_parallel::IntoParIter; -// fn fun(source: impl IntoParIter) { -// let _iter = source.into_par(); -// } +#[test] +fn trait_bounds_into_par_iter() { + use orx_parallel::IntoParIter; + fn fun(source: impl IntoParIter) { + let _iter = source.into_par(); + } -// // owned -// fun(vec![1, 2, 3]); -// fun(VecDeque::::new()); -// fun(FixedVec::::new(3)); -// fun(SplitVec::::new()); + // owned + fun(vec![1, 2, 3]); + fun(VecDeque::::new()); + fun(FixedVec::::new(3)); + fun(SplitVec::::new()); -// // ref -// fun(vec![1, 2, 3].as_slice()); -// fun(&vec![1, 2, 3]); -// fun(&VecDeque::::new()); -// fun(0..9); -// fun(&FixedVec::::new(3)); -// fun(&SplitVec::::new()); -// } + // ref + fun(vec![1, 2, 3].as_slice()); + fun(&vec![1, 2, 3]); + fun(&VecDeque::::new()); + fun(0..9); + fun(&FixedVec::::new(3)); + fun(&SplitVec::::new()); +} From 2b55146906f9879419f4c2e41875d7f5f836d2a7 Mon Sep 17 00:00:00 2001 From: orxfun Date: Fri, 24 Oct 2025 11:22:45 +0200 Subject: [PATCH 56/96] revise benches --- Cargo.toml | 2 +- benches/rec_iter_map_sum.rs | 23 +++++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 97ac89fc..b1e82fd1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,7 +46,7 @@ rayon = "1.11.0" test-case = "3.3.1" [[bench]] -name = "find_iter_into_par" +name = "rec_iter_map_sum" harness = false [package.metadata.docs.rs] diff --git a/benches/rec_iter_map_sum.rs b/benches/rec_iter_map_sum.rs index a846eaad..2d4ba333 100644 --- a/benches/rec_iter_map_sum.rs +++ b/benches/rec_iter_map_sum.rs @@ -123,6 +123,17 @@ fn orx_lazy_exact(roots: &[Node], work: usize, num_nodes: usize) -> u64 { .sum() } +fn orx_lazy_exact_flat_map(roots: &[Node], work: usize, num_nodes: usize) -> u64 { + fn extend<'a, 'b>(node: &'a &'b Node) -> &'b [Node] { + &node.children + } + + roots + .into_par_rec_exact(extend, num_nodes) + .flat_map(|x| x.value.iter().map(|x| fibonacci(*x, work))) + .sum() +} + fn orx_eager(roots: &[Node], work: usize) -> u64 { fn extend<'a, 'b>(node: &'a &'b Node) -> &'b [Node] { &node.children @@ -174,6 +185,18 @@ fn run(c: &mut Criterion) { b.iter(|| orx_lazy_exact(&roots, *work, num_nodes)) }); + group.bench_with_input( + BenchmarkId::new("orx_lazy_exact_flat_map", work), + work, + |b, _| { + assert_eq!( + &expected, + &orx_lazy_exact_flat_map(&roots, *work, num_nodes) + ); + b.iter(|| orx_lazy_exact_flat_map(&roots, *work, num_nodes)) + }, + ); + group.bench_with_input(BenchmarkId::new("orx_eager", work), work, |b, _| { assert_eq!(&expected, &orx_eager(&roots, *work)); b.iter(|| orx_eager(&roots, *work)) From 8f0dd1f19a64e80c5c23e32e6de71228a38226a8 Mon Sep 17 00:00:00 2001 From: orxfun Date: Fri, 24 Oct 2025 11:34:06 +0200 Subject: [PATCH 57/96] fix into_eager implementations --- Cargo.toml | 3 +- src/iter/mod.rs | 2 +- src/iter/recursive/mod.rs | 4 +-- src/iter/recursive/rec_per_iter.rs | 50 ++++++++++++------------------ src/lib.rs | 2 +- 5 files changed, 25 insertions(+), 36 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index b1e82fd1..eebd20c2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,8 @@ orx-pinned-concurrent-col = { version = "2.18.0", default-features = false } orx-iterable = { version = "1.3.0", default-features = false } orx-priority-queue = { version = "1.7.0", default-features = false } orx-pseudo-default = { version = "2.1.0", default-features = false } -orx-concurrent-recursive-iter = { version = "1.1.0", default-features = false } +# orx-concurrent-recursive-iter = { version = "1.1.0", default-features = false } +orx-concurrent-recursive-iter = { path = "../orx-concurrent-recursive-iter", default-features = false } # optional: generic iterator rayon = { version = "1.11.0", optional = true, default-features = false } diff --git a/src/iter/mod.rs b/src/iter/mod.rs index b5392a95..8f37a7d5 100644 --- a/src/iter/mod.rs +++ b/src/iter/mod.rs @@ -1,5 +1,5 @@ mod recursive; mod special_iterators; -pub use recursive::{IntoParIterRec, IntoParIterRecExact}; +// pub use recursive::{IntoParIterRec, IntoParIterRecExact}; pub use special_iterators::{ParEmpty, empty}; diff --git a/src/iter/recursive/mod.rs b/src/iter/recursive/mod.rs index 2b96eac5..4d2f28b3 100644 --- a/src/iter/recursive/mod.rs +++ b/src/iter/recursive/mod.rs @@ -1,4 +1,4 @@ -mod into_par_rec_iter; +// mod into_par_rec_iter; mod rec_per_iter; -pub use into_par_rec_iter::{IntoParIterRec, IntoParIterRecExact}; +// pub use into_par_rec_iter::{IntoParIterRec, IntoParIterRecExact}; diff --git a/src/iter/recursive/rec_per_iter.rs b/src/iter/recursive/rec_per_iter.rs index adfd5cbc..4153ae13 100644 --- a/src/iter/recursive/rec_per_iter.rs +++ b/src/iter/recursive/rec_per_iter.rs @@ -4,17 +4,14 @@ use crate::{ generic_values::{TransformableValues, runner_results::Infallible}, }; use orx_concurrent_iter::{ConcurrentIter, IntoConcurrentIter, implementations::ConIterVec}; -use orx_concurrent_recursive_iter::{ConcurrentRecursiveIterCore, Size}; +use orx_concurrent_recursive_iter::{ConcurrentRecursiveIter, Queue}; -type Rec = ConcurrentRecursiveIterCore::Item, E, I>; +type Rec = ConcurrentRecursiveIter; -impl Par, R> +impl Par, R> where - S: Size, - I: IntoIterator, - I::IntoIter: ExactSizeIterator, - I::Item: Send, - E: Fn(&I::Item) -> I + Sync, + T: Send, + E: Fn(&T, &Queue) + Sync, R: ParallelRunner, { /// Even with exact length, a recursive parallel iterator is much more dynamic than a flat parallel @@ -29,7 +26,7 @@ where /// /// [`into_par_rec`]: crate::IntoParIterRec::into_par_rec /// [`into_par_rec_exact`]: crate::IntoParIterRecExact::into_par_rec_exact - pub fn into_eager(self) -> Par, R> { + pub fn into_eager(self) -> Par, R> { let (orchestrator, params, iter) = self.destruct(); let items = collect_items(iter); let iter = items.into_con_iter(); @@ -37,15 +34,12 @@ where } } -impl ParMap, O, M1, R> +impl ParMap, O, M1, R> where - S: Size, - I: IntoIterator, - I::IntoIter: ExactSizeIterator, - I::Item: Send, - E: Fn(&I::Item) -> I + Sync, + T: Send, + E: Fn(&T, &Queue) + Sync, R: ParallelRunner, - M1: Fn(I::Item) -> O + Sync, + M1: Fn(T) -> O + Sync, { /// Even with exact length, a recursive parallel iterator is much more dynamic than a flat parallel /// iterator. This dynamic nature of shrinking and growing concurrently requires a greater parallelization @@ -59,7 +53,7 @@ where /// /// [`into_par_rec`]: crate::IntoParIterRec::into_par_rec /// [`into_par_rec_exact`]: crate::IntoParIterRecExact::into_par_rec_exact - pub fn into_eager(self) -> ParMap, O, M1, R> { + pub fn into_eager(self) -> ParMap, O, M1, R> { let (orchestrator, params, iter, map1) = self.destruct(); let items = collect_items(iter); let iter = items.into_con_iter(); @@ -67,15 +61,12 @@ where } } -impl ParXap, Vo, X1, R> +impl ParXap, Vo, X1, R> where - S: Size, - I: IntoIterator, - I::IntoIter: ExactSizeIterator, - I::Item: Send, - E: Fn(&I::Item) -> I + Sync, + T: Send, + E: Fn(&T, &Queue) + Sync, R: ParallelRunner, - X1: Fn(I::Item) -> Vo + Sync, + X1: Fn(T) -> Vo + Sync, Vo: TransformableValues, { /// Even with exact length, a recursive parallel iterator is much more dynamic than a flat parallel @@ -90,7 +81,7 @@ where /// /// [`into_par_rec`]: crate::IntoParIterRec::into_par_rec /// [`into_par_rec_exact`]: crate::IntoParIterRecExact::into_par_rec_exact - pub fn into_eager(self) -> ParXap, Vo, X1, R> { + pub fn into_eager(self) -> ParXap, Vo, X1, R> { let (orchestrator, params, iter, xap1) = self.destruct(); let items = collect_items(iter); let iter = items.into_con_iter(); @@ -98,13 +89,10 @@ where } } -fn collect_items(iter: Rec) -> Vec +fn collect_items(iter: Rec) -> Vec where - S: Size, - I: IntoIterator, - I::IntoIter: ExactSizeIterator, - I::Item: Send, - E: Fn(&I::Item) -> I + Sync, + T: Send, + E: Fn(&T, &Queue) + Sync, { match iter.try_get_len() { Some(len) => { diff --git a/src/lib.rs b/src/lib.rs index 64f9e031..0c30c5fd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -62,7 +62,7 @@ pub use executor::{ DefaultExecutor, ParallelExecutor, ParallelExecutorWithDiagnostics, ThreadExecutor, }; pub use into_par_iter::IntoParIter; -pub use iter::{IntoParIterRec, IntoParIterRecExact}; +// pub use iter::{IntoParIterRec, IntoParIterRecExact}; pub use iter_into_par_iter::IterIntoParIter; pub use par_iter::ParIter; pub use par_iter_option::ParIterOption; From 77691ab2c9384580fed9f558e9f568ebea96fb23 Mon Sep 17 00:00:00 2001 From: orxfun Date: Fri, 24 Oct 2025 11:36:30 +0200 Subject: [PATCH 58/96] revise into_par_rec --- src/iter/recursive/into_par_rec_iter.rs | 496 ++++++++++++------------ src/iter/recursive/mod.rs | 2 +- 2 files changed, 247 insertions(+), 251 deletions(-) diff --git a/src/iter/recursive/into_par_rec_iter.rs b/src/iter/recursive/into_par_rec_iter.rs index 67ff9cc6..87c65068 100644 --- a/src/iter/recursive/into_par_rec_iter.rs +++ b/src/iter/recursive/into_par_rec_iter.rs @@ -1,5 +1,5 @@ use crate::{DefaultRunner, Params, computational_variants::Par}; -use orx_concurrent_recursive_iter::{ConcurrentRecursiveIter, ConcurrentRecursiveIterExact}; +use orx_concurrent_recursive_iter::{ConcurrentRecursiveIter, Queue}; // unknown size @@ -212,14 +212,12 @@ where /// .sum(); /// assert_eq!(sum, seq_sum); /// ``` - fn into_par_rec( + fn into_par_rec( self, extend: E, - ) -> Par, DefaultRunner> + ) -> Par, DefaultRunner> where - I: IntoIterator, - I::IntoIter: ExactSizeIterator, - E: Fn(&Self::Item) -> I + Sync; + E: Fn(&Self::Item, &Queue) + Sync; } impl IntoParIterRec for X @@ -227,257 +225,255 @@ where X: IntoIterator, X::Item: Send, { - fn into_par_rec( + fn into_par_rec( self, extend: E, - ) -> Par, DefaultRunner> + ) -> Par, DefaultRunner> where - I: IntoIterator, - I::IntoIter: ExactSizeIterator, - E: Fn(&Self::Item) -> I + Sync, + E: Fn(&Self::Item, &Queue) + Sync, { - let con_rec_iter = ConcurrentRecursiveIter::new(extend, self); + let con_rec_iter = ConcurrentRecursiveIter::new(self, extend); Par::new(DefaultRunner::default(), Params::default(), con_rec_iter) } } -// exact size +// // exact size -/// Trait to convert an iterator into an exact-sized recursive parallel iterator together with the `extend` method -/// and `exact_len`, -/// -/// Created parallel iterator is a regular parallel iterator; i.e., we have access to -/// all [`ParIter`] features. -/// -/// It is recursive due to the extension. The recursive parallel iterator will yield -/// * all initial elements contained in this iterator, -/// * all elements created by calling `extend` on each of the initial elements, let's call these depth-1 elements, -/// * all elements created by calling `extend` on each of the depth-1 elements, let's call these depth-2 elements, -/// * ..., and so on. -/// -/// You may read more about the [`ConcurrentRecursiveIterCore`]. -/// -/// See also [`IntoParIterRec`] -pub trait IntoParIterRecExact -where - Self: IntoIterator, - Self::Item: Send, -{ - /// Converts this iterator into a recursive parallel iterator together with the `extend` method and `exact_len`. - /// - /// Created parallel iterator is a regular parallel iterator; i.e., we have access to all [`ParIter`] features. - /// - /// It is recursive due to the extension. The recursive parallel iterator will yield - /// * all initial elements contained in this iterator, - /// * all elements created by calling `extend` on each of the initial elements, let's call these depth-1 elements, - /// * all elements created by calling `extend` on each of the depth-1 elements, let's call these depth-2 elements, - /// * ..., and so on. - /// - /// You may read more about the [`ConcurrentRecursiveIterCore`]. - /// - /// See also [`IntoParIterRec`] - /// - /// # Examples - /// - /// The following example has some code to set up until the `# usage` line. Notice that the `Node` - /// is a recursive data structure with children being other nodes. - /// - /// We have three initial elements `roots`. - /// - /// We want to compute is the sum of Fibonacci numbers of values of all nodes descending from the - /// roots. - /// - /// The `expand` function defines the recursive expansion behavior: - /// * every process node first adds its children to the end of the iterator, - /// * then, once they are process, we will create the children of these children as well, - /// * this process will recursively continue until there is no unprocessed node left. - /// - /// This crate makes use of the [`ConcurrentRecursiveIter`] and [`ConcurrentRecursiveIterExact`] - /// for this computation and provides three ways to execute this computation in parallel. - /// - /// ## A. Recursive Iterator with Exact Length - /// - /// If we know, or if it is possible and sufficiently cheap to find out, the exact length of the iterator, - /// it is recommended to work with exact length recursive iterator. Note that the exact length of an - /// iterator is the total of all elements that will be created. This gives the parallel executor - /// opportunity to optimize the chunk sizes. - /// - /// In the following example, we first calculate this number with `roots.iter().map(|x| x.seq_num_nodes()).sum();` - /// and then use `(&roots).into_par_rec_exact(extend, count)` to create our exact length recursive parallel - /// iterator. - /// - /// Note that, once we create the recursive parallel iterator, it is just another [`ParIter`]. In other words, - /// we have access to all parallel iterator features. - /// - /// ## B. Recursive Iterator with Unknown Length - /// - /// If we cannot know or it is expensive to know the exact length of the iterator ahead of time, we can - /// still create a recursive parallel iterator. In these cases; however, it is recommended to provide - /// chunk size explicitly depending on the number of threads that will be used and any estimate on the exact - /// length. - /// - /// In the following example, we directly create the parallel iterator with `(&roots).into_par_rec(extend)` - /// without providing any length information. Then, we ask the parallel executor to pull tasks in chunks of - /// 1024 with `.chunk_size(1024)`. Recall the general rule-of-thumb on chunk size parameter: - /// * the longer each individual computation, the smaller the chunks can be, - /// * when it is too small, we might suffer from parallelization overhead, - /// * when it is too large, we might suffer from heterogeneity of tasks which might lead to imbalance of - /// load of threads, - /// * we might try to set it to a large enough value to reduce parallelization overhead without causing - /// imbalance. - /// - /// ## C. Into Eager Transformation - /// - /// Even with exact length, a recursive parallel iterator is much more dynamic than a flat parallel - /// iterator. This dynamic nature of shrinking and growing concurrently requires a greater parallelization - /// overhead. An alternative approach is to eagerly discover all tasks and then perform the parallel - /// computation over the flattened input of tasks. - /// - /// This might increase performance in certain cases; however, requires storing the flattened tasks. - /// Therefore, it fits best to situations where the input elements are not very large. - /// In the following example, for instance, elements are of type `&Node` which is a pointer size - /// which makes it suitable for this approach. - /// - /// Note that exact size will be obtained during flattening; and hence, we do not need to provide the - /// `count`. - /// - /// In the example, we create eagerly flattened parallel iterator with the - /// `(&roots).into_par_rec(extend).into_eager()` call. - /// - /// [`ParIter`]: crate::ParIter - /// [`ConcurrentRecursiveIterCore`]: orx_concurrent_recursive_iter::ConcurrentRecursiveIterCore - /// - /// ## Example with all three approaches - /// - /// ``` - /// use orx_parallel::*; - /// use rand::{Rng, SeedableRng}; - /// use rand_chacha::ChaCha8Rng; - /// - /// struct Node { - /// value: Vec, - /// children: Vec, - /// } - /// - /// impl Node { - /// fn new(mut n: u32, rng: &mut impl Rng) -> Self { - /// let mut children = Vec::new(); - /// if n < 5 { - /// for _ in 0..n { - /// children.push(Node::new(0, rng)); - /// } - /// } else { - /// while n > 0 { - /// let n2 = rng.random_range(0..=n); - /// children.push(Node::new(n2, rng)); - /// n -= n2; - /// } - /// } - /// Self { - /// value: (0..rng.random_range(1..500)) - /// .map(|_| rng.random_range(0..40)) - /// .collect(), - /// children, - /// } - /// } - /// - /// fn seq_num_nodes(&self) -> usize { - /// 1 + self - /// .children - /// .iter() - /// .map(|node| node.seq_num_nodes()) - /// .sum::() - /// } - /// - /// fn seq_sum_fib(&self) -> u64 { - /// self.value.iter().map(|x| fibonacci(*x)).sum::() - /// + self.children.iter().map(|x| x.seq_sum_fib()).sum::() - /// } - /// } - /// - /// fn fibonacci(n: u64) -> u64 { - /// let mut a = 0; - /// let mut b = 1; - /// for _ in 0..n { - /// let c = a + b; - /// a = b; - /// b = c; - /// } - /// a - /// } - /// - /// // # usage - /// - /// // this defines how the iterator must extend: - /// // each node drawn from the iterator adds its children to the end of the iterator - /// fn extend<'a, 'b>(node: &'a &'b Node) -> &'b [Node] { - /// &node.children - /// } - /// - /// let mut rng = ChaCha8Rng::seed_from_u64(42); - /// let roots = vec![ - /// Node::new(50, &mut rng), - /// Node::new(20, &mut rng), - /// Node::new(40, &mut rng), - /// ]; - /// - /// let seq_sum: u64 = roots.iter().map(|x| x.seq_sum_fib()).sum(); - /// - /// // A. exact length, recommended when possible - /// - /// let count: usize = roots.iter().map(|x| x.seq_num_nodes()).sum(); - /// - /// let sum = (&roots) - /// .into_par_rec_exact(extend, count) - /// .map(|x| x.value.iter().map(|x| fibonacci(*x)).sum::()) - /// .sum(); - /// assert_eq!(sum, seq_sum); - /// - /// // B. guide the computation with chunk size, when length is unknown - /// - /// let sum = (&roots) - /// .into_par_rec(extend) - /// .chunk_size(1024) - /// .map(|x| x.value.iter().map(|x| fibonacci(*x)).sum::()) - /// .sum(); - /// assert_eq!(sum, seq_sum); - /// - /// // C. eagerly convert to a flat iterator - /// - /// let sum = (&roots) - /// .into_par_rec(extend) - /// .into_eager() - /// .map(|x| x.value.iter().map(|x| fibonacci(*x)).sum::()) - /// .sum(); - /// assert_eq!(sum, seq_sum); - /// ``` - fn into_par_rec_exact( - self, - extend: E, - exact_len: usize, - ) -> Par, DefaultRunner> - where - I: IntoIterator, - I::IntoIter: ExactSizeIterator, - E: Fn(&Self::Item) -> I + Sync; -} +// /// Trait to convert an iterator into an exact-sized recursive parallel iterator together with the `extend` method +// /// and `exact_len`, +// /// +// /// Created parallel iterator is a regular parallel iterator; i.e., we have access to +// /// all [`ParIter`] features. +// /// +// /// It is recursive due to the extension. The recursive parallel iterator will yield +// /// * all initial elements contained in this iterator, +// /// * all elements created by calling `extend` on each of the initial elements, let's call these depth-1 elements, +// /// * all elements created by calling `extend` on each of the depth-1 elements, let's call these depth-2 elements, +// /// * ..., and so on. +// /// +// /// You may read more about the [`ConcurrentRecursiveIterCore`]. +// /// +// /// See also [`IntoParIterRec`] +// pub trait IntoParIterRecExact +// where +// Self: IntoIterator, +// Self::Item: Send, +// { +// /// Converts this iterator into a recursive parallel iterator together with the `extend` method and `exact_len`. +// /// +// /// Created parallel iterator is a regular parallel iterator; i.e., we have access to all [`ParIter`] features. +// /// +// /// It is recursive due to the extension. The recursive parallel iterator will yield +// /// * all initial elements contained in this iterator, +// /// * all elements created by calling `extend` on each of the initial elements, let's call these depth-1 elements, +// /// * all elements created by calling `extend` on each of the depth-1 elements, let's call these depth-2 elements, +// /// * ..., and so on. +// /// +// /// You may read more about the [`ConcurrentRecursiveIterCore`]. +// /// +// /// See also [`IntoParIterRec`] +// /// +// /// # Examples +// /// +// /// The following example has some code to set up until the `# usage` line. Notice that the `Node` +// /// is a recursive data structure with children being other nodes. +// /// +// /// We have three initial elements `roots`. +// /// +// /// We want to compute is the sum of Fibonacci numbers of values of all nodes descending from the +// /// roots. +// /// +// /// The `expand` function defines the recursive expansion behavior: +// /// * every process node first adds its children to the end of the iterator, +// /// * then, once they are process, we will create the children of these children as well, +// /// * this process will recursively continue until there is no unprocessed node left. +// /// +// /// This crate makes use of the [`ConcurrentRecursiveIter`] and [`ConcurrentRecursiveIterExact`] +// /// for this computation and provides three ways to execute this computation in parallel. +// /// +// /// ## A. Recursive Iterator with Exact Length +// /// +// /// If we know, or if it is possible and sufficiently cheap to find out, the exact length of the iterator, +// /// it is recommended to work with exact length recursive iterator. Note that the exact length of an +// /// iterator is the total of all elements that will be created. This gives the parallel executor +// /// opportunity to optimize the chunk sizes. +// /// +// /// In the following example, we first calculate this number with `roots.iter().map(|x| x.seq_num_nodes()).sum();` +// /// and then use `(&roots).into_par_rec_exact(extend, count)` to create our exact length recursive parallel +// /// iterator. +// /// +// /// Note that, once we create the recursive parallel iterator, it is just another [`ParIter`]. In other words, +// /// we have access to all parallel iterator features. +// /// +// /// ## B. Recursive Iterator with Unknown Length +// /// +// /// If we cannot know or it is expensive to know the exact length of the iterator ahead of time, we can +// /// still create a recursive parallel iterator. In these cases; however, it is recommended to provide +// /// chunk size explicitly depending on the number of threads that will be used and any estimate on the exact +// /// length. +// /// +// /// In the following example, we directly create the parallel iterator with `(&roots).into_par_rec(extend)` +// /// without providing any length information. Then, we ask the parallel executor to pull tasks in chunks of +// /// 1024 with `.chunk_size(1024)`. Recall the general rule-of-thumb on chunk size parameter: +// /// * the longer each individual computation, the smaller the chunks can be, +// /// * when it is too small, we might suffer from parallelization overhead, +// /// * when it is too large, we might suffer from heterogeneity of tasks which might lead to imbalance of +// /// load of threads, +// /// * we might try to set it to a large enough value to reduce parallelization overhead without causing +// /// imbalance. +// /// +// /// ## C. Into Eager Transformation +// /// +// /// Even with exact length, a recursive parallel iterator is much more dynamic than a flat parallel +// /// iterator. This dynamic nature of shrinking and growing concurrently requires a greater parallelization +// /// overhead. An alternative approach is to eagerly discover all tasks and then perform the parallel +// /// computation over the flattened input of tasks. +// /// +// /// This might increase performance in certain cases; however, requires storing the flattened tasks. +// /// Therefore, it fits best to situations where the input elements are not very large. +// /// In the following example, for instance, elements are of type `&Node` which is a pointer size +// /// which makes it suitable for this approach. +// /// +// /// Note that exact size will be obtained during flattening; and hence, we do not need to provide the +// /// `count`. +// /// +// /// In the example, we create eagerly flattened parallel iterator with the +// /// `(&roots).into_par_rec(extend).into_eager()` call. +// /// +// /// [`ParIter`]: crate::ParIter +// /// [`ConcurrentRecursiveIterCore`]: orx_concurrent_recursive_iter::ConcurrentRecursiveIterCore +// /// +// /// ## Example with all three approaches +// /// +// /// ``` +// /// use orx_parallel::*; +// /// use rand::{Rng, SeedableRng}; +// /// use rand_chacha::ChaCha8Rng; +// /// +// /// struct Node { +// /// value: Vec, +// /// children: Vec, +// /// } +// /// +// /// impl Node { +// /// fn new(mut n: u32, rng: &mut impl Rng) -> Self { +// /// let mut children = Vec::new(); +// /// if n < 5 { +// /// for _ in 0..n { +// /// children.push(Node::new(0, rng)); +// /// } +// /// } else { +// /// while n > 0 { +// /// let n2 = rng.random_range(0..=n); +// /// children.push(Node::new(n2, rng)); +// /// n -= n2; +// /// } +// /// } +// /// Self { +// /// value: (0..rng.random_range(1..500)) +// /// .map(|_| rng.random_range(0..40)) +// /// .collect(), +// /// children, +// /// } +// /// } +// /// +// /// fn seq_num_nodes(&self) -> usize { +// /// 1 + self +// /// .children +// /// .iter() +// /// .map(|node| node.seq_num_nodes()) +// /// .sum::() +// /// } +// /// +// /// fn seq_sum_fib(&self) -> u64 { +// /// self.value.iter().map(|x| fibonacci(*x)).sum::() +// /// + self.children.iter().map(|x| x.seq_sum_fib()).sum::() +// /// } +// /// } +// /// +// /// fn fibonacci(n: u64) -> u64 { +// /// let mut a = 0; +// /// let mut b = 1; +// /// for _ in 0..n { +// /// let c = a + b; +// /// a = b; +// /// b = c; +// /// } +// /// a +// /// } +// /// +// /// // # usage +// /// +// /// // this defines how the iterator must extend: +// /// // each node drawn from the iterator adds its children to the end of the iterator +// /// fn extend<'a, 'b>(node: &'a &'b Node) -> &'b [Node] { +// /// &node.children +// /// } +// /// +// /// let mut rng = ChaCha8Rng::seed_from_u64(42); +// /// let roots = vec![ +// /// Node::new(50, &mut rng), +// /// Node::new(20, &mut rng), +// /// Node::new(40, &mut rng), +// /// ]; +// /// +// /// let seq_sum: u64 = roots.iter().map(|x| x.seq_sum_fib()).sum(); +// /// +// /// // A. exact length, recommended when possible +// /// +// /// let count: usize = roots.iter().map(|x| x.seq_num_nodes()).sum(); +// /// +// /// let sum = (&roots) +// /// .into_par_rec_exact(extend, count) +// /// .map(|x| x.value.iter().map(|x| fibonacci(*x)).sum::()) +// /// .sum(); +// /// assert_eq!(sum, seq_sum); +// /// +// /// // B. guide the computation with chunk size, when length is unknown +// /// +// /// let sum = (&roots) +// /// .into_par_rec(extend) +// /// .chunk_size(1024) +// /// .map(|x| x.value.iter().map(|x| fibonacci(*x)).sum::()) +// /// .sum(); +// /// assert_eq!(sum, seq_sum); +// /// +// /// // C. eagerly convert to a flat iterator +// /// +// /// let sum = (&roots) +// /// .into_par_rec(extend) +// /// .into_eager() +// /// .map(|x| x.value.iter().map(|x| fibonacci(*x)).sum::()) +// /// .sum(); +// /// assert_eq!(sum, seq_sum); +// /// ``` +// fn into_par_rec_exact( +// self, +// extend: E, +// exact_len: usize, +// ) -> Par, DefaultRunner> +// where +// I: IntoIterator, +// I::IntoIter: ExactSizeIterator, +// E: Fn(&Self::Item) -> I + Sync; +// } -impl IntoParIterRecExact for X -where - X: IntoIterator, - X::Item: Send, -{ - fn into_par_rec_exact( - self, - extend: E, - exact_len: usize, - ) -> Par, DefaultRunner> - where - I: IntoIterator, - I::IntoIter: ExactSizeIterator, - E: Fn(&Self::Item) -> I + Sync, - { - let con_rec_iter = ConcurrentRecursiveIterExact::new_exact(extend, self, exact_len); - Par::new(DefaultRunner::default(), Params::default(), con_rec_iter) - } -} +// impl IntoParIterRecExact for X +// where +// X: IntoIterator, +// X::Item: Send, +// { +// fn into_par_rec_exact( +// self, +// extend: E, +// exact_len: usize, +// ) -> Par, DefaultRunner> +// where +// I: IntoIterator, +// I::IntoIter: ExactSizeIterator, +// E: Fn(&Self::Item) -> I + Sync, +// { +// let con_rec_iter = ConcurrentRecursiveIterExact::new_exact(extend, self, exact_len); +// Par::new(DefaultRunner::default(), Params::default(), con_rec_iter) +// } +// } diff --git a/src/iter/recursive/mod.rs b/src/iter/recursive/mod.rs index 4d2f28b3..0e8eb5f8 100644 --- a/src/iter/recursive/mod.rs +++ b/src/iter/recursive/mod.rs @@ -1,4 +1,4 @@ -// mod into_par_rec_iter; +mod into_par_rec_iter; mod rec_per_iter; // pub use into_par_rec_iter::{IntoParIterRec, IntoParIterRecExact}; From dcd33410c6486100c6c1d6f6d21ac53485e377e4 Mon Sep 17 00:00:00 2001 From: orxfun Date: Fri, 24 Oct 2025 11:37:37 +0200 Subject: [PATCH 59/96] implement into_par_rec_exact --- src/iter/recursive/into_par_rec_iter.rs | 261 ++---------------------- 1 file changed, 20 insertions(+), 241 deletions(-) diff --git a/src/iter/recursive/into_par_rec_iter.rs b/src/iter/recursive/into_par_rec_iter.rs index 87c65068..66cd7768 100644 --- a/src/iter/recursive/into_par_rec_iter.rs +++ b/src/iter/recursive/into_par_rec_iter.rs @@ -218,6 +218,14 @@ where ) -> Par, DefaultRunner> where E: Fn(&Self::Item, &Queue) + Sync; + + fn into_par_rec_exact( + self, + extend: E, + exact_len: usize, + ) -> Par, DefaultRunner> + where + E: Fn(&Self::Item, &Queue) + Sync; } impl IntoParIterRec for X @@ -235,245 +243,16 @@ where let con_rec_iter = ConcurrentRecursiveIter::new(self, extend); Par::new(DefaultRunner::default(), Params::default(), con_rec_iter) } -} - -// // exact size - -// /// Trait to convert an iterator into an exact-sized recursive parallel iterator together with the `extend` method -// /// and `exact_len`, -// /// -// /// Created parallel iterator is a regular parallel iterator; i.e., we have access to -// /// all [`ParIter`] features. -// /// -// /// It is recursive due to the extension. The recursive parallel iterator will yield -// /// * all initial elements contained in this iterator, -// /// * all elements created by calling `extend` on each of the initial elements, let's call these depth-1 elements, -// /// * all elements created by calling `extend` on each of the depth-1 elements, let's call these depth-2 elements, -// /// * ..., and so on. -// /// -// /// You may read more about the [`ConcurrentRecursiveIterCore`]. -// /// -// /// See also [`IntoParIterRec`] -// pub trait IntoParIterRecExact -// where -// Self: IntoIterator, -// Self::Item: Send, -// { -// /// Converts this iterator into a recursive parallel iterator together with the `extend` method and `exact_len`. -// /// -// /// Created parallel iterator is a regular parallel iterator; i.e., we have access to all [`ParIter`] features. -// /// -// /// It is recursive due to the extension. The recursive parallel iterator will yield -// /// * all initial elements contained in this iterator, -// /// * all elements created by calling `extend` on each of the initial elements, let's call these depth-1 elements, -// /// * all elements created by calling `extend` on each of the depth-1 elements, let's call these depth-2 elements, -// /// * ..., and so on. -// /// -// /// You may read more about the [`ConcurrentRecursiveIterCore`]. -// /// -// /// See also [`IntoParIterRec`] -// /// -// /// # Examples -// /// -// /// The following example has some code to set up until the `# usage` line. Notice that the `Node` -// /// is a recursive data structure with children being other nodes. -// /// -// /// We have three initial elements `roots`. -// /// -// /// We want to compute is the sum of Fibonacci numbers of values of all nodes descending from the -// /// roots. -// /// -// /// The `expand` function defines the recursive expansion behavior: -// /// * every process node first adds its children to the end of the iterator, -// /// * then, once they are process, we will create the children of these children as well, -// /// * this process will recursively continue until there is no unprocessed node left. -// /// -// /// This crate makes use of the [`ConcurrentRecursiveIter`] and [`ConcurrentRecursiveIterExact`] -// /// for this computation and provides three ways to execute this computation in parallel. -// /// -// /// ## A. Recursive Iterator with Exact Length -// /// -// /// If we know, or if it is possible and sufficiently cheap to find out, the exact length of the iterator, -// /// it is recommended to work with exact length recursive iterator. Note that the exact length of an -// /// iterator is the total of all elements that will be created. This gives the parallel executor -// /// opportunity to optimize the chunk sizes. -// /// -// /// In the following example, we first calculate this number with `roots.iter().map(|x| x.seq_num_nodes()).sum();` -// /// and then use `(&roots).into_par_rec_exact(extend, count)` to create our exact length recursive parallel -// /// iterator. -// /// -// /// Note that, once we create the recursive parallel iterator, it is just another [`ParIter`]. In other words, -// /// we have access to all parallel iterator features. -// /// -// /// ## B. Recursive Iterator with Unknown Length -// /// -// /// If we cannot know or it is expensive to know the exact length of the iterator ahead of time, we can -// /// still create a recursive parallel iterator. In these cases; however, it is recommended to provide -// /// chunk size explicitly depending on the number of threads that will be used and any estimate on the exact -// /// length. -// /// -// /// In the following example, we directly create the parallel iterator with `(&roots).into_par_rec(extend)` -// /// without providing any length information. Then, we ask the parallel executor to pull tasks in chunks of -// /// 1024 with `.chunk_size(1024)`. Recall the general rule-of-thumb on chunk size parameter: -// /// * the longer each individual computation, the smaller the chunks can be, -// /// * when it is too small, we might suffer from parallelization overhead, -// /// * when it is too large, we might suffer from heterogeneity of tasks which might lead to imbalance of -// /// load of threads, -// /// * we might try to set it to a large enough value to reduce parallelization overhead without causing -// /// imbalance. -// /// -// /// ## C. Into Eager Transformation -// /// -// /// Even with exact length, a recursive parallel iterator is much more dynamic than a flat parallel -// /// iterator. This dynamic nature of shrinking and growing concurrently requires a greater parallelization -// /// overhead. An alternative approach is to eagerly discover all tasks and then perform the parallel -// /// computation over the flattened input of tasks. -// /// -// /// This might increase performance in certain cases; however, requires storing the flattened tasks. -// /// Therefore, it fits best to situations where the input elements are not very large. -// /// In the following example, for instance, elements are of type `&Node` which is a pointer size -// /// which makes it suitable for this approach. -// /// -// /// Note that exact size will be obtained during flattening; and hence, we do not need to provide the -// /// `count`. -// /// -// /// In the example, we create eagerly flattened parallel iterator with the -// /// `(&roots).into_par_rec(extend).into_eager()` call. -// /// -// /// [`ParIter`]: crate::ParIter -// /// [`ConcurrentRecursiveIterCore`]: orx_concurrent_recursive_iter::ConcurrentRecursiveIterCore -// /// -// /// ## Example with all three approaches -// /// -// /// ``` -// /// use orx_parallel::*; -// /// use rand::{Rng, SeedableRng}; -// /// use rand_chacha::ChaCha8Rng; -// /// -// /// struct Node { -// /// value: Vec, -// /// children: Vec, -// /// } -// /// -// /// impl Node { -// /// fn new(mut n: u32, rng: &mut impl Rng) -> Self { -// /// let mut children = Vec::new(); -// /// if n < 5 { -// /// for _ in 0..n { -// /// children.push(Node::new(0, rng)); -// /// } -// /// } else { -// /// while n > 0 { -// /// let n2 = rng.random_range(0..=n); -// /// children.push(Node::new(n2, rng)); -// /// n -= n2; -// /// } -// /// } -// /// Self { -// /// value: (0..rng.random_range(1..500)) -// /// .map(|_| rng.random_range(0..40)) -// /// .collect(), -// /// children, -// /// } -// /// } -// /// -// /// fn seq_num_nodes(&self) -> usize { -// /// 1 + self -// /// .children -// /// .iter() -// /// .map(|node| node.seq_num_nodes()) -// /// .sum::() -// /// } -// /// -// /// fn seq_sum_fib(&self) -> u64 { -// /// self.value.iter().map(|x| fibonacci(*x)).sum::() -// /// + self.children.iter().map(|x| x.seq_sum_fib()).sum::() -// /// } -// /// } -// /// -// /// fn fibonacci(n: u64) -> u64 { -// /// let mut a = 0; -// /// let mut b = 1; -// /// for _ in 0..n { -// /// let c = a + b; -// /// a = b; -// /// b = c; -// /// } -// /// a -// /// } -// /// -// /// // # usage -// /// -// /// // this defines how the iterator must extend: -// /// // each node drawn from the iterator adds its children to the end of the iterator -// /// fn extend<'a, 'b>(node: &'a &'b Node) -> &'b [Node] { -// /// &node.children -// /// } -// /// -// /// let mut rng = ChaCha8Rng::seed_from_u64(42); -// /// let roots = vec![ -// /// Node::new(50, &mut rng), -// /// Node::new(20, &mut rng), -// /// Node::new(40, &mut rng), -// /// ]; -// /// -// /// let seq_sum: u64 = roots.iter().map(|x| x.seq_sum_fib()).sum(); -// /// -// /// // A. exact length, recommended when possible -// /// -// /// let count: usize = roots.iter().map(|x| x.seq_num_nodes()).sum(); -// /// -// /// let sum = (&roots) -// /// .into_par_rec_exact(extend, count) -// /// .map(|x| x.value.iter().map(|x| fibonacci(*x)).sum::()) -// /// .sum(); -// /// assert_eq!(sum, seq_sum); -// /// -// /// // B. guide the computation with chunk size, when length is unknown -// /// -// /// let sum = (&roots) -// /// .into_par_rec(extend) -// /// .chunk_size(1024) -// /// .map(|x| x.value.iter().map(|x| fibonacci(*x)).sum::()) -// /// .sum(); -// /// assert_eq!(sum, seq_sum); -// /// -// /// // C. eagerly convert to a flat iterator -// /// -// /// let sum = (&roots) -// /// .into_par_rec(extend) -// /// .into_eager() -// /// .map(|x| x.value.iter().map(|x| fibonacci(*x)).sum::()) -// /// .sum(); -// /// assert_eq!(sum, seq_sum); -// /// ``` -// fn into_par_rec_exact( -// self, -// extend: E, -// exact_len: usize, -// ) -> Par, DefaultRunner> -// where -// I: IntoIterator, -// I::IntoIter: ExactSizeIterator, -// E: Fn(&Self::Item) -> I + Sync; -// } -// impl IntoParIterRecExact for X -// where -// X: IntoIterator, -// X::Item: Send, -// { -// fn into_par_rec_exact( -// self, -// extend: E, -// exact_len: usize, -// ) -> Par, DefaultRunner> -// where -// I: IntoIterator, -// I::IntoIter: ExactSizeIterator, -// E: Fn(&Self::Item) -> I + Sync, -// { -// let con_rec_iter = ConcurrentRecursiveIterExact::new_exact(extend, self, exact_len); -// Par::new(DefaultRunner::default(), Params::default(), con_rec_iter) -// } -// } + fn into_par_rec_exact( + self, + extend: E, + exact_len: usize, + ) -> Par, DefaultRunner> + where + E: Fn(&Self::Item, &Queue) + Sync, + { + let con_rec_iter = ConcurrentRecursiveIter::new_exact(self, extend, exact_len); + Par::new(DefaultRunner::default(), Params::default(), con_rec_iter) + } +} From e226c7bdd5f1df6ea078f50ffe5f548332b572cc Mon Sep 17 00:00:00 2001 From: orxfun Date: Fri, 24 Oct 2025 11:38:13 +0200 Subject: [PATCH 60/96] export recursive iterators --- src/iter/mod.rs | 2 +- src/iter/recursive/mod.rs | 2 +- src/lib.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/iter/mod.rs b/src/iter/mod.rs index 8f37a7d5..5cf888b1 100644 --- a/src/iter/mod.rs +++ b/src/iter/mod.rs @@ -1,5 +1,5 @@ mod recursive; mod special_iterators; -// pub use recursive::{IntoParIterRec, IntoParIterRecExact}; +pub use recursive::IntoParIterRec; pub use special_iterators::{ParEmpty, empty}; diff --git a/src/iter/recursive/mod.rs b/src/iter/recursive/mod.rs index 0e8eb5f8..93e0ce01 100644 --- a/src/iter/recursive/mod.rs +++ b/src/iter/recursive/mod.rs @@ -1,4 +1,4 @@ mod into_par_rec_iter; mod rec_per_iter; -// pub use into_par_rec_iter::{IntoParIterRec, IntoParIterRecExact}; +pub use into_par_rec_iter::IntoParIterRec; diff --git a/src/lib.rs b/src/lib.rs index 0c30c5fd..8cf42389 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -62,7 +62,7 @@ pub use executor::{ DefaultExecutor, ParallelExecutor, ParallelExecutorWithDiagnostics, ThreadExecutor, }; pub use into_par_iter::IntoParIter; -// pub use iter::{IntoParIterRec, IntoParIterRecExact}; +pub use iter::IntoParIterRec; pub use iter_into_par_iter::IterIntoParIter; pub use par_iter::ParIter; pub use par_iter_option::ParIterOption; From ab1078f0d25083510461ba1d360a363e9997f9e1 Mon Sep 17 00:00:00 2001 From: orxfun Date: Fri, 24 Oct 2025 11:56:52 +0200 Subject: [PATCH 61/96] revise recursive iterator examples and benches --- benches/rec_iter_map_collect.rs | 13 +++++++------ benches/rec_iter_map_sum.rs | 17 +++++++++-------- examples/rec_iter_map_collect.rs | 9 +++++---- examples/rec_iter_map_sum.rs | 10 ++++++---- 4 files changed, 27 insertions(+), 22 deletions(-) diff --git a/benches/rec_iter_map_collect.rs b/benches/rec_iter_map_collect.rs index baffda78..d6db790c 100644 --- a/benches/rec_iter_map_collect.rs +++ b/benches/rec_iter_map_collect.rs @@ -1,4 +1,5 @@ use criterion::{BenchmarkId, Criterion, criterion_group, criterion_main}; +use orx_concurrent_recursive_iter::Queue; use orx_parallel::*; use orx_split_vec::SplitVec; use rand::prelude::*; @@ -75,8 +76,8 @@ fn seq(roots: &[Node], work: usize) -> Vec { } fn orx_lazy_unknown_chunk1024(roots: &[Node], work: usize) -> SplitVec { - fn extend<'a, 'b>(node: &'a &'b Node) -> &'b [Node] { - &node.children + fn extend<'a, 'b>(node: &'a &'b Node, queue: &Queue<&'b Node>) { + queue.extend(&node.children); } roots @@ -87,8 +88,8 @@ fn orx_lazy_unknown_chunk1024(roots: &[Node], work: usize) -> SplitVec { } fn orx_lazy_exact(roots: &[Node], work: usize, num_nodes: usize) -> SplitVec { - fn extend<'a, 'b>(node: &'a &'b Node) -> &'b [Node] { - &node.children + fn extend<'a, 'b>(node: &'a &'b Node, queue: &Queue<&'b Node>) { + queue.extend(&node.children); } roots @@ -98,8 +99,8 @@ fn orx_lazy_exact(roots: &[Node], work: usize, num_nodes: usize) -> SplitVec SplitVec { - fn extend<'a, 'b>(node: &'a &'b Node) -> &'b [Node] { - &node.children + fn extend<'a, 'b>(node: &'a &'b Node, queue: &Queue<&'b Node>) { + queue.extend(&node.children); } roots diff --git a/benches/rec_iter_map_sum.rs b/benches/rec_iter_map_sum.rs index 2d4ba333..e21c91d1 100644 --- a/benches/rec_iter_map_sum.rs +++ b/benches/rec_iter_map_sum.rs @@ -1,4 +1,5 @@ use criterion::{BenchmarkId, Criterion, criterion_group, criterion_main}; +use orx_concurrent_recursive_iter::Queue; use orx_parallel::*; use rand::prelude::*; use rand_chacha::ChaCha8Rng; @@ -101,8 +102,8 @@ fn rayon(roots: &[Node], work: usize) -> u64 { } fn orx_lazy_unknown_chunk1024(roots: &[Node], work: usize) -> u64 { - fn extend<'a, 'b>(node: &'a &'b Node) -> &'b [Node] { - &node.children + fn extend<'a, 'b>(node: &'a &'b Node, queue: &Queue<&'b Node>) { + queue.extend(&node.children); } roots @@ -113,8 +114,8 @@ fn orx_lazy_unknown_chunk1024(roots: &[Node], work: usize) -> u64 { } fn orx_lazy_exact(roots: &[Node], work: usize, num_nodes: usize) -> u64 { - fn extend<'a, 'b>(node: &'a &'b Node) -> &'b [Node] { - &node.children + fn extend<'a, 'b>(node: &'a &'b Node, queue: &Queue<&'b Node>) { + queue.extend(&node.children); } roots @@ -124,8 +125,8 @@ fn orx_lazy_exact(roots: &[Node], work: usize, num_nodes: usize) -> u64 { } fn orx_lazy_exact_flat_map(roots: &[Node], work: usize, num_nodes: usize) -> u64 { - fn extend<'a, 'b>(node: &'a &'b Node) -> &'b [Node] { - &node.children + fn extend<'a, 'b>(node: &'a &'b Node, queue: &Queue<&'b Node>) { + queue.extend(&node.children); } roots @@ -135,8 +136,8 @@ fn orx_lazy_exact_flat_map(roots: &[Node], work: usize, num_nodes: usize) -> u64 } fn orx_eager(roots: &[Node], work: usize) -> u64 { - fn extend<'a, 'b>(node: &'a &'b Node) -> &'b [Node] { - &node.children + fn extend<'a, 'b>(node: &'a &'b Node, queue: &Queue<&'b Node>) { + queue.extend(&node.children); } roots diff --git a/examples/rec_iter_map_collect.rs b/examples/rec_iter_map_collect.rs index c26a0a78..bebdb06d 100644 --- a/examples/rec_iter_map_collect.rs +++ b/examples/rec_iter_map_collect.rs @@ -1,3 +1,4 @@ +use orx_concurrent_recursive_iter::Queue; use orx_parallel::*; use orx_split_vec::SplitVec; use rand::{Rng, SeedableRng}; @@ -58,8 +59,8 @@ impl Node { } fn par_rec(roots: &[Node]) -> SplitVec { - fn extend<'a, 'b>(node: &'a &'b Node) -> &'b [Node] { - &node.children + fn extend<'a, 'b>(node: &'a &'b Node, queue: &Queue<&'b Node>) { + queue.extend(&node.children); } let count: usize = roots.iter().map(|x| x.seq_num_nodes()).sum(); @@ -73,8 +74,8 @@ fn par_rec(roots: &[Node]) -> SplitVec { } fn par_rec_eager(roots: &[Node]) -> SplitVec { - fn extend<'a, 'b>(node: &'a &'b Node) -> &'b [Node] { - &node.children + fn extend<'a, 'b>(node: &'a &'b Node, queue: &Queue<&'b Node>) { + queue.extend(&node.children); } let runner = DefaultRunner::default().with_diagnostics(); diff --git a/examples/rec_iter_map_sum.rs b/examples/rec_iter_map_sum.rs index 9fffc9a2..25f700d5 100644 --- a/examples/rec_iter_map_sum.rs +++ b/examples/rec_iter_map_sum.rs @@ -1,3 +1,4 @@ +use orx_concurrent_recursive_iter::Queue; use orx_parallel::*; use rand::{Rng, SeedableRng}; use rand_chacha::ChaCha8Rng; @@ -55,9 +56,10 @@ impl Node { } fn par_rec(roots: &[Node]) -> u64 { - fn extend<'a, 'b>(node: &'a &'b Node) -> &'b [Node] { - &node.children + fn extend<'a, 'b>(node: &'a &'b Node, queue: &Queue<&'b Node>) { + queue.extend(&node.children); } + let count: usize = roots.iter().map(|x| x.seq_num_nodes()).sum(); let runner = DefaultRunner::default().with_diagnostics(); @@ -70,8 +72,8 @@ fn par_rec(roots: &[Node]) -> u64 { } fn par_rec_eager(roots: &[Node]) -> u64 { - fn extend<'a, 'b>(node: &'a &'b Node) -> &'b [Node] { - &node.children + fn extend<'a, 'b>(node: &'a &'b Node, queue: &Queue<&'b Node>) { + queue.extend(&node.children); } let runner = DefaultRunner::default().with_diagnostics(); From c48336deafca05320b35084ac905dc6951017fb6 Mon Sep 17 00:00:00 2001 From: orxfun Date: Fri, 24 Oct 2025 12:13:23 +0200 Subject: [PATCH 62/96] fix doc test for par rec iter --- src/iter/recursive/into_par_rec_iter.rs | 67 +++++++++++++------------ src/lib.rs | 5 ++ 2 files changed, 41 insertions(+), 31 deletions(-) diff --git a/src/iter/recursive/into_par_rec_iter.rs b/src/iter/recursive/into_par_rec_iter.rs index 66cd7768..a64558ff 100644 --- a/src/iter/recursive/into_par_rec_iter.rs +++ b/src/iter/recursive/into_par_rec_iter.rs @@ -22,39 +22,51 @@ where Self: IntoIterator, Self::Item: Send, { - /// Converts this iterator into a recursive parallel iterator together with the `extend` - /// method. + /// Converts this iterator into a recursive parallel iterator together with the `extend` method. /// - /// Created parallel iterator is a regular parallel iterator; i.e., we have access to - /// all [`ParIter`] features. + /// Created parallel iterator is a regular parallel iterator; i.e., we have access to all [`ParIter`] features. /// /// It is recursive due to the extension. The recursive parallel iterator will yield /// * all initial elements contained in this iterator, - /// * all elements created by calling `extend` on each of the initial elements, let's call these depth-1 elements, - /// * all elements created by calling `extend` on each of the depth-1 elements, let's call these depth-2 elements, - /// * ..., and so on. + /// * all elements dynamically added to the queue with the `extend` method while processing the elements. /// - /// You may read more about the [`ConcurrentRecursiveIterCore`]. - /// - /// See also [`IntoParIterRecExact`] + /// You may read more about the [`ConcurrentRecursiveIter`]. /// /// # Examples /// /// The following example has some code to set up until the `# usage` line. Notice that the `Node` - /// is a recursive data structure with children being other nodes. + /// is a non-linear data structure, each node having children nodes to be recursively processed. /// /// We have three initial elements `roots`. /// - /// We want to compute is the sum of Fibonacci numbers of values of all nodes descending from the - /// roots. + /// We want to compute sum of Fibonacci numbers of values of all nodes descending from the roots. + /// + /// The `expand` function defines the recursive expansion behavior. It takes two arguments: + /// * `element: &Self::Item` is the item being processed. + /// * `queue: Queue` is the queue of remaining elements/tasks which exposes two methods: + /// * `push(item)` allows us to add one item to the queue, + /// * `extend(items)` allows us to add all of the items to the queue. Here `items` must have a known + /// size (`ExactSizeIterator`). + /// + /// Using either of the methods might be beneficial for different use cases. /// - /// The `expand` function defines the recursive expansion behavior: - /// * every process node first adds its children to the end of the iterator, - /// * then, once they are process, we will create the children of these children as well, - /// * this process will recursively continue until there is no unprocessed node left. + /// Pushing children one by one makes the new task available for other threads as fast as possible. Further, + /// when we don't know the exact number of children ahead of time, and we don't want to use heap allocation + /// to store the children in a vec before adding them to the queue just to make it sized, we can add the + /// elements one-by-one with the `queue.push(item)` method. On the other hand, this approach will have more + /// parallelization overhead. /// - /// This crate makes use of the [`ConcurrentRecursiveIter`] and [`ConcurrentRecursiveIterExact`] - /// for this computation and provides three ways to execute this computation in parallel. + /// When we extending children all at once using `queue.extend(items)`, we minimize the parallelization overhead + /// for adding tasks to the queue. On the other hand, the children will be available only when writing of all + /// children to the queue is complete which might cause idleness when tasks are scarce. Still, the recommendation + /// is to try to `extend` first whenever possible due to the following: (i) if we extend with a lot of children, + /// the tasks will not be scarce; (ii) and if we extend with only a few of items, the delay of making the tasks + /// available for other threads will be short. + /// + /// Nevertheless, the decision is use-case specific and best to benchmark for the specific input. + /// + /// This crate makes use of the [`ConcurrentRecursiveIter`] for this computation and provides three ways to execute + /// this computation in parallel. /// /// ## A. Recursive Iterator with Exact Length /// @@ -94,19 +106,12 @@ where /// overhead. An alternative approach is to eagerly discover all tasks and then perform the parallel /// computation over the flattened input of tasks. /// - /// This might increase performance in certain cases; however, requires storing the flattened tasks. - /// Therefore, it fits best to situations where the input elements are not very large. - /// In the following example, for instance, elements are of type `&Node` which is a pointer size - /// which makes it suitable for this approach. - /// - /// Note that exact size will be obtained during flattening; and hence, we do not need to provide the - /// `count`. + /// Note that exact size will be obtained during flattening; and hence, we do not need to provide the `count`. /// - /// In the example, we create eagerly flattened parallel iterator with the - /// `(&roots).into_par_rec(extend).into_eager()` call. + /// In the example, we create eagerly flattened parallel iterator with the `(&roots).into_par_rec(extend).into_eager()` call. /// /// [`ParIter`]: crate::ParIter - /// [`ConcurrentRecursiveIterCore`]: orx_concurrent_recursive_iter::ConcurrentRecursiveIterCore + /// [`ConcurrentRecursiveIter`]: orx_concurrent_recursive_iter::ConcurrentRecursiveIter /// /// ## Example with all three approaches /// @@ -171,8 +176,8 @@ where /// /// // this defines how the iterator must extend: /// // each node drawn from the iterator adds its children to the end of the iterator - /// fn extend<'a, 'b>(node: &'a &'b Node) -> &'b [Node] { - /// &node.children + /// fn extend<'a, 'b>(node: &'a &'b Node, queue: &Queue<&'b Node>) { + /// queue.extend(&node.children); /// } /// /// let mut rng = ChaCha8Rng::seed_from_u64(42); diff --git a/src/lib.rs b/src/lib.rs index 8cf42389..8b637a3a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -57,6 +57,11 @@ pub mod generic_iterator; #[cfg(test)] mod test_utils; +// re-export +pub use orx_concurrent_recursive_iter::Queue; + +// export + pub use collect_into::ParCollectInto; pub use executor::{ DefaultExecutor, ParallelExecutor, ParallelExecutorWithDiagnostics, ThreadExecutor, From 2b015a37d6340d17cb7c0bc78934f6dcda371ccf Mon Sep 17 00:00:00 2001 From: orxfun Date: Fri, 24 Oct 2025 12:14:01 +0200 Subject: [PATCH 63/96] add missing docs --- src/iter/recursive/into_par_rec_iter.rs | 195 ++++++++++++++++++++++++ 1 file changed, 195 insertions(+) diff --git a/src/iter/recursive/into_par_rec_iter.rs b/src/iter/recursive/into_par_rec_iter.rs index a64558ff..61bb6f17 100644 --- a/src/iter/recursive/into_par_rec_iter.rs +++ b/src/iter/recursive/into_par_rec_iter.rs @@ -224,6 +224,201 @@ where where E: Fn(&Self::Item, &Queue) + Sync; + /// Converts this iterator into a recursive parallel iterator together with the `extend` method. + /// + /// Created parallel iterator is a regular parallel iterator; i.e., we have access to all [`ParIter`] features. + /// + /// It is recursive due to the extension. The recursive parallel iterator will yield + /// * all initial elements contained in this iterator, + /// * all elements dynamically added to the queue with the `extend` method while processing the elements. + /// + /// You may read more about the [`ConcurrentRecursiveIter`]. + /// + /// # Examples + /// + /// The following example has some code to set up until the `# usage` line. Notice that the `Node` + /// is a non-linear data structure, each node having children nodes to be recursively processed. + /// + /// We have three initial elements `roots`. + /// + /// We want to compute sum of Fibonacci numbers of values of all nodes descending from the roots. + /// + /// The `expand` function defines the recursive expansion behavior. It takes two arguments: + /// * `element: &Self::Item` is the item being processed. + /// * `queue: Queue` is the queue of remaining elements/tasks which exposes two methods: + /// * `push(item)` allows us to add one item to the queue, + /// * `extend(items)` allows us to add all of the items to the queue. Here `items` must have a known + /// size (`ExactSizeIterator`). + /// + /// Using either of the methods might be beneficial for different use cases. + /// + /// Pushing children one by one makes the new task available for other threads as fast as possible. Further, + /// when we don't know the exact number of children ahead of time, and we don't want to use heap allocation + /// to store the children in a vec before adding them to the queue just to make it sized, we can add the + /// elements one-by-one with the `queue.push(item)` method. On the other hand, this approach will have more + /// parallelization overhead. + /// + /// When we extending children all at once using `queue.extend(items)`, we minimize the parallelization overhead + /// for adding tasks to the queue. On the other hand, the children will be available only when writing of all + /// children to the queue is complete which might cause idleness when tasks are scarce. Still, the recommendation + /// is to try to `extend` first whenever possible due to the following: (i) if we extend with a lot of children, + /// the tasks will not be scarce; (ii) and if we extend with only a few of items, the delay of making the tasks + /// available for other threads will be short. + /// + /// Nevertheless, the decision is use-case specific and best to benchmark for the specific input. + /// + /// This crate makes use of the [`ConcurrentRecursiveIter`] for this computation and provides three ways to execute + /// this computation in parallel. + /// + /// ## A. Recursive Iterator with Exact Length + /// + /// If we know, or if it is possible and sufficiently cheap to find out, the exact length of the iterator, + /// it is recommended to work with exact length recursive iterator. Note that the exact length of an + /// iterator is the total of all elements that will be created. This gives the parallel executor + /// opportunity to optimize the chunk sizes. + /// + /// In the following example, we first calculate this number with `roots.iter().map(|x| x.seq_num_nodes()).sum();` + /// and then use `(&roots).into_par_rec_exact(extend, count)` to create our exact length recursive parallel + /// iterator. + /// + /// Note that, once we create the recursive parallel iterator, it is just another [`ParIter`]. In other words, + /// we have access to all parallel iterator features. + /// + /// ## B. Recursive Iterator with Unknown Length + /// + /// If we cannot know or it is expensive to know the exact length of the iterator ahead of time, we can + /// still create a recursive parallel iterator. In these cases; however, it is recommended to provide + /// chunk size explicitly depending on the number of threads that will be used and any estimate on the exact + /// length. + /// + /// In the following example, we directly create the parallel iterator with `(&roots).into_par_rec(extend)` + /// without providing any length information. Then, we ask the parallel executor to pull tasks in chunks of + /// 1024 with `.chunk_size(1024)`. Recall the general rule-of-thumb on chunk size parameter: + /// * the longer each individual computation, the smaller the chunks can be, + /// * when it is too small, we might suffer from parallelization overhead, + /// * when it is too large, we might suffer from heterogeneity of tasks which might lead to imbalance of + /// load of threads, + /// * we might try to set it to a large enough value to reduce parallelization overhead without causing + /// imbalance. + /// + /// ## C. Into Eager Transformation + /// + /// Even with exact length, a recursive parallel iterator is much more dynamic than a flat parallel + /// iterator. This dynamic nature of shrinking and growing concurrently requires a greater parallelization + /// overhead. An alternative approach is to eagerly discover all tasks and then perform the parallel + /// computation over the flattened input of tasks. + /// + /// Note that exact size will be obtained during flattening; and hence, we do not need to provide the `count`. + /// + /// In the example, we create eagerly flattened parallel iterator with the `(&roots).into_par_rec(extend).into_eager()` call. + /// + /// [`ParIter`]: crate::ParIter + /// [`ConcurrentRecursiveIter`]: orx_concurrent_recursive_iter::ConcurrentRecursiveIter + /// + /// ## Example with all three approaches + /// + /// ``` + /// use orx_parallel::*; + /// use rand::{Rng, SeedableRng}; + /// use rand_chacha::ChaCha8Rng; + /// + /// struct Node { + /// value: Vec, + /// children: Vec, + /// } + /// + /// impl Node { + /// fn new(mut n: u32, rng: &mut impl Rng) -> Self { + /// let mut children = Vec::new(); + /// if n < 5 { + /// for _ in 0..n { + /// children.push(Node::new(0, rng)); + /// } + /// } else { + /// while n > 0 { + /// let n2 = rng.random_range(0..=n); + /// children.push(Node::new(n2, rng)); + /// n -= n2; + /// } + /// } + /// Self { + /// value: (0..rng.random_range(1..500)) + /// .map(|_| rng.random_range(0..40)) + /// .collect(), + /// children, + /// } + /// } + /// + /// fn seq_num_nodes(&self) -> usize { + /// 1 + self + /// .children + /// .iter() + /// .map(|node| node.seq_num_nodes()) + /// .sum::() + /// } + /// + /// fn seq_sum_fib(&self) -> u64 { + /// self.value.iter().map(|x| fibonacci(*x)).sum::() + /// + self.children.iter().map(|x| x.seq_sum_fib()).sum::() + /// } + /// } + /// + /// fn fibonacci(n: u64) -> u64 { + /// let mut a = 0; + /// let mut b = 1; + /// for _ in 0..n { + /// let c = a + b; + /// a = b; + /// b = c; + /// } + /// a + /// } + /// + /// // # usage + /// + /// // this defines how the iterator must extend: + /// // each node drawn from the iterator adds its children to the end of the iterator + /// fn extend<'a, 'b>(node: &'a &'b Node, queue: &Queue<&'b Node>) { + /// queue.extend(&node.children); + /// } + /// + /// let mut rng = ChaCha8Rng::seed_from_u64(42); + /// let roots = vec![ + /// Node::new(50, &mut rng), + /// Node::new(20, &mut rng), + /// Node::new(40, &mut rng), + /// ]; + /// + /// let seq_sum: u64 = roots.iter().map(|x| x.seq_sum_fib()).sum(); + /// + /// // A. exact length, recommended when possible + /// + /// let count: usize = roots.iter().map(|x| x.seq_num_nodes()).sum(); + /// + /// let sum = (&roots) + /// .into_par_rec_exact(extend, count) + /// .map(|x| x.value.iter().map(|x| fibonacci(*x)).sum::()) + /// .sum(); + /// assert_eq!(sum, seq_sum); + /// + /// // B. guide the computation with chunk size, when length is unknown + /// + /// let sum = (&roots) + /// .into_par_rec(extend) + /// .chunk_size(1024) + /// .map(|x| x.value.iter().map(|x| fibonacci(*x)).sum::()) + /// .sum(); + /// assert_eq!(sum, seq_sum); + /// + /// // C. eagerly convert to a flat iterator + /// + /// let sum = (&roots) + /// .into_par_rec(extend) + /// .into_eager() + /// .map(|x| x.value.iter().map(|x| fibonacci(*x)).sum::()) + /// .sum(); + /// assert_eq!(sum, seq_sum); + /// ``` fn into_par_rec_exact( self, extend: E, From c3af9aabe78d301bd9f751f6f24bbf56ddf9a5cc Mon Sep 17 00:00:00 2001 From: orxfun Date: Fri, 24 Oct 2025 12:20:26 +0200 Subject: [PATCH 64/96] details and hints on how to extend --- src/iter/recursive/into_par_rec_iter.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/iter/recursive/into_par_rec_iter.rs b/src/iter/recursive/into_par_rec_iter.rs index 61bb6f17..9743c3fa 100644 --- a/src/iter/recursive/into_par_rec_iter.rs +++ b/src/iter/recursive/into_par_rec_iter.rs @@ -48,6 +48,11 @@ where /// * `extend(items)` allows us to add all of the items to the queue. Here `items` must have a known /// size (`ExactSizeIterator`). /// + /// Adding children one-by-one with `push` or all together with `extend` might be the extreme options. + /// Actually, any intermediate approach is also possible. For instance, we can choose to `extend` in + /// chunks of say 50 tasks. If the item happens to create 140 children, we can handle this with four + /// `extend` calls. + /// /// Using either of the methods might be beneficial for different use cases. /// /// Pushing children one by one makes the new task available for other threads as fast as possible. Further, @@ -250,6 +255,11 @@ where /// * `extend(items)` allows us to add all of the items to the queue. Here `items` must have a known /// size (`ExactSizeIterator`). /// + /// Adding children one-by-one with `push` or all together with `extend` might be the extreme options. + /// Actually, any intermediate approach is also possible. For instance, we can choose to `extend` in + /// chunks of say 50 tasks. If the item happens to create 140 children, we can handle this with four + /// `extend` calls. + /// /// Using either of the methods might be beneficial for different use cases. /// /// Pushing children one by one makes the new task available for other threads as fast as possible. Further, From b866942535bc515dccdeee167ab3734e936de56e Mon Sep 17 00:00:00 2001 From: orxfun Date: Fri, 24 Oct 2025 12:58:13 +0200 Subject: [PATCH 65/96] clippy and doc fixes --- src/iter/recursive/into_par_rec_iter.rs | 11 ++++------- src/iter/recursive/mod.rs | 2 +- .../recursive/{rec_per_iter.rs => rec_par_iter.rs} | 6 +++--- 3 files changed, 8 insertions(+), 11 deletions(-) rename src/iter/recursive/{rec_per_iter.rs => rec_par_iter.rs} (94%) diff --git a/src/iter/recursive/into_par_rec_iter.rs b/src/iter/recursive/into_par_rec_iter.rs index 9743c3fa..38a6582a 100644 --- a/src/iter/recursive/into_par_rec_iter.rs +++ b/src/iter/recursive/into_par_rec_iter.rs @@ -5,18 +5,15 @@ use orx_concurrent_recursive_iter::{ConcurrentRecursiveIter, Queue}; /// Trait to convert an iterator into a recursive parallel iterator together with the `extend` method. /// -/// Created parallel iterator is a regular parallel iterator; i.e., we have access to -/// all [`ParIter`] features. +/// Created parallel iterator is a regular parallel iterator; i.e., we have access to all [`ParIter`] features. /// /// It is recursive due to the extension. The recursive parallel iterator will yield /// * all initial elements contained in this iterator, -/// * all elements created by calling `extend` on each of the initial elements, let's call these depth-1 elements, -/// * all elements created by calling `extend` on each of the depth-1 elements, let's call these depth-2 elements, -/// * ..., and so on. +/// * all elements dynamically added to the queue with the `extend` method while processing the elements. /// -/// You may read more about the [`ConcurrentRecursiveIterCore`]. +/// You may read more about the [`ConcurrentRecursiveIter`]. /// -/// See also [`IntoParIterRecExact`] +/// [`ParIter`]: crate::ParIter pub trait IntoParIterRec where Self: IntoIterator, diff --git a/src/iter/recursive/mod.rs b/src/iter/recursive/mod.rs index 93e0ce01..2e1bd63a 100644 --- a/src/iter/recursive/mod.rs +++ b/src/iter/recursive/mod.rs @@ -1,4 +1,4 @@ mod into_par_rec_iter; -mod rec_per_iter; +mod rec_par_iter; pub use into_par_rec_iter::IntoParIterRec; diff --git a/src/iter/recursive/rec_per_iter.rs b/src/iter/recursive/rec_par_iter.rs similarity index 94% rename from src/iter/recursive/rec_per_iter.rs rename to src/iter/recursive/rec_par_iter.rs index 4153ae13..efefe4a5 100644 --- a/src/iter/recursive/rec_per_iter.rs +++ b/src/iter/recursive/rec_par_iter.rs @@ -25,7 +25,7 @@ where /// See [`into_par_rec`] and [`into_par_rec_exact`] for examples. /// /// [`into_par_rec`]: crate::IntoParIterRec::into_par_rec - /// [`into_par_rec_exact`]: crate::IntoParIterRecExact::into_par_rec_exact + /// [`into_par_rec_exact`]: crate::IntoParIterRec::into_par_rec_exact pub fn into_eager(self) -> Par, R> { let (orchestrator, params, iter) = self.destruct(); let items = collect_items(iter); @@ -52,7 +52,7 @@ where /// See [`into_par_rec`] and [`into_par_rec_exact`] for examples. /// /// [`into_par_rec`]: crate::IntoParIterRec::into_par_rec - /// [`into_par_rec_exact`]: crate::IntoParIterRecExact::into_par_rec_exact + /// [`into_par_rec_exact`]: crate::IntoParIterRec::into_par_rec_exact pub fn into_eager(self) -> ParMap, O, M1, R> { let (orchestrator, params, iter, map1) = self.destruct(); let items = collect_items(iter); @@ -80,7 +80,7 @@ where /// See [`into_par_rec`] and [`into_par_rec_exact`] for examples. /// /// [`into_par_rec`]: crate::IntoParIterRec::into_par_rec - /// [`into_par_rec_exact`]: crate::IntoParIterRecExact::into_par_rec_exact + /// [`into_par_rec_exact`]: crate::IntoParIterRec::into_par_rec_exact pub fn into_eager(self) -> ParXap, Vo, X1, R> { let (orchestrator, params, iter, xap1) = self.destruct(); let items = collect_items(iter); From b4206e5666cd43d6cad4b8210b01e4412d672bae Mon Sep 17 00:00:00 2001 From: orxfun Date: Fri, 24 Oct 2025 12:59:10 +0200 Subject: [PATCH 66/96] update concurrent recursive iter dependency to the wip branch --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index eebd20c2..bfac6d87 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ orx-iterable = { version = "1.3.0", default-features = false } orx-priority-queue = { version = "1.7.0", default-features = false } orx-pseudo-default = { version = "2.1.0", default-features = false } # orx-concurrent-recursive-iter = { version = "1.1.0", default-features = false } -orx-concurrent-recursive-iter = { path = "../orx-concurrent-recursive-iter", default-features = false } +orx-concurrent-recursive-iter = { git = "https://github.com/orxfun/orx-concurrent-recursive-iter", branch = "more-flexible-extension", default-features = false } # optional: generic iterator rayon = { version = "1.11.0", optional = true, default-features = false } From afbd7ffe4227751626c64bbe5eb3c0f016542550 Mon Sep 17 00:00:00 2001 From: orxfun Date: Sun, 26 Oct 2025 01:29:36 +0200 Subject: [PATCH 67/96] update docs --- src/iter/recursive/into_par_rec_iter.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/iter/recursive/into_par_rec_iter.rs b/src/iter/recursive/into_par_rec_iter.rs index 38a6582a..44ad1ef8 100644 --- a/src/iter/recursive/into_par_rec_iter.rs +++ b/src/iter/recursive/into_par_rec_iter.rs @@ -32,7 +32,7 @@ where /// # Examples /// /// The following example has some code to set up until the `# usage` line. Notice that the `Node` - /// is a non-linear data structure, each node having children nodes to be recursively processed. + /// is a **non-linear** data structure, each node having children nodes to be recursively processed. /// /// We have three initial elements `roots`. /// @@ -239,7 +239,7 @@ where /// # Examples /// /// The following example has some code to set up until the `# usage` line. Notice that the `Node` - /// is a non-linear data structure, each node having children nodes to be recursively processed. + /// is a **non-linear** data structure, each node having children nodes to be recursively processed. /// /// We have three initial elements `roots`. /// From 088de1c52a701377970fdd1e664e002306f5cc46 Mon Sep 17 00:00:00 2001 From: orxfun Date: Sun, 26 Oct 2025 19:52:51 +0100 Subject: [PATCH 68/96] recursive parallelization example --- .../collection_on_entire_tree.rs | 122 +++++++++++++++ examples/parallelization_on_tree/main.rs | 50 ++++++ examples/parallelization_on_tree/node.rs | 35 +++++ .../reduction_on_entire_tree.rs | 148 ++++++++++++++++++ .../reduction_on_subset_of_tree.rs | 109 +++++++++++++ examples/parallelization_on_tree/run_utils.rs | 36 +++++ examples/parallelization_on_tree/tree.rs | 62 ++++++++ examples/rec_iter_map_collect.rs | 4 +- 8 files changed, 564 insertions(+), 2 deletions(-) create mode 100644 examples/parallelization_on_tree/collection_on_entire_tree.rs create mode 100644 examples/parallelization_on_tree/main.rs create mode 100644 examples/parallelization_on_tree/node.rs create mode 100644 examples/parallelization_on_tree/reduction_on_entire_tree.rs create mode 100644 examples/parallelization_on_tree/reduction_on_subset_of_tree.rs create mode 100644 examples/parallelization_on_tree/run_utils.rs create mode 100644 examples/parallelization_on_tree/tree.rs diff --git a/examples/parallelization_on_tree/collection_on_entire_tree.rs b/examples/parallelization_on_tree/collection_on_entire_tree.rs new file mode 100644 index 00000000..5254e866 --- /dev/null +++ b/examples/parallelization_on_tree/collection_on_entire_tree.rs @@ -0,0 +1,122 @@ +use crate::run_utils::timed; +use orx_parallel::*; + +type Node = crate::node::Node; + +pub fn run(root: &Node) { + println!("\n\n\n\n"); + println!( + r#"# COLLECTION ON ENTIRE TREE + +This example is almost the same as the "reduction" example. + +The only difference is that instead of computing the some of mapped values, +we collect all mapped values in a vector. + +This demonstrates the fact that a "parallel recursive iterator" is nothing but +a "parallel iterator" with access to all `ParIter` methods. + +In order to change the computation from reduction to collection, +all we need to do is to change + +[root].into_par_rec(extend).map(compute).sum() + +into + +[root].into_par_rec(extend).map(compute).collect() + "# + ); + + let log = |vec: Vec| println!(" collection-len = {:?}", vec.len()); + + timed("sequential", || sequential(root), log); + timed("orx_rec", || orx_rec(root), log); + timed("orx_rec_eager", || orx_rec_eager(root), log); + timed("orx_rec_exact", || orx_rec_exact(root), log); + + println!(); +} + +/// Just a demo computation we perform for each node. +fn compute(node: &Node) -> u64 { + crate::run_utils::compute(node.data.parse::().unwrap()) +} + +/// # sequential +/// +/// This is a recursive sequential implementation to compute and reduce values of +/// all nodes descending from the root. +fn sequential(root: &Node) -> Vec { + fn seq_compute_node(node: &Node, result: &mut Vec) { + let node_value = compute(node); + result.push(node_value); + + for child in &node.children { + seq_compute_node(child, result); + } + } + + let mut result = vec![]; + seq_compute_node(root, &mut result); + result +} + +/// # orx-parallel: parallel recursive iterator with unknown length +/// +/// Here we parallelize by providing the `extend` function. +/// +/// Although we don't use it here, please consider `chunk_size` +/// optimization depending on the data whenever necessary. This might +/// be more important in non-linear data structures compared to linear +/// due to the dynamic nature of iteration. +fn orx_rec(root: &Node) -> Vec { + fn extend<'a, 'b>(node: &'a &'b Node, queue: &Queue<&'b Node>) { + queue.extend(&node.children); + } + + [root].into_par_rec(extend).map(compute).collect() +} + +/// # orx-parallel: parallel recursive iterator with unknown length +/// +/// Here we parallelize by providing the `extend` function. +/// +/// However, rather than parallel processing over a dynamic recursive +/// input, the iterator first flattens the tasks with the `into_eager` +/// call and then operates on it as if it is over a linear data structure. +fn orx_rec_eager(root: &Node) -> Vec { + fn extend<'a, 'b>(node: &'a &'b Node, queue: &Queue<&'b Node>) { + queue.extend(&node.children); + } + + [root] + .into_par_rec(extend) + .into_eager() + .map(compute) + .collect() +} + +/// # orx-parallel: parallel recursive iterator with exact length +/// +/// Here we parallelize by providing the `extend` function. +/// Further, we precompute the total number of children and provide it while creating +/// the parallel iterator. This is helpful to optimize parallel execution whenever +/// it is available and cheap to compute. +/// +/// Good thing, we can also count the number of nodes in parallel. +/// +/// On the other hand, it is good not to keep the chunk size too large in a recursive +/// iterator, as we limit it to 32 in the following. +fn orx_rec_exact(root: &Node) -> Vec { + fn extend<'a, 'b>(node: &'a &'b Node, queue: &Queue<&'b Node>) { + queue.extend(&node.children); + } + + let num_nodes = [root].into_par_rec(extend).count(); + + [root] + .into_par_rec_exact(extend, num_nodes) + .chunk_size(32) + .map(compute) + .collect() +} diff --git a/examples/parallelization_on_tree/main.rs b/examples/parallelization_on_tree/main.rs new file mode 100644 index 00000000..6ea03142 --- /dev/null +++ b/examples/parallelization_on_tree/main.rs @@ -0,0 +1,50 @@ +use crate::tree::Tree; +use clap::Parser; +use rand::SeedableRng; +use rand_chacha::ChaCha8Rng; +use std::sync::OnceLock; + +mod collection_on_entire_tree; +mod node; +mod reduction_on_entire_tree; +mod reduction_on_subset_of_tree; +mod run_utils; +mod tree; + +#[derive(Parser, Debug)] +struct Args { + /// Amount of work (num times Fibonacci will be repeated). + #[arg(long, default_value_t = 10)] + amount_of_work: usize, +} + +pub fn amount_of_work() -> &'static usize { + static WORK: OnceLock = OnceLock::new(); + WORK.get_or_init(|| Args::parse().amount_of_work) +} + +fn main() { + let num_nodes = 100_000; + let out_degree = 0..100; + + let mut rng = ChaCha8Rng::seed_from_u64(42); + + let data = |idx: usize| idx.to_string(); + let root = Tree::new(num_nodes, out_degree, data, &mut rng); + + println!("\nOne path from root to a leaf as an example"); + let mut next_node = Some(&root); + let mut i = 0; + while let Some(node) = next_node { + let indent: String = (0..i).map(|_| '*').collect(); + println!("{indent}{node:?}"); + i += 1; + next_node = node.children.iter().max_by_key(|x| x.children.len()); + } + + println!("\nTotal number of nodes = {}", root.num_nodes()); + + reduction_on_entire_tree::run(&root); + collection_on_entire_tree::run(&root); + reduction_on_subset_of_tree::run(&root); +} diff --git a/examples/parallelization_on_tree/node.rs b/examples/parallelization_on_tree/node.rs new file mode 100644 index 00000000..1f724060 --- /dev/null +++ b/examples/parallelization_on_tree/node.rs @@ -0,0 +1,35 @@ +use std::fmt::Debug; + +pub struct Node { + pub idx: usize, + pub data: T, + pub children: Vec>, +} + +impl Debug for Node { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Node") + .field("idx", &self.idx) + .field("num_children", &self.children.len()) + .field( + "children_idx", + &self + .children + .iter() + .take(10) + .map(|x| x.idx) + .collect::>(), + ) + .finish() + } +} + +impl Node { + pub fn num_nodes(&self) -> usize { + 1 + self + .children + .iter() + .map(|node| node.num_nodes()) + .sum::() + } +} diff --git a/examples/parallelization_on_tree/reduction_on_entire_tree.rs b/examples/parallelization_on_tree/reduction_on_entire_tree.rs new file mode 100644 index 00000000..9a282760 --- /dev/null +++ b/examples/parallelization_on_tree/reduction_on_entire_tree.rs @@ -0,0 +1,148 @@ +use crate::run_utils::timed; +use orx_parallel::*; +use std::sync::atomic::{AtomicU64, Ordering}; + +type Node = crate::node::Node; + +pub fn run(root: &Node) { + println!("\n\n\n\n"); + println!( + r#"# REDUCTION ON ENTIRE TREE + +This example demonstrates parallel computation over a tree. +Unlike parallelization over linear data structures, we don't have access to all +input elements, or say tasks, ahead of time. + +Instead, the new elements are added dynamically on the fly. +Therefore, these iterators are called "parallel recursive iterator"s. + +In addition to an initial set of elements, a parallel recursive iterator is +created with an "extend" function which defines the recursive behavior. + +In this example we create an iterator where elements are "&Node". +We define the "extend" function as follows: + +fn extend<'a, 'b>(node: &'a &'b Node, queue: &Queue<&'b Node>) {{ + queue.extend(&node.children); +}} + +While processing a particular "node", we add all its children to the "queue". +In this example, we use "queue.extend", later we will also use "queue.push". + +This allows to express the parallel computation as simple as over a linear +data structure: + +[root].into_par_rec(extend).map(compute).sum() + "# + ); + + let log = |sum: u64| println!(" sum = {sum}"); + + timed("sequential", || sequential(root), log); + + // rayon miri fails with: + // Undefined Behavior: trying to retag from <84156795> for SharedReadWrite permission at alloc41643328[0x8], + // but that tag does not exist in the borrow stack for this location + #[cfg(not(miri))] + timed("rayon", || rayon(root), log); + + timed("orx_rec", || orx_rec(root), log); + timed("orx_rec_eager", || orx_rec_eager(root), log); + timed("orx_rec_exact", || orx_rec_exact(root), log); + + println!(); +} + +/// Just a demo computation we perform for each node. +fn compute(node: &Node) -> u64 { + crate::run_utils::compute(node.data.parse::().unwrap()) +} + +/// # sequential +/// +/// This is a recursive sequential implementation to compute and reduce values of +/// all nodes descending from the root. +fn sequential(root: &Node) -> u64 { + fn seq_compute_node(node: &Node) -> u64 { + let node_value = compute(node); + let child_values = node.children.iter().map(|x| seq_compute_node(x)); + node_value + child_values.sum::() + } + + seq_compute_node(root) +} + +/// # rayon: defining the computation with rayon's scoped threads. +pub fn rayon(root: &Node) -> u64 { + fn process_node<'scope>(sum: &'scope AtomicU64, node: &'scope Node, s: &rayon::Scope<'scope>) { + for child in &node.children { + s.spawn(move |s| { + process_node(sum, child, s); + }); + } + let node_value = compute(node); + sum.fetch_add(node_value, Ordering::Relaxed); + } + + let sum = AtomicU64::new(0); + rayon::in_place_scope(|s| { + process_node(&sum, root, s); + }); + sum.into_inner() +} + +/// # orx-parallel: parallel recursive iterator with unknown length +/// +/// Here we parallelize by providing the `extend` function. +/// +/// Although we don't use it here, please consider `chunk_size` +/// optimization depending on the data whenever necessary. This might +/// be more important in non-linear data structures compared to linear +/// due to the dynamic nature of iteration. +fn orx_rec(root: &Node) -> u64 { + fn extend<'a, 'b>(node: &'a &'b Node, queue: &Queue<&'b Node>) { + queue.extend(&node.children); + } + + [root].into_par_rec(extend).map(compute).sum() +} + +/// # orx-parallel: parallel recursive iterator with unknown length +/// +/// Here we parallelize by providing the `extend` function. +/// +/// However, rather than parallel processing over a dynamic recursive +/// input, the iterator first flattens the tasks with the `into_eager` +/// call and then operates on it as if it is over a linear data structure. +fn orx_rec_eager(root: &Node) -> u64 { + fn extend<'a, 'b>(node: &'a &'b Node, queue: &Queue<&'b Node>) { + queue.extend(&node.children); + } + + [root].into_par_rec(extend).into_eager().map(compute).sum() +} + +/// # orx-parallel: parallel recursive iterator with exact length +/// +/// Here we parallelize by providing the `extend` function. +/// Further, we precompute the total number of children and provide it while creating +/// the parallel iterator. This is helpful to optimize parallel execution whenever +/// it is available and cheap to compute. +/// +/// Good thing, we can also count the number of nodes in parallel. +/// +/// On the other hand, it is good not to keep the chunk size too large in a recursive +/// iterator, as we limit it to 32 in the following. +fn orx_rec_exact(root: &Node) -> u64 { + fn extend<'a, 'b>(node: &'a &'b Node, queue: &Queue<&'b Node>) { + queue.extend(&node.children); + } + + let num_nodes = [root].into_par_rec(extend).count(); + + [root] + .into_par_rec_exact(extend, num_nodes) + .chunk_size(32) + .map(compute) + .sum() +} diff --git a/examples/parallelization_on_tree/reduction_on_subset_of_tree.rs b/examples/parallelization_on_tree/reduction_on_subset_of_tree.rs new file mode 100644 index 00000000..bec2016f --- /dev/null +++ b/examples/parallelization_on_tree/reduction_on_subset_of_tree.rs @@ -0,0 +1,109 @@ +use crate::run_utils::timed; +use orx_parallel::*; + +type Node = crate::node::Node; + +pub fn run(root: &Node) { + println!("\n\n\n\n"); + println!( + r#"# REDUCTION ON SUBSET OF THE TREE + +In the previous examples we used "queue.extend" method to dynamically add children +to the queue. + +However, this method requires the children to implement 'ExactSizeIterator'. +When we don't have pre-allocated children, or when we apply a filter on these children, +we cannot always satisfy this requirement. + +In these cases, + +* we can use `queue.push(child)` to add children one-by-one; or +* we can collect children into a vec and then use `queue.extend(children_vec)` to add them + together. + +`queue.push` approach has the following pros and cons: +* (+) makes new children available as soon as available. +* (+) does not require allocation. +* (-) might have greater parallelization overhead. + +`queue.extend` approach has the following pros and cons: +* (+) will have the minimum parallelization overhead. +* (-) requires allocation for processing each node. + +These are a couple of recommendations, we can use `push` and `extend` methods in a different +way to optimize our use case. + "# + ); + + println!("\n\n\n\n# REDUCTION ON SUBSET OF THE TREE"); + let log = |sum: u64| println!(" sum = {sum}"); + + timed("sequential", || sequential(root), log); + + timed("push_orx_rec", || push_orx_rec(root), log); + timed( + "collect_extend_orx_rec", + || collect_extend_orx_rec(root), + log, + ); + + println!(); +} + +/// Just a demo computation we perform for each node. +fn compute(node: &Node) -> u64 { + crate::run_utils::compute(node.data.parse::().unwrap()) +} + +fn filter(node: &&Node) -> bool { + !node.data.parse::().unwrap().is_multiple_of(42) +} + +/// # sequential +/// +/// This is a recursive sequential implementation to compute and reduce values of +/// all nodes descending from the root. +fn sequential(root: &Node) -> u64 { + fn seq_compute_node(node: &Node) -> u64 { + let node_value = compute(node); + let child_values = node.children.iter().filter(filter).map(seq_compute_node); + node_value + child_values.sum::() + } + + seq_compute_node(root) +} + +/// # orx-parallel: parallel recursive iterator with unknown length +/// +/// Since we do not know how many children to add ahead-of-time, we +/// don't have an ExactSizeIterator. Therefore, instead of `queue.extend`, +/// we use `queue.push` to add new children. +/// +/// * (+) makes new children available as soon as available. +/// * (+) does not require allocation. +/// * (-) might have greater parallelization overhead. +fn push_orx_rec(root: &Node) -> u64 { + fn extend<'a, 'b>(node: &'a &'b Node, queue: &Queue<&'b Node>) { + for child in node.children.iter().filter(filter) { + queue.push(child); + } + } + + [root].into_par_rec(extend).map(compute).sum() +} + +/// # orx-parallel: parallel recursive iterator with unknown length +/// +/// Alternatively, we can collect children in a vector and then call +/// `queue.extend` to add the new children. +/// +/// * (+) will have the minimum parallelization overhead. +/// * (-) requires allocation for processing each node. +fn collect_extend_orx_rec(root: &Node) -> u64 { + fn extend<'a, 'b>(node: &'a &'b Node, queue: &Queue<&'b Node>) { + let children: Vec<_> = node.children.iter().filter(filter).collect(); + queue.extend(children); + } + + [root].into_par_rec(extend).map(compute).sum() +} diff --git a/examples/parallelization_on_tree/run_utils.rs b/examples/parallelization_on_tree/run_utils.rs new file mode 100644 index 00000000..f505951b --- /dev/null +++ b/examples/parallelization_on_tree/run_utils.rs @@ -0,0 +1,36 @@ +use crate::amount_of_work; +use std::time::Instant; + +pub fn timed(name: &'static str, fun: F, log: L) +where + F: Fn() -> T, + L: Fn(T), +{ + println!("> {name}"); + let start = Instant::now(); + + let result = fun(); + + let elapsed = start.elapsed(); + + println!(" elapsed = {elapsed:?}"); + log(result); + println!(); +} + +/// Fibonacci as example computation on each of the node values. +pub fn compute(value: u64) -> u64 { + (0..*amount_of_work()) + .map(|j| { + let n = core::hint::black_box(value + j as u64); + let mut a = 0; + let mut b = 1; + for _ in 0..n { + let c = a + b; + a = b; + b = c; + } + a + }) + .sum() +} diff --git a/examples/parallelization_on_tree/tree.rs b/examples/parallelization_on_tree/tree.rs new file mode 100644 index 00000000..b2537b7e --- /dev/null +++ b/examples/parallelization_on_tree/tree.rs @@ -0,0 +1,62 @@ +use crate::node::Node; +use rand::Rng; +use std::{collections::HashSet, marker::PhantomData, ops::Range}; + +pub struct Tree(PhantomData); + +impl Tree { + pub fn new( + num_nodes: usize, + degree: Range, + data: fn(usize) -> T, + rng: &mut impl Rng, + ) -> Node { + assert!(num_nodes >= 2); + + let mut leaves = vec![0]; + let mut remaining: Vec<_> = (1..num_nodes).collect(); + let mut edges = vec![]; + let mut out_edges = vec![vec![]; num_nodes]; + + while !remaining.is_empty() { + let leaf_idx = rng.random_range(0..leaves.len()); + let leaf = leaves.remove(leaf_idx); + + let degree = rng.random_range(degree.clone()); + match degree == 0 { + true => leaves.push(leaf), + false => { + let children_indices: HashSet<_> = (0..degree) + .map(|_| rng.random_range(0..remaining.len())) + .collect(); + + let mut sorted: Vec<_> = children_indices.iter().copied().collect(); + sorted.sort(); + + edges.extend(children_indices.iter().map(|c| (leaf, remaining[*c]))); + out_edges[leaf] = children_indices.iter().map(|c| remaining[*c]).collect(); + leaves.extend(children_indices.iter().map(|c| remaining[*c])); + + for idx in sorted.into_iter().rev() { + remaining.remove(idx); + } + } + } + } + + create_node(&out_edges, 0, data) + } +} + +fn create_node(out_edges: &[Vec], idx: usize, data: fn(usize) -> T) -> Node { + let children: Vec<_> = out_edges[idx] + .iter() + .map(|child_idx| create_node(out_edges, *child_idx, data)) + .collect(); + let data = data(idx); + Node { + idx, + data, + children, + } +} diff --git a/examples/rec_iter_map_collect.rs b/examples/rec_iter_map_collect.rs index bebdb06d..0518506a 100644 --- a/examples/rec_iter_map_collect.rs +++ b/examples/rec_iter_map_collect.rs @@ -106,12 +106,12 @@ fn main() { } expected.sort(); - println!("\n\n# par_rec"); + println!("\n\n\n\n# par_rec"); let mut result = par_rec(&roots).to_vec(); result.sort(); assert_eq!(result, expected); - println!("\n\n# par_rec_eager"); + println!("\n\n\n\n# par_rec_eager"); let mut result = par_rec_eager(&roots).to_vec(); result.sort(); assert_eq!(result, expected); From 0a2af9b73a003db43d847c6d1d21fc6c98c15cdf Mon Sep 17 00:00:00 2001 From: orxfun Date: Sun, 26 Oct 2025 19:53:09 +0100 Subject: [PATCH 69/96] clean up prior examples --- examples/rec_iter_map_collect.rs | 118 ------------------------------- examples/rec_iter_map_sum.rs | 110 ---------------------------- 2 files changed, 228 deletions(-) delete mode 100644 examples/rec_iter_map_collect.rs delete mode 100644 examples/rec_iter_map_sum.rs diff --git a/examples/rec_iter_map_collect.rs b/examples/rec_iter_map_collect.rs deleted file mode 100644 index 0518506a..00000000 --- a/examples/rec_iter_map_collect.rs +++ /dev/null @@ -1,118 +0,0 @@ -use orx_concurrent_recursive_iter::Queue; -use orx_parallel::*; -use orx_split_vec::SplitVec; -use rand::{Rng, SeedableRng}; -use rand_chacha::ChaCha8Rng; - -fn fibonacci(n: u64) -> u64 { - let mut a = 0; - let mut b = 1; - for _ in 0..n { - let c = a + b; - a = b; - b = c; - } - a -} - -struct Node { - value: Vec, - children: Vec, -} - -impl Node { - fn new(mut n: u32, rng: &mut impl Rng) -> Self { - let mut children = Vec::new(); - if n < 5 { - for _ in 0..n { - children.push(Node::new(0, rng)); - } - } else { - while n > 0 { - let n2 = rng.random_range(0..=n); - children.push(Node::new(n2, rng)); - n -= n2; - } - } - Self { - value: (0..rng.random_range(1..500)) - .map(|_| rng.random_range(0..40)) - .collect(), - children, - } - } - - fn seq_num_nodes(&self) -> usize { - 1 + self - .children - .iter() - .map(|node| node.seq_num_nodes()) - .sum::() - } - - fn seq(&self, numbers: &mut Vec) { - numbers.extend(self.value.iter().map(|x| fibonacci(*x))); - for c in &self.children { - c.seq(numbers); - } - } -} - -fn par_rec(roots: &[Node]) -> SplitVec { - fn extend<'a, 'b>(node: &'a &'b Node, queue: &Queue<&'b Node>) { - queue.extend(&node.children); - } - let count: usize = roots.iter().map(|x| x.seq_num_nodes()).sum(); - - let runner = DefaultRunner::default().with_diagnostics(); - - roots - .into_par_rec_exact(extend, count) - .with_runner(runner) - .flat_map(|x| x.value.iter().map(|x| fibonacci(*x))) - .collect() -} - -fn par_rec_eager(roots: &[Node]) -> SplitVec { - fn extend<'a, 'b>(node: &'a &'b Node, queue: &Queue<&'b Node>) { - queue.extend(&node.children); - } - - let runner = DefaultRunner::default().with_diagnostics(); - - roots - .into_par_rec(extend) - .into_eager() - .with_runner(runner) - .flat_map(|x| x.value.iter().map(|x| fibonacci(*x))) - .collect() -} - -fn main() { - println!("\n\n"); - let mut rng = ChaCha8Rng::seed_from_u64(42); - let roots = vec![ - Node::new(5000, &mut rng), - Node::new(2000, &mut rng), - Node::new(4000, &mut rng), - ]; - - let count: usize = roots.iter().map(|x| x.seq_num_nodes()).sum(); - println!("Tree contains {count} nodes"); - - let mut expected = vec![]; - for root in &roots { - root.seq(&mut expected); - } - expected.sort(); - - println!("\n\n\n\n# par_rec"); - let mut result = par_rec(&roots).to_vec(); - result.sort(); - assert_eq!(result, expected); - - println!("\n\n\n\n# par_rec_eager"); - let mut result = par_rec_eager(&roots).to_vec(); - result.sort(); - assert_eq!(result, expected); -} diff --git a/examples/rec_iter_map_sum.rs b/examples/rec_iter_map_sum.rs deleted file mode 100644 index 25f700d5..00000000 --- a/examples/rec_iter_map_sum.rs +++ /dev/null @@ -1,110 +0,0 @@ -use orx_concurrent_recursive_iter::Queue; -use orx_parallel::*; -use rand::{Rng, SeedableRng}; -use rand_chacha::ChaCha8Rng; - -fn fibonacci(n: u64) -> u64 { - let mut a = 0; - let mut b = 1; - for _ in 0..n { - let c = a + b; - a = b; - b = c; - } - a -} - -struct Node { - value: Vec, - children: Vec, -} - -impl Node { - fn new(mut n: u32, rng: &mut impl Rng) -> Self { - let mut children = Vec::new(); - if n < 5 { - for _ in 0..n { - children.push(Node::new(0, rng)); - } - } else { - while n > 0 { - let n2 = rng.random_range(0..=n); - children.push(Node::new(n2, rng)); - n -= n2; - } - } - Self { - value: (0..rng.random_range(1..500)) - .map(|_| rng.random_range(0..40)) - .collect(), - children, - } - } - - fn seq_num_nodes(&self) -> usize { - 1 + self - .children - .iter() - .map(|node| node.seq_num_nodes()) - .sum::() - } - - fn seq_sum_fib(&self) -> u64 { - self.value.iter().map(|x| fibonacci(*x)).sum::() - + self.children.iter().map(|x| x.seq_sum_fib()).sum::() - } -} - -fn par_rec(roots: &[Node]) -> u64 { - fn extend<'a, 'b>(node: &'a &'b Node, queue: &Queue<&'b Node>) { - queue.extend(&node.children); - } - - let count: usize = roots.iter().map(|x| x.seq_num_nodes()).sum(); - - let runner = DefaultRunner::default().with_diagnostics(); - - roots - .into_par_rec_exact(extend, count) - .with_runner(runner) - .map(|x| x.value.iter().map(|x| fibonacci(*x)).sum::()) - .sum() -} - -fn par_rec_eager(roots: &[Node]) -> u64 { - fn extend<'a, 'b>(node: &'a &'b Node, queue: &Queue<&'b Node>) { - queue.extend(&node.children); - } - - let runner = DefaultRunner::default().with_diagnostics(); - - roots - .into_par_rec(extend) - .into_eager() - .with_runner(runner) - .map(|x| x.value.iter().map(|x| fibonacci(*x)).sum::()) - .sum() -} - -fn main() { - println!("\n\n"); - let mut rng = ChaCha8Rng::seed_from_u64(42); - let roots = vec![ - Node::new(500, &mut rng), - Node::new(200, &mut rng), - Node::new(400, &mut rng), - ]; - - let count: usize = roots.iter().map(|x| x.seq_num_nodes()).sum(); - println!("Tree contains {count} nodes"); - - let expected: u64 = roots.iter().map(|x| x.seq_sum_fib()).sum(); - - let sum_fib = par_rec_eager(&roots); - assert_eq!(sum_fib, expected); - println!("Sum of Fibonacci of node values is {sum_fib}"); - - let sum_fib = par_rec(&roots); - assert_eq!(sum_fib, expected); - println!("Sum of Fibonacci of node values is {sum_fib}"); -} From 1bd33c6127fe2f36c553041046fa0760b4558768 Mon Sep 17 00:00:00 2001 From: orxfun Date: Sun, 26 Oct 2025 20:10:37 +0100 Subject: [PATCH 70/96] update readme --- README.md | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/README.md b/README.md index 1158daf0..037782a5 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ * [Parallel Computation by Iterators](#parallel-computation-by-iterators) * [Parallelizable Collections](#parallelizable-collections) +* [Parallelization over Nonlinear Data Structures](#parallelization-over-nonlinear-data-structures) * [Performance and Benchmarks](#performance-and-benchmarks) * [Fallible Parallel Iterators](#fallible-parallel-iterators) * [Using Mutable Variables](#using-mutable-variables) @@ -150,6 +151,41 @@ The following table demonstrates these methods for the `HashSet`; however, they Note that each approach can be more efficient in different scenarios. For large elements, (ii) might be preferred to avoid allocation of the vector. For insignificant tasks to be performed on each element, (i) might be preferred to take full benefit of vector-specific optimizations. +## Parallelization over Nonlinear Data Structures + +[IntoParIterRec](https://docs.rs/orx-parallel/latest/orx_parallel/trait.IntoParIterRec.html) trait can be used to create a **parallel recursive iterator** over an initial set of elements which is useful when working with non-linear data structures such as **trees** and **graphs**. + +Consider, for instance, a tree which is defined by the following node struct: + +```rust ignore +pub struct Node { + pub data: T, + pub children: Vec>, +} +``` + +Assume that we want to map all the data with `map: impl Fn(T) -> u64` and compute the sum of mapped values of all nodes descending from a `root: &Node`. + +We can express this computation and execute in parallel with the following: + +```rust ignore +fn extend<'a, 'b>(node: &'a &'b Node, queue: &Queue<&'b Node>) { + queue.extend(&node.children); +} + +[root].into_par_rec(extend).map(map).sum() +``` + +Instead of `into_par`, we use `into_par_rec` and provide `extend` function as its argument. This function defines the recursive extension of the parallel iterator such that every time we process a `node` we first add its children to the `queue`. `Queue` is the queue of elements to be processed and it exposes two growth methods: `push` and `extend` that we can use to define the recursive extension. + +Although we create the parallel iterator differently, we get a `ParIter`. Therefore, we have access to all features of a regular parallel iterator. + +For instance, assume we want to filter nodes first. Further, instead of summing up the mapped values, we need to collect them in a vector. We can express this computation just as we would do on a linear data structure: + +```rust ignore +[root].into_par_rec(extend).filter(filter).map(map).collect() +``` + ## Performance and Benchmarks You may find some sample parallel programs in [examples](https://github.com/orxfun/orx-parallel/blob/main/examples) directory. These examples allow to express parallel computations as iterator method compositions and run quick experiments with different approaches. Examples use `GenericIterator`. As the name suggests, it is a generalization of sequential iterator, rayon's parallel iterator and orx-parallel's parallel iterator, and hence, allows for convenient experiments. You may play with the code, update the tested computations and run these examples by including **generic_iterator** feature, such as: From 0c5f222dc748ee8add95f7d3f73c6d3e921e99e4 Mon Sep 17 00:00:00 2001 From: orxfun Date: Sun, 26 Oct 2025 20:44:27 +0100 Subject: [PATCH 71/96] upgrade concurrent recursive iter dependency --- Cargo.toml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index bfac6d87..1e689a50 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,8 +21,7 @@ orx-pinned-concurrent-col = { version = "2.18.0", default-features = false } orx-iterable = { version = "1.3.0", default-features = false } orx-priority-queue = { version = "1.7.0", default-features = false } orx-pseudo-default = { version = "2.1.0", default-features = false } -# orx-concurrent-recursive-iter = { version = "1.1.0", default-features = false } -orx-concurrent-recursive-iter = { git = "https://github.com/orxfun/orx-concurrent-recursive-iter", branch = "more-flexible-extension", default-features = false } +orx-concurrent-recursive-iter = { version = "2.0.0", default-features = false } # optional: generic iterator rayon = { version = "1.11.0", optional = true, default-features = false } From e8f9b55ae5d626b743e04fa6fdcb2c07f251499c Mon Sep 17 00:00:00 2001 From: orxfun Date: Sun, 26 Oct 2025 20:44:42 +0100 Subject: [PATCH 72/96] update readme concurrent recursive iter links --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 037782a5..eee00a9e 100644 --- a/README.md +++ b/README.md @@ -176,7 +176,7 @@ fn extend<'a, 'b>(node: &'a &'b Node, queue: &Queue<&'b Node>) { [root].into_par_rec(extend).map(map).sum() ``` -Instead of `into_par`, we use `into_par_rec` and provide `extend` function as its argument. This function defines the recursive extension of the parallel iterator such that every time we process a `node` we first add its children to the `queue`. `Queue` is the queue of elements to be processed and it exposes two growth methods: `push` and `extend` that we can use to define the recursive extension. +Instead of `into_par`, we use `into_par_rec` and provide `extend` function as its argument. This function defines the recursive extension of the parallel iterator such that every time we process a `node` we first add its children to the `queue`. [`Queue`](https://docs.rs/orx-concurrent-recursive-iter/latest/orx_concurrent_recursive_iter/struct.Queue.html) is the queue of elements to be processed and it exposes two growth methods to define the recursive extension: `push` and `extend`. Although we create the parallel iterator differently, we get a `ParIter`. Therefore, we have access to all features of a regular parallel iterator. @@ -186,6 +186,8 @@ For instance, assume we want to filter nodes first. Further, instead of summing [root].into_par_rec(extend).filter(filter).map(map).collect() ``` +For more details, you may see the [parallelization_on_tree](https://github.com/orxfun/orx-parallel/blob/main/examples/parallelization_on_tree) example. + ## Performance and Benchmarks You may find some sample parallel programs in [examples](https://github.com/orxfun/orx-parallel/blob/main/examples) directory. These examples allow to express parallel computations as iterator method compositions and run quick experiments with different approaches. Examples use `GenericIterator`. As the name suggests, it is a generalization of sequential iterator, rayon's parallel iterator and orx-parallel's parallel iterator, and hence, allows for convenient experiments. You may play with the code, update the tested computations and run these examples by including **generic_iterator** feature, such as: From 0afbaee34bd00fcf058cf85eb74162824c970ce6 Mon Sep 17 00:00:00 2001 From: orxfun Date: Sun, 26 Oct 2025 21:10:47 +0100 Subject: [PATCH 73/96] simplify example lifetimes for recursive iterators --- .../parallelization_on_tree/collection_on_entire_tree.rs | 6 +++--- .../parallelization_on_tree/reduction_on_entire_tree.rs | 6 +++--- .../parallelization_on_tree/reduction_on_subset_of_tree.rs | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/examples/parallelization_on_tree/collection_on_entire_tree.rs b/examples/parallelization_on_tree/collection_on_entire_tree.rs index 5254e866..4addd0f7 100644 --- a/examples/parallelization_on_tree/collection_on_entire_tree.rs +++ b/examples/parallelization_on_tree/collection_on_entire_tree.rs @@ -70,7 +70,7 @@ fn sequential(root: &Node) -> Vec { /// be more important in non-linear data structures compared to linear /// due to the dynamic nature of iteration. fn orx_rec(root: &Node) -> Vec { - fn extend<'a, 'b>(node: &'a &'b Node, queue: &Queue<&'b Node>) { + fn extend<'a>(node: &&'a Node, queue: &Queue<&'a Node>) { queue.extend(&node.children); } @@ -85,7 +85,7 @@ fn orx_rec(root: &Node) -> Vec { /// input, the iterator first flattens the tasks with the `into_eager` /// call and then operates on it as if it is over a linear data structure. fn orx_rec_eager(root: &Node) -> Vec { - fn extend<'a, 'b>(node: &'a &'b Node, queue: &Queue<&'b Node>) { + fn extend<'a>(node: &&'a Node, queue: &Queue<&'a Node>) { queue.extend(&node.children); } @@ -108,7 +108,7 @@ fn orx_rec_eager(root: &Node) -> Vec { /// On the other hand, it is good not to keep the chunk size too large in a recursive /// iterator, as we limit it to 32 in the following. fn orx_rec_exact(root: &Node) -> Vec { - fn extend<'a, 'b>(node: &'a &'b Node, queue: &Queue<&'b Node>) { + fn extend<'a>(node: &&'a Node, queue: &Queue<&'a Node>) { queue.extend(&node.children); } diff --git a/examples/parallelization_on_tree/reduction_on_entire_tree.rs b/examples/parallelization_on_tree/reduction_on_entire_tree.rs index 9a282760..c8629b62 100644 --- a/examples/parallelization_on_tree/reduction_on_entire_tree.rs +++ b/examples/parallelization_on_tree/reduction_on_entire_tree.rs @@ -100,7 +100,7 @@ pub fn rayon(root: &Node) -> u64 { /// be more important in non-linear data structures compared to linear /// due to the dynamic nature of iteration. fn orx_rec(root: &Node) -> u64 { - fn extend<'a, 'b>(node: &'a &'b Node, queue: &Queue<&'b Node>) { + fn extend<'a>(node: &&'a Node, queue: &Queue<&'a Node>) { queue.extend(&node.children); } @@ -115,7 +115,7 @@ fn orx_rec(root: &Node) -> u64 { /// input, the iterator first flattens the tasks with the `into_eager` /// call and then operates on it as if it is over a linear data structure. fn orx_rec_eager(root: &Node) -> u64 { - fn extend<'a, 'b>(node: &'a &'b Node, queue: &Queue<&'b Node>) { + fn extend<'a>(node: &&'a Node, queue: &Queue<&'a Node>) { queue.extend(&node.children); } @@ -134,7 +134,7 @@ fn orx_rec_eager(root: &Node) -> u64 { /// On the other hand, it is good not to keep the chunk size too large in a recursive /// iterator, as we limit it to 32 in the following. fn orx_rec_exact(root: &Node) -> u64 { - fn extend<'a, 'b>(node: &'a &'b Node, queue: &Queue<&'b Node>) { + fn extend<'a>(node: &&'a Node, queue: &Queue<&'a Node>) { queue.extend(&node.children); } diff --git a/examples/parallelization_on_tree/reduction_on_subset_of_tree.rs b/examples/parallelization_on_tree/reduction_on_subset_of_tree.rs index bec2016f..3022a810 100644 --- a/examples/parallelization_on_tree/reduction_on_subset_of_tree.rs +++ b/examples/parallelization_on_tree/reduction_on_subset_of_tree.rs @@ -83,7 +83,7 @@ fn sequential(root: &Node) -> u64 { /// * (+) does not require allocation. /// * (-) might have greater parallelization overhead. fn push_orx_rec(root: &Node) -> u64 { - fn extend<'a, 'b>(node: &'a &'b Node, queue: &Queue<&'b Node>) { + fn extend<'a>(node: &&'a Node, queue: &Queue<&'a Node>) { for child in node.children.iter().filter(filter) { queue.push(child); } @@ -100,7 +100,7 @@ fn push_orx_rec(root: &Node) -> u64 { /// * (+) will have the minimum parallelization overhead. /// * (-) requires allocation for processing each node. fn collect_extend_orx_rec(root: &Node) -> u64 { - fn extend<'a, 'b>(node: &'a &'b Node, queue: &Queue<&'b Node>) { + fn extend<'a>(node: &&'a Node, queue: &Queue<&'a Node>) { let children: Vec<_> = node.children.iter().filter(filter).collect(); queue.extend(children); } From 5a61670519debe711c002eb7a7a447b4b1bf5cce Mon Sep 17 00:00:00 2001 From: orxfun Date: Sun, 26 Oct 2025 21:13:10 +0100 Subject: [PATCH 74/96] simplify lifetimes on examples --- README.md | 2 +- benches/rec_iter_map_collect.rs | 6 +++--- benches/rec_iter_map_sum.rs | 8 ++++---- .../parallelization_on_tree/reduction_on_entire_tree.rs | 2 +- src/iter/recursive/into_par_rec_iter.rs | 4 ++-- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index eee00a9e..3aca2e35 100644 --- a/README.md +++ b/README.md @@ -169,7 +169,7 @@ Assume that we want to map all the data with `map: impl Fn(T) -> u64` and comput We can express this computation and execute in parallel with the following: ```rust ignore -fn extend<'a, 'b>(node: &'a &'b Node, queue: &Queue<&'b Node>) { +fn extend<'a>(node: &&'a Node, queue: &Queue<&'a Node>) { queue.extend(&node.children); } diff --git a/benches/rec_iter_map_collect.rs b/benches/rec_iter_map_collect.rs index d6db790c..91700d80 100644 --- a/benches/rec_iter_map_collect.rs +++ b/benches/rec_iter_map_collect.rs @@ -76,7 +76,7 @@ fn seq(roots: &[Node], work: usize) -> Vec { } fn orx_lazy_unknown_chunk1024(roots: &[Node], work: usize) -> SplitVec { - fn extend<'a, 'b>(node: &'a &'b Node, queue: &Queue<&'b Node>) { + fn extend<'a>(node: &&'a Node, queue: &Queue<&'a Node>) { queue.extend(&node.children); } @@ -88,7 +88,7 @@ fn orx_lazy_unknown_chunk1024(roots: &[Node], work: usize) -> SplitVec { } fn orx_lazy_exact(roots: &[Node], work: usize, num_nodes: usize) -> SplitVec { - fn extend<'a, 'b>(node: &'a &'b Node, queue: &Queue<&'b Node>) { + fn extend<'a>(node: &&'a Node, queue: &Queue<&'a Node>) { queue.extend(&node.children); } @@ -99,7 +99,7 @@ fn orx_lazy_exact(roots: &[Node], work: usize, num_nodes: usize) -> SplitVec SplitVec { - fn extend<'a, 'b>(node: &'a &'b Node, queue: &Queue<&'b Node>) { + fn extend<'a>(node: &&'a Node, queue: &Queue<&'a Node>) { queue.extend(&node.children); } diff --git a/benches/rec_iter_map_sum.rs b/benches/rec_iter_map_sum.rs index e21c91d1..9185d3e0 100644 --- a/benches/rec_iter_map_sum.rs +++ b/benches/rec_iter_map_sum.rs @@ -102,7 +102,7 @@ fn rayon(roots: &[Node], work: usize) -> u64 { } fn orx_lazy_unknown_chunk1024(roots: &[Node], work: usize) -> u64 { - fn extend<'a, 'b>(node: &'a &'b Node, queue: &Queue<&'b Node>) { + fn extend<'a>(node: &&'a Node, queue: &Queue<&'a Node>) { queue.extend(&node.children); } @@ -114,7 +114,7 @@ fn orx_lazy_unknown_chunk1024(roots: &[Node], work: usize) -> u64 { } fn orx_lazy_exact(roots: &[Node], work: usize, num_nodes: usize) -> u64 { - fn extend<'a, 'b>(node: &'a &'b Node, queue: &Queue<&'b Node>) { + fn extend<'a>(node: &&'a Node, queue: &Queue<&'a Node>) { queue.extend(&node.children); } @@ -125,7 +125,7 @@ fn orx_lazy_exact(roots: &[Node], work: usize, num_nodes: usize) -> u64 { } fn orx_lazy_exact_flat_map(roots: &[Node], work: usize, num_nodes: usize) -> u64 { - fn extend<'a, 'b>(node: &'a &'b Node, queue: &Queue<&'b Node>) { + fn extend<'a>(node: &&'a Node, queue: &Queue<&'a Node>) { queue.extend(&node.children); } @@ -136,7 +136,7 @@ fn orx_lazy_exact_flat_map(roots: &[Node], work: usize, num_nodes: usize) -> u64 } fn orx_eager(roots: &[Node], work: usize) -> u64 { - fn extend<'a, 'b>(node: &'a &'b Node, queue: &Queue<&'b Node>) { + fn extend<'a>(node: &&'a Node, queue: &Queue<&'a Node>) { queue.extend(&node.children); } diff --git a/examples/parallelization_on_tree/reduction_on_entire_tree.rs b/examples/parallelization_on_tree/reduction_on_entire_tree.rs index c8629b62..52909568 100644 --- a/examples/parallelization_on_tree/reduction_on_entire_tree.rs +++ b/examples/parallelization_on_tree/reduction_on_entire_tree.rs @@ -22,7 +22,7 @@ created with an "extend" function which defines the recursive behavior. In this example we create an iterator where elements are "&Node". We define the "extend" function as follows: -fn extend<'a, 'b>(node: &'a &'b Node, queue: &Queue<&'b Node>) {{ +fn extend<'a>(node: &&'a Node, queue: &Queue<&'a Node>) {{ queue.extend(&node.children); }} diff --git a/src/iter/recursive/into_par_rec_iter.rs b/src/iter/recursive/into_par_rec_iter.rs index 44ad1ef8..84d9d79b 100644 --- a/src/iter/recursive/into_par_rec_iter.rs +++ b/src/iter/recursive/into_par_rec_iter.rs @@ -178,7 +178,7 @@ where /// /// // this defines how the iterator must extend: /// // each node drawn from the iterator adds its children to the end of the iterator - /// fn extend<'a, 'b>(node: &'a &'b Node, queue: &Queue<&'b Node>) { + /// fn extend<'a>(node: &&'a Node, queue: &Queue<&'a Node>) { /// queue.extend(&node.children); /// } /// @@ -385,7 +385,7 @@ where /// /// // this defines how the iterator must extend: /// // each node drawn from the iterator adds its children to the end of the iterator - /// fn extend<'a, 'b>(node: &'a &'b Node, queue: &Queue<&'b Node>) { + /// fn extend<'a>(node: &&'a Node, queue: &Queue<&'a Node>) { /// queue.extend(&node.children); /// } /// From 7f3600106a109e34dd58a1d31309c7db82ef000c Mon Sep 17 00:00:00 2001 From: orxfun Date: Sun, 26 Oct 2025 21:45:55 +0100 Subject: [PATCH 75/96] update documentation --- src/iter/recursive/into_par_rec_iter.rs | 416 ++++++++++++------------ 1 file changed, 200 insertions(+), 216 deletions(-) diff --git a/src/iter/recursive/into_par_rec_iter.rs b/src/iter/recursive/into_par_rec_iter.rs index 84d9d79b..861b2fc8 100644 --- a/src/iter/recursive/into_par_rec_iter.rs +++ b/src/iter/recursive/into_par_rec_iter.rs @@ -4,6 +4,8 @@ use orx_concurrent_recursive_iter::{ConcurrentRecursiveIter, Queue}; // unknown size /// Trait to convert an iterator into a recursive parallel iterator together with the `extend` method. +/// Recursive iterators are most useful for defining parallel computations over non-linear data structures +/// such as trees or graphs. /// /// Created parallel iterator is a regular parallel iterator; i.e., we have access to all [`ParIter`] features. /// @@ -20,6 +22,8 @@ where Self::Item: Send, { /// Converts this iterator into a recursive parallel iterator together with the `extend` method. + /// Recursive iterators are most useful for defining parallel computations over non-linear data structures + /// such as trees or graphs. /// /// Created parallel iterator is a regular parallel iterator; i.e., we have access to all [`ParIter`] features. /// @@ -29,16 +33,7 @@ where /// /// You may read more about the [`ConcurrentRecursiveIter`]. /// - /// # Examples - /// - /// The following example has some code to set up until the `# usage` line. Notice that the `Node` - /// is a **non-linear** data structure, each node having children nodes to be recursively processed. - /// - /// We have three initial elements `roots`. - /// - /// We want to compute sum of Fibonacci numbers of values of all nodes descending from the roots. - /// - /// The `expand` function defines the recursive expansion behavior. It takes two arguments: + /// The `extend` function defines the recursive expansion behavior. It takes two arguments: /// * `element: &Self::Item` is the item being processed. /// * `queue: Queue` is the queue of remaining elements/tasks which exposes two methods: /// * `push(item)` allows us to add one item to the queue, @@ -65,7 +60,7 @@ where /// the tasks will not be scarce; (ii) and if we extend with only a few of items, the delay of making the tasks /// available for other threads will be short. /// - /// Nevertheless, the decision is use-case specific and best to benchmark for the specific input. + /// The decision is use-case specific and best to benchmark for the specific input. /// /// This crate makes use of the [`ConcurrentRecursiveIter`] for this computation and provides three ways to execute /// this computation in parallel. @@ -77,12 +72,7 @@ where /// iterator is the total of all elements that will be created. This gives the parallel executor /// opportunity to optimize the chunk sizes. /// - /// In the following example, we first calculate this number with `roots.iter().map(|x| x.seq_num_nodes()).sum();` - /// and then use `(&roots).into_par_rec_exact(extend, count)` to create our exact length recursive parallel - /// iterator. - /// - /// Note that, once we create the recursive parallel iterator, it is just another [`ParIter`]. In other words, - /// we have access to all parallel iterator features. + /// We can use `initial_elements.into_par_rec_exact(extend, count)` to create the iterator with exact length. /// /// ## B. Recursive Iterator with Unknown Length /// @@ -91,133 +81,136 @@ where /// chunk size explicitly depending on the number of threads that will be used and any estimate on the exact /// length. /// - /// In the following example, we directly create the parallel iterator with `(&roots).into_par_rec(extend)` - /// without providing any length information. Then, we ask the parallel executor to pull tasks in chunks of - /// 1024 with `.chunk_size(1024)`. Recall the general rule-of-thumb on chunk size parameter: - /// * the longer each individual computation, the smaller the chunks can be, - /// * when it is too small, we might suffer from parallelization overhead, - /// * when it is too large, we might suffer from heterogeneity of tasks which might lead to imbalance of - /// load of threads, - /// * we might try to set it to a large enough value to reduce parallelization overhead without causing - /// imbalance. + /// We can use `initial_elements.into_par_rec(extend)` to create the iterator without length information. /// /// ## C. Into Eager Transformation /// /// Even with exact length, a recursive parallel iterator is much more dynamic than a flat parallel /// iterator. This dynamic nature of shrinking and growing concurrently requires a greater parallelization /// overhead. An alternative approach is to eagerly discover all tasks and then perform the parallel - /// computation over the flattened input of tasks. - /// - /// Note that exact size will be obtained during flattening; and hence, we do not need to provide the `count`. + /// computation over the flattened input of tasks using [`into_eager`] transformation. /// - /// In the example, we create eagerly flattened parallel iterator with the `(&roots).into_par_rec(extend).into_eager()` call. + /// We can use `initial_elements.into_par_rec(extend).into_eager()` to create the flattened iterator. /// /// [`ParIter`]: crate::ParIter /// [`ConcurrentRecursiveIter`]: orx_concurrent_recursive_iter::ConcurrentRecursiveIter + /// [`into_eager`]: crate::computational_variants::Par::into_eager + /// + /// ## Examples /// - /// ## Example with all three approaches + /// In the following example we perform some parallel computations over a tree. + /// It demonstrates that a "recursive parallel iterator" is just a parallel iterator with + /// access to all [`ParIter`] methods. + /// Once we create the recursive parallel iterator with the `extend` definition, we can use it as + /// a regular parallel iterator. + /// + /// Unfortunately, the example requires a long set up for completeness. Note that the relevant + /// code blocks begin after line `// parallel reduction`. /// /// ``` /// use orx_parallel::*; /// use rand::{Rng, SeedableRng}; /// use rand_chacha::ChaCha8Rng; + /// use std::{collections::HashSet, ops::Range}; /// - /// struct Node { - /// value: Vec, - /// children: Vec, + /// pub struct Node { + /// pub idx: usize, + /// pub data: T, + /// pub children: Vec>, /// } /// - /// impl Node { - /// fn new(mut n: u32, rng: &mut impl Rng) -> Self { - /// let mut children = Vec::new(); - /// if n < 5 { - /// for _ in 0..n { - /// children.push(Node::new(0, rng)); - /// } - /// } else { - /// while n > 0 { - /// let n2 = rng.random_range(0..=n); - /// children.push(Node::new(n2, rng)); - /// n -= n2; - /// } - /// } - /// Self { - /// value: (0..rng.random_range(1..500)) - /// .map(|_| rng.random_range(0..40)) + /// impl Node { + /// fn create_node(out_edges: &[Vec], idx: usize, data: fn(usize) -> T) -> Node { + /// Node { + /// idx, + /// data: data(idx), + /// children: out_edges[idx] + /// .iter() + /// .map(|child_idx| Self::create_node(out_edges, *child_idx, data)) /// .collect(), - /// children, /// } /// } /// - /// fn seq_num_nodes(&self) -> usize { - /// 1 + self - /// .children - /// .iter() - /// .map(|node| node.seq_num_nodes()) - /// .sum::() - /// } - /// - /// fn seq_sum_fib(&self) -> u64 { - /// self.value.iter().map(|x| fibonacci(*x)).sum::() - /// + self.children.iter().map(|x| x.seq_sum_fib()).sum::() - /// } - /// } + /// pub fn new_tree( + /// num_nodes: usize, + /// degree: Range, + /// data: fn(usize) -> T, + /// rng: &mut impl Rng, + /// ) -> Node { + /// assert!(num_nodes >= 2); + /// + /// let mut leaves = vec![0]; + /// let mut remaining: Vec<_> = (1..num_nodes).collect(); + /// let mut edges = vec![]; + /// let mut out_edges = vec![vec![]; num_nodes]; + /// + /// while !remaining.is_empty() { + /// let leaf_idx = rng.random_range(0..leaves.len()); + /// let leaf = leaves.remove(leaf_idx); + /// + /// let degree = rng.random_range(degree.clone()); + /// match degree == 0 { + /// true => leaves.push(leaf), + /// false => { + /// let children_indices: HashSet<_> = (0..degree) + /// .map(|_| rng.random_range(0..remaining.len())) + /// .collect(); + /// + /// let mut sorted: Vec<_> = children_indices.iter().copied().collect(); + /// sorted.sort(); + /// + /// edges.extend(children_indices.iter().map(|c| (leaf, remaining[*c]))); + /// out_edges[leaf] = children_indices.iter().map(|c| remaining[*c]).collect(); + /// leaves.extend(children_indices.iter().map(|c| remaining[*c])); + /// + /// for idx in sorted.into_iter().rev() { + /// remaining.remove(idx); + /// } + /// } + /// } + /// } /// - /// fn fibonacci(n: u64) -> u64 { - /// let mut a = 0; - /// let mut b = 1; - /// for _ in 0..n { - /// let c = a + b; - /// a = b; - /// b = c; + /// Self::create_node(&out_edges, 0, data) /// } - /// a - /// } - /// - /// // # usage - /// - /// // this defines how the iterator must extend: - /// // each node drawn from the iterator adds its children to the end of the iterator - /// fn extend<'a>(node: &&'a Node, queue: &Queue<&'a Node>) { - /// queue.extend(&node.children); /// } /// + /// let num_nodes = 1_000; + /// let out_degree = 0..100; /// let mut rng = ChaCha8Rng::seed_from_u64(42); - /// let roots = vec![ - /// Node::new(50, &mut rng), - /// Node::new(20, &mut rng), - /// Node::new(40, &mut rng), - /// ]; + /// let data = |idx: usize| idx.to_string(); + /// let root = Node::new_tree(num_nodes, out_degree, data, &mut rng); /// - /// let seq_sum: u64 = roots.iter().map(|x| x.seq_sum_fib()).sum(); + /// let compute = |node: &Node| node.data.parse::().unwrap(); /// - /// // A. exact length, recommended when possible + /// // parallel reduction /// - /// let count: usize = roots.iter().map(|x| x.seq_num_nodes()).sum(); + /// fn extend<'a, T: Sync>(node: &&'a Node, queue: &Queue<&'a Node>) { + /// queue.extend(&node.children); + /// } /// - /// let sum = (&roots) - /// .into_par_rec_exact(extend, count) - /// .map(|x| x.value.iter().map(|x| fibonacci(*x)).sum::()) - /// .sum(); - /// assert_eq!(sum, seq_sum); + /// let sum = [&root].into_par_rec(extend).map(compute).sum(); + /// assert_eq!(sum, 499500); /// - /// // B. guide the computation with chunk size, when length is unknown + /// // or any parallel computation such as map->filter->collect /// - /// let sum = (&roots) + /// let result: Vec<_> = [&root] /// .into_par_rec(extend) - /// .chunk_size(1024) - /// .map(|x| x.value.iter().map(|x| fibonacci(*x)).sum::()) - /// .sum(); - /// assert_eq!(sum, seq_sum); - /// - /// // C. eagerly convert to a flat iterator + /// .map(compute) + /// .filter(|x| x.is_multiple_of(7)) + /// .collect(); + /// assert_eq!(result.len(), 143); + /// + /// // or filter during extension + /// fn extend_filtered<'a>(node: &&'a Node, queue: &Queue<&'a Node>) { + /// for child in &node.children { + /// if child.idx != 42 { + /// queue.push(child); + /// } + /// } + /// } /// - /// let sum = (&roots) - /// .into_par_rec(extend) - /// .into_eager() - /// .map(|x| x.value.iter().map(|x| fibonacci(*x)).sum::()) - /// .sum(); - /// assert_eq!(sum, seq_sum); + /// let sum = [&root].into_par_rec(extend_filtered).map(compute).sum(); + /// assert_eq!(sum, 499458); /// ``` fn into_par_rec( self, @@ -227,6 +220,8 @@ where E: Fn(&Self::Item, &Queue) + Sync; /// Converts this iterator into a recursive parallel iterator together with the `extend` method. + /// Recursive iterators are most useful for defining parallel computations over non-linear data structures + /// such as trees or graphs. /// /// Created parallel iterator is a regular parallel iterator; i.e., we have access to all [`ParIter`] features. /// @@ -236,16 +231,7 @@ where /// /// You may read more about the [`ConcurrentRecursiveIter`]. /// - /// # Examples - /// - /// The following example has some code to set up until the `# usage` line. Notice that the `Node` - /// is a **non-linear** data structure, each node having children nodes to be recursively processed. - /// - /// We have three initial elements `roots`. - /// - /// We want to compute sum of Fibonacci numbers of values of all nodes descending from the roots. - /// - /// The `expand` function defines the recursive expansion behavior. It takes two arguments: + /// The `extend` function defines the recursive expansion behavior. It takes two arguments: /// * `element: &Self::Item` is the item being processed. /// * `queue: Queue` is the queue of remaining elements/tasks which exposes two methods: /// * `push(item)` allows us to add one item to the queue, @@ -272,7 +258,7 @@ where /// the tasks will not be scarce; (ii) and if we extend with only a few of items, the delay of making the tasks /// available for other threads will be short. /// - /// Nevertheless, the decision is use-case specific and best to benchmark for the specific input. + /// The decision is use-case specific and best to benchmark for the specific input. /// /// This crate makes use of the [`ConcurrentRecursiveIter`] for this computation and provides three ways to execute /// this computation in parallel. @@ -284,12 +270,7 @@ where /// iterator is the total of all elements that will be created. This gives the parallel executor /// opportunity to optimize the chunk sizes. /// - /// In the following example, we first calculate this number with `roots.iter().map(|x| x.seq_num_nodes()).sum();` - /// and then use `(&roots).into_par_rec_exact(extend, count)` to create our exact length recursive parallel - /// iterator. - /// - /// Note that, once we create the recursive parallel iterator, it is just another [`ParIter`]. In other words, - /// we have access to all parallel iterator features. + /// We can use `initial_elements.into_par_rec_exact(extend, count)` to create the iterator with exact length. /// /// ## B. Recursive Iterator with Unknown Length /// @@ -298,133 +279,136 @@ where /// chunk size explicitly depending on the number of threads that will be used and any estimate on the exact /// length. /// - /// In the following example, we directly create the parallel iterator with `(&roots).into_par_rec(extend)` - /// without providing any length information. Then, we ask the parallel executor to pull tasks in chunks of - /// 1024 with `.chunk_size(1024)`. Recall the general rule-of-thumb on chunk size parameter: - /// * the longer each individual computation, the smaller the chunks can be, - /// * when it is too small, we might suffer from parallelization overhead, - /// * when it is too large, we might suffer from heterogeneity of tasks which might lead to imbalance of - /// load of threads, - /// * we might try to set it to a large enough value to reduce parallelization overhead without causing - /// imbalance. + /// We can use `initial_elements.into_par_rec(extend)` to create the iterator without length information. /// /// ## C. Into Eager Transformation /// /// Even with exact length, a recursive parallel iterator is much more dynamic than a flat parallel /// iterator. This dynamic nature of shrinking and growing concurrently requires a greater parallelization /// overhead. An alternative approach is to eagerly discover all tasks and then perform the parallel - /// computation over the flattened input of tasks. - /// - /// Note that exact size will be obtained during flattening; and hence, we do not need to provide the `count`. + /// computation over the flattened input of tasks using [`into_eager`] transformation. /// - /// In the example, we create eagerly flattened parallel iterator with the `(&roots).into_par_rec(extend).into_eager()` call. + /// We can use `initial_elements.into_par_rec(extend).into_eager()` to create the flattened iterator. /// /// [`ParIter`]: crate::ParIter /// [`ConcurrentRecursiveIter`]: orx_concurrent_recursive_iter::ConcurrentRecursiveIter + /// [`into_eager`]: crate::computational_variants::Par::into_eager + /// + /// ## Examples /// - /// ## Example with all three approaches + /// In the following example we perform some parallel computations over a tree. + /// It demonstrates that a "recursive parallel iterator" is just a parallel iterator with + /// access to all [`ParIter`] methods. + /// Once we create the recursive parallel iterator with the `extend` definition, we can use it as + /// a regular parallel iterator. + /// + /// Unfortunately, the example requires a long set up for completeness. Note that the relevant + /// code blocks begin after line `// parallel reduction`. /// /// ``` /// use orx_parallel::*; /// use rand::{Rng, SeedableRng}; /// use rand_chacha::ChaCha8Rng; + /// use std::{collections::HashSet, ops::Range}; /// - /// struct Node { - /// value: Vec, - /// children: Vec, + /// pub struct Node { + /// pub idx: usize, + /// pub data: T, + /// pub children: Vec>, /// } /// - /// impl Node { - /// fn new(mut n: u32, rng: &mut impl Rng) -> Self { - /// let mut children = Vec::new(); - /// if n < 5 { - /// for _ in 0..n { - /// children.push(Node::new(0, rng)); - /// } - /// } else { - /// while n > 0 { - /// let n2 = rng.random_range(0..=n); - /// children.push(Node::new(n2, rng)); - /// n -= n2; - /// } - /// } - /// Self { - /// value: (0..rng.random_range(1..500)) - /// .map(|_| rng.random_range(0..40)) + /// impl Node { + /// fn create_node(out_edges: &[Vec], idx: usize, data: fn(usize) -> T) -> Node { + /// Node { + /// idx, + /// data: data(idx), + /// children: out_edges[idx] + /// .iter() + /// .map(|child_idx| Self::create_node(out_edges, *child_idx, data)) /// .collect(), - /// children, /// } /// } /// - /// fn seq_num_nodes(&self) -> usize { - /// 1 + self - /// .children - /// .iter() - /// .map(|node| node.seq_num_nodes()) - /// .sum::() - /// } - /// - /// fn seq_sum_fib(&self) -> u64 { - /// self.value.iter().map(|x| fibonacci(*x)).sum::() - /// + self.children.iter().map(|x| x.seq_sum_fib()).sum::() - /// } - /// } + /// pub fn new_tree( + /// num_nodes: usize, + /// degree: Range, + /// data: fn(usize) -> T, + /// rng: &mut impl Rng, + /// ) -> Node { + /// assert!(num_nodes >= 2); + /// + /// let mut leaves = vec![0]; + /// let mut remaining: Vec<_> = (1..num_nodes).collect(); + /// let mut edges = vec![]; + /// let mut out_edges = vec![vec![]; num_nodes]; + /// + /// while !remaining.is_empty() { + /// let leaf_idx = rng.random_range(0..leaves.len()); + /// let leaf = leaves.remove(leaf_idx); + /// + /// let degree = rng.random_range(degree.clone()); + /// match degree == 0 { + /// true => leaves.push(leaf), + /// false => { + /// let children_indices: HashSet<_> = (0..degree) + /// .map(|_| rng.random_range(0..remaining.len())) + /// .collect(); + /// + /// let mut sorted: Vec<_> = children_indices.iter().copied().collect(); + /// sorted.sort(); + /// + /// edges.extend(children_indices.iter().map(|c| (leaf, remaining[*c]))); + /// out_edges[leaf] = children_indices.iter().map(|c| remaining[*c]).collect(); + /// leaves.extend(children_indices.iter().map(|c| remaining[*c])); + /// + /// for idx in sorted.into_iter().rev() { + /// remaining.remove(idx); + /// } + /// } + /// } + /// } /// - /// fn fibonacci(n: u64) -> u64 { - /// let mut a = 0; - /// let mut b = 1; - /// for _ in 0..n { - /// let c = a + b; - /// a = b; - /// b = c; + /// Self::create_node(&out_edges, 0, data) /// } - /// a - /// } - /// - /// // # usage - /// - /// // this defines how the iterator must extend: - /// // each node drawn from the iterator adds its children to the end of the iterator - /// fn extend<'a>(node: &&'a Node, queue: &Queue<&'a Node>) { - /// queue.extend(&node.children); /// } /// + /// let num_nodes = 1_000; + /// let out_degree = 0..100; /// let mut rng = ChaCha8Rng::seed_from_u64(42); - /// let roots = vec![ - /// Node::new(50, &mut rng), - /// Node::new(20, &mut rng), - /// Node::new(40, &mut rng), - /// ]; + /// let data = |idx: usize| idx.to_string(); + /// let root = Node::new_tree(num_nodes, out_degree, data, &mut rng); /// - /// let seq_sum: u64 = roots.iter().map(|x| x.seq_sum_fib()).sum(); + /// let compute = |node: &Node| node.data.parse::().unwrap(); /// - /// // A. exact length, recommended when possible + /// // parallel reduction /// - /// let count: usize = roots.iter().map(|x| x.seq_num_nodes()).sum(); + /// fn extend<'a, T: Sync>(node: &&'a Node, queue: &Queue<&'a Node>) { + /// queue.extend(&node.children); + /// } /// - /// let sum = (&roots) - /// .into_par_rec_exact(extend, count) - /// .map(|x| x.value.iter().map(|x| fibonacci(*x)).sum::()) - /// .sum(); - /// assert_eq!(sum, seq_sum); + /// let sum = [&root].into_par_rec(extend).map(compute).sum(); + /// assert_eq!(sum, 499500); /// - /// // B. guide the computation with chunk size, when length is unknown + /// // or any parallel computation such as map->filter->collect /// - /// let sum = (&roots) + /// let result: Vec<_> = [&root] /// .into_par_rec(extend) - /// .chunk_size(1024) - /// .map(|x| x.value.iter().map(|x| fibonacci(*x)).sum::()) - /// .sum(); - /// assert_eq!(sum, seq_sum); - /// - /// // C. eagerly convert to a flat iterator + /// .map(compute) + /// .filter(|x| x.is_multiple_of(7)) + /// .collect(); + /// assert_eq!(result.len(), 143); + /// + /// // or filter during extension + /// fn extend_filtered<'a>(node: &&'a Node, queue: &Queue<&'a Node>) { + /// for child in &node.children { + /// if child.idx != 42 { + /// queue.push(child); + /// } + /// } + /// } /// - /// let sum = (&roots) - /// .into_par_rec(extend) - /// .into_eager() - /// .map(|x| x.value.iter().map(|x| fibonacci(*x)).sum::()) - /// .sum(); - /// assert_eq!(sum, seq_sum); + /// let sum = [&root].into_par_rec(extend_filtered).map(compute).sum(); + /// assert_eq!(sum, 499458); /// ``` fn into_par_rec_exact( self, From d7dc5d51abc52f7115f006905c081dd2664ff304 Mon Sep 17 00:00:00 2001 From: orxfun Date: Mon, 27 Oct 2025 15:36:58 +0100 Subject: [PATCH 76/96] into_eager is renamed as linearize --- benches/rec_iter_map_collect.rs | 10 +++++----- benches/rec_iter_map_sum.rs | 10 +++++----- .../collection_on_entire_tree.rs | 8 ++++---- .../reduction_on_entire_tree.rs | 8 ++++---- src/iter/recursive/into_par_rec_iter.rs | 16 ++++++++-------- src/iter/recursive/rec_par_iter.rs | 6 +++--- 6 files changed, 29 insertions(+), 29 deletions(-) diff --git a/benches/rec_iter_map_collect.rs b/benches/rec_iter_map_collect.rs index 91700d80..e33305ee 100644 --- a/benches/rec_iter_map_collect.rs +++ b/benches/rec_iter_map_collect.rs @@ -98,14 +98,14 @@ fn orx_lazy_exact(roots: &[Node], work: usize, num_nodes: usize) -> SplitVec SplitVec { +fn orx_linearized(roots: &[Node], work: usize) -> SplitVec { fn extend<'a>(node: &&'a Node, queue: &Queue<&'a Node>) { queue.extend(&node.children); } roots .into_par_rec(extend) - .into_eager() + .linearize() .flat_map(|x| x.value.iter().map(|x| fibonacci(*x, work))) .collect() } @@ -151,11 +151,11 @@ fn run(c: &mut Criterion) { }, ); - group.bench_with_input(BenchmarkId::new("orx_eager", work), work, |b, _| { - let mut result = orx_eager(&roots, *work).to_vec(); + group.bench_with_input(BenchmarkId::new("orx_linearized", work), work, |b, _| { + let mut result = orx_linearized(&roots, *work).to_vec(); result.sort(); assert_eq!(&expected, &result); - b.iter(|| orx_eager(&roots, *work)) + b.iter(|| orx_linearized(&roots, *work)) }); } diff --git a/benches/rec_iter_map_sum.rs b/benches/rec_iter_map_sum.rs index 9185d3e0..fab904b8 100644 --- a/benches/rec_iter_map_sum.rs +++ b/benches/rec_iter_map_sum.rs @@ -135,14 +135,14 @@ fn orx_lazy_exact_flat_map(roots: &[Node], work: usize, num_nodes: usize) -> u64 .sum() } -fn orx_eager(roots: &[Node], work: usize) -> u64 { +fn orx_linearized(roots: &[Node], work: usize) -> u64 { fn extend<'a>(node: &&'a Node, queue: &Queue<&'a Node>) { queue.extend(&node.children); } roots .into_par_rec(extend) - .into_eager() + .linearize() .map(|x| x.value.iter().map(|x| fibonacci(*x, work)).sum::()) .sum() } @@ -198,9 +198,9 @@ fn run(c: &mut Criterion) { }, ); - group.bench_with_input(BenchmarkId::new("orx_eager", work), work, |b, _| { - assert_eq!(&expected, &orx_eager(&roots, *work)); - b.iter(|| orx_eager(&roots, *work)) + group.bench_with_input(BenchmarkId::new("orx_linearized", work), work, |b, _| { + assert_eq!(&expected, &orx_linearized(&roots, *work)); + b.iter(|| orx_linearized(&roots, *work)) }); } diff --git a/examples/parallelization_on_tree/collection_on_entire_tree.rs b/examples/parallelization_on_tree/collection_on_entire_tree.rs index 4addd0f7..40738771 100644 --- a/examples/parallelization_on_tree/collection_on_entire_tree.rs +++ b/examples/parallelization_on_tree/collection_on_entire_tree.rs @@ -31,7 +31,7 @@ into timed("sequential", || sequential(root), log); timed("orx_rec", || orx_rec(root), log); - timed("orx_rec_eager", || orx_rec_eager(root), log); + timed("orx_rec_linearized", || orx_rec_linearized(root), log); timed("orx_rec_exact", || orx_rec_exact(root), log); println!(); @@ -82,16 +82,16 @@ fn orx_rec(root: &Node) -> Vec { /// Here we parallelize by providing the `extend` function. /// /// However, rather than parallel processing over a dynamic recursive -/// input, the iterator first flattens the tasks with the `into_eager` +/// input, the iterator first flattens the tasks with the `linearize` /// call and then operates on it as if it is over a linear data structure. -fn orx_rec_eager(root: &Node) -> Vec { +fn orx_rec_linearized(root: &Node) -> Vec { fn extend<'a>(node: &&'a Node, queue: &Queue<&'a Node>) { queue.extend(&node.children); } [root] .into_par_rec(extend) - .into_eager() + .linearize() .map(compute) .collect() } diff --git a/examples/parallelization_on_tree/reduction_on_entire_tree.rs b/examples/parallelization_on_tree/reduction_on_entire_tree.rs index 52909568..46401403 100644 --- a/examples/parallelization_on_tree/reduction_on_entire_tree.rs +++ b/examples/parallelization_on_tree/reduction_on_entire_tree.rs @@ -47,7 +47,7 @@ data structure: timed("rayon", || rayon(root), log); timed("orx_rec", || orx_rec(root), log); - timed("orx_rec_eager", || orx_rec_eager(root), log); + timed("orx_rec_linearized", || orx_rec_linearized(root), log); timed("orx_rec_exact", || orx_rec_exact(root), log); println!(); @@ -112,14 +112,14 @@ fn orx_rec(root: &Node) -> u64 { /// Here we parallelize by providing the `extend` function. /// /// However, rather than parallel processing over a dynamic recursive -/// input, the iterator first flattens the tasks with the `into_eager` +/// input, the iterator first flattens the tasks with the `linearize` /// call and then operates on it as if it is over a linear data structure. -fn orx_rec_eager(root: &Node) -> u64 { +fn orx_rec_linearized(root: &Node) -> u64 { fn extend<'a>(node: &&'a Node, queue: &Queue<&'a Node>) { queue.extend(&node.children); } - [root].into_par_rec(extend).into_eager().map(compute).sum() + [root].into_par_rec(extend).linearize().map(compute).sum() } /// # orx-parallel: parallel recursive iterator with exact length diff --git a/src/iter/recursive/into_par_rec_iter.rs b/src/iter/recursive/into_par_rec_iter.rs index 861b2fc8..ff21b8e8 100644 --- a/src/iter/recursive/into_par_rec_iter.rs +++ b/src/iter/recursive/into_par_rec_iter.rs @@ -83,18 +83,18 @@ where /// /// We can use `initial_elements.into_par_rec(extend)` to create the iterator without length information. /// - /// ## C. Into Eager Transformation + /// ## C. Linearization /// /// Even with exact length, a recursive parallel iterator is much more dynamic than a flat parallel /// iterator. This dynamic nature of shrinking and growing concurrently requires a greater parallelization /// overhead. An alternative approach is to eagerly discover all tasks and then perform the parallel - /// computation over the flattened input of tasks using [`into_eager`] transformation. + /// computation over the flattened input of tasks using [`linearize`] transformation. /// - /// We can use `initial_elements.into_par_rec(extend).into_eager()` to create the flattened iterator. + /// We can use `initial_elements.into_par_rec(extend).linearize()` to create the flattened iterator. /// /// [`ParIter`]: crate::ParIter /// [`ConcurrentRecursiveIter`]: orx_concurrent_recursive_iter::ConcurrentRecursiveIter - /// [`into_eager`]: crate::computational_variants::Par::into_eager + /// [`linearize`]: crate::computational_variants::Par::linearize /// /// ## Examples /// @@ -281,18 +281,18 @@ where /// /// We can use `initial_elements.into_par_rec(extend)` to create the iterator without length information. /// - /// ## C. Into Eager Transformation + /// ## C. Linearization /// /// Even with exact length, a recursive parallel iterator is much more dynamic than a flat parallel /// iterator. This dynamic nature of shrinking and growing concurrently requires a greater parallelization /// overhead. An alternative approach is to eagerly discover all tasks and then perform the parallel - /// computation over the flattened input of tasks using [`into_eager`] transformation. + /// computation over the flattened input of tasks using [`linearize`] transformation. /// - /// We can use `initial_elements.into_par_rec(extend).into_eager()` to create the flattened iterator. + /// We can use `initial_elements.into_par_rec(extend).linearize()` to create the flattened iterator. /// /// [`ParIter`]: crate::ParIter /// [`ConcurrentRecursiveIter`]: orx_concurrent_recursive_iter::ConcurrentRecursiveIter - /// [`into_eager`]: crate::computational_variants::Par::into_eager + /// [`linearize`]: crate::computational_variants::Par::linearize /// /// ## Examples /// diff --git a/src/iter/recursive/rec_par_iter.rs b/src/iter/recursive/rec_par_iter.rs index efefe4a5..d73f1e48 100644 --- a/src/iter/recursive/rec_par_iter.rs +++ b/src/iter/recursive/rec_par_iter.rs @@ -26,7 +26,7 @@ where /// /// [`into_par_rec`]: crate::IntoParIterRec::into_par_rec /// [`into_par_rec_exact`]: crate::IntoParIterRec::into_par_rec_exact - pub fn into_eager(self) -> Par, R> { + pub fn linearize(self) -> Par, R> { let (orchestrator, params, iter) = self.destruct(); let items = collect_items(iter); let iter = items.into_con_iter(); @@ -53,7 +53,7 @@ where /// /// [`into_par_rec`]: crate::IntoParIterRec::into_par_rec /// [`into_par_rec_exact`]: crate::IntoParIterRec::into_par_rec_exact - pub fn into_eager(self) -> ParMap, O, M1, R> { + pub fn linearize(self) -> ParMap, O, M1, R> { let (orchestrator, params, iter, map1) = self.destruct(); let items = collect_items(iter); let iter = items.into_con_iter(); @@ -81,7 +81,7 @@ where /// /// [`into_par_rec`]: crate::IntoParIterRec::into_par_rec /// [`into_par_rec_exact`]: crate::IntoParIterRec::into_par_rec_exact - pub fn into_eager(self) -> ParXap, Vo, X1, R> { + pub fn linearize(self) -> ParXap, Vo, X1, R> { let (orchestrator, params, iter, xap1) = self.destruct(); let items = collect_items(iter); let iter = items.into_con_iter(); From ed39dacb74278d29453b40d0314766b3807d66b2 Mon Sep 17 00:00:00 2001 From: orxfun Date: Mon, 27 Oct 2025 15:45:26 +0100 Subject: [PATCH 77/96] wip parallel linearization --- src/iter/recursive/rec_par_iter.rs | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/src/iter/recursive/rec_par_iter.rs b/src/iter/recursive/rec_par_iter.rs index d73f1e48..3847b992 100644 --- a/src/iter/recursive/rec_par_iter.rs +++ b/src/iter/recursive/rec_par_iter.rs @@ -1,5 +1,5 @@ use crate::{ - ParallelRunner, + ParIter, ParallelRunner, computational_variants::{Par, ParMap, ParXap}, generic_values::{TransformableValues, runner_results::Infallible}, }; @@ -10,7 +10,7 @@ type Rec = ConcurrentRecursiveIter; impl Par, R> where - T: Send, + T: Send + Sync, E: Fn(&T, &Queue) + Sync, R: ParallelRunner, { @@ -27,10 +27,17 @@ where /// [`into_par_rec`]: crate::IntoParIterRec::into_par_rec /// [`into_par_rec_exact`]: crate::IntoParIterRec::into_par_rec_exact pub fn linearize(self) -> Par, R> { - let (orchestrator, params, iter) = self.destruct(); - let items = collect_items(iter); + let params = self.params(); + + let items: Vec<_> = self.map(|x| x).collect(); let iter = items.into_con_iter(); - Par::new(orchestrator, params, iter) + + // a + todo!() + // let (orchestrator, params, iter) = self.destruct(); + // let items = collect_items(iter); + // let iter = items.into_con_iter(); + // Par::new(orchestrator, params, iter) } } @@ -103,3 +110,11 @@ where None => iter.into_seq_iter().collect(), } } + +fn collect_items_par(iter: Rec) -> Vec +where + T: Send, + E: Fn(&T, &Queue) + Sync, +{ + todo!() +} From 90a7228c0d10a49a7e804e1eb52af072fdba25e7 Mon Sep 17 00:00:00 2001 From: orxfun Date: Mon, 27 Oct 2025 15:59:32 +0100 Subject: [PATCH 78/96] linearization in parallel --- src/computational_variants/par.rs | 4 ++ .../parallel_executor.rs | 1 + .../fixed_chunk_executor/parallel_executor.rs | 11 ++++ src/executor/parallel_executor.rs | 2 +- src/iter/recursive/rec_par_iter.rs | 53 +++++-------------- src/runner/implementations/pond.rs | 1 + .../implementations/runner_with_pool.rs | 1 + src/runner/implementations/sequential.rs | 2 +- src/runner/implementations/std_runner.rs | 1 + src/runner/implementations/yastl.rs | 1 + src/runner/parallel_runner.rs | 2 +- 11 files changed, 36 insertions(+), 43 deletions(-) diff --git a/src/computational_variants/par.rs b/src/computational_variants/par.rs index 669ee757..2c364cec 100644 --- a/src/computational_variants/par.rs +++ b/src/computational_variants/par.rs @@ -39,6 +39,10 @@ where pub(crate) fn destruct(self) -> (R, Params, I) { (self.orchestrator, self.params, self.iter) } + + pub(crate) fn orchestrator(&self) -> &R { + &self.orchestrator + } } unsafe impl Send for Par diff --git a/src/executor/executor_with_diagnostics/parallel_executor.rs b/src/executor/executor_with_diagnostics/parallel_executor.rs index 3d0db78d..0227b2d2 100644 --- a/src/executor/executor_with_diagnostics/parallel_executor.rs +++ b/src/executor/executor_with_diagnostics/parallel_executor.rs @@ -57,6 +57,7 @@ use std::num::NonZeroUsize; /// // - [3]: 0, 0, 0, [] /// // - [4]: 0, 0, 0, [] /// ``` +#[derive(Clone)] pub struct ParallelExecutorWithDiagnostics where E: ParallelExecutor, diff --git a/src/executor/fixed_chunk_executor/parallel_executor.rs b/src/executor/fixed_chunk_executor/parallel_executor.rs index e0c4df8b..4a3283ab 100644 --- a/src/executor/fixed_chunk_executor/parallel_executor.rs +++ b/src/executor/fixed_chunk_executor/parallel_executor.rs @@ -18,6 +18,17 @@ pub struct FixedChunkRunner { current_chunk_size: AtomicUsize, } +impl Clone for FixedChunkRunner { + fn clone(&self) -> Self { + Self { + initial_len: self.initial_len.clone(), + resolved_chunk_size: self.resolved_chunk_size.clone(), + max_num_threads: self.max_num_threads.clone(), + current_chunk_size: self.current_chunk_size.load(Ordering::Relaxed).into(), + } + } +} + impl FixedChunkRunner { fn spawn_new(&self, num_spawned: usize, remaining: Option) -> bool { match (num_spawned, remaining) { diff --git a/src/executor/parallel_executor.rs b/src/executor/parallel_executor.rs index 62e7ad57..b44be6b0 100644 --- a/src/executor/parallel_executor.rs +++ b/src/executor/parallel_executor.rs @@ -9,7 +9,7 @@ use orx_concurrent_iter::ConcurrentIter; /// A parallel executor which is responsible for taking a computation defined as a composition /// of iterator methods, spawns threads, shares tasks and returns the result of the parallel /// execution. -pub trait ParallelExecutor: Sized + Sync + 'static { +pub trait ParallelExecutor: Sized + Sync + 'static + Clone { /// Data shared to the thread executors. type SharedState: Send + Sync; diff --git a/src/iter/recursive/rec_par_iter.rs b/src/iter/recursive/rec_par_iter.rs index 3847b992..d8db3218 100644 --- a/src/iter/recursive/rec_par_iter.rs +++ b/src/iter/recursive/rec_par_iter.rs @@ -3,7 +3,7 @@ use crate::{ computational_variants::{Par, ParMap, ParXap}, generic_values::{TransformableValues, runner_results::Infallible}, }; -use orx_concurrent_iter::{ConcurrentIter, IntoConcurrentIter, implementations::ConIterVec}; +use orx_concurrent_iter::{IntoConcurrentIter, implementations::ConIterVec}; use orx_concurrent_recursive_iter::{ConcurrentRecursiveIter, Queue}; type Rec = ConcurrentRecursiveIter; @@ -12,7 +12,7 @@ impl Par, R> where T: Send + Sync, E: Fn(&T, &Queue) + Sync, - R: ParallelRunner, + R: ParallelRunner + Clone, { /// Even with exact length, a recursive parallel iterator is much more dynamic than a flat parallel /// iterator. This dynamic nature of shrinking and growing concurrently requires a greater parallelization @@ -28,24 +28,18 @@ where /// [`into_par_rec_exact`]: crate::IntoParIterRec::into_par_rec_exact pub fn linearize(self) -> Par, R> { let params = self.params(); - - let items: Vec<_> = self.map(|x| x).collect(); + let orchestrator = self.orchestrator().clone(); + let items: Vec<_> = self.collect(); let iter = items.into_con_iter(); - - // a - todo!() - // let (orchestrator, params, iter) = self.destruct(); - // let items = collect_items(iter); - // let iter = items.into_con_iter(); - // Par::new(orchestrator, params, iter) + Par::new(orchestrator, params, iter) } } impl ParMap, O, M1, R> where - T: Send, + T: Send + Sync, E: Fn(&T, &Queue) + Sync, - R: ParallelRunner, + R: ParallelRunner + Clone, M1: Fn(T) -> O + Sync, { /// Even with exact length, a recursive parallel iterator is much more dynamic than a flat parallel @@ -62,7 +56,8 @@ where /// [`into_par_rec_exact`]: crate::IntoParIterRec::into_par_rec_exact pub fn linearize(self) -> ParMap, O, M1, R> { let (orchestrator, params, iter, map1) = self.destruct(); - let items = collect_items(iter); + let par = Par::new(orchestrator.clone(), params, iter); + let items: Vec<_> = par.collect(); let iter = items.into_con_iter(); ParMap::new(orchestrator, params, iter, map1) } @@ -70,9 +65,9 @@ where impl ParXap, Vo, X1, R> where - T: Send, + T: Send + Sync, E: Fn(&T, &Queue) + Sync, - R: ParallelRunner, + R: ParallelRunner + Clone, X1: Fn(T) -> Vo + Sync, Vo: TransformableValues, { @@ -90,31 +85,9 @@ where /// [`into_par_rec_exact`]: crate::IntoParIterRec::into_par_rec_exact pub fn linearize(self) -> ParXap, Vo, X1, R> { let (orchestrator, params, iter, xap1) = self.destruct(); - let items = collect_items(iter); + let par = Par::new(orchestrator.clone(), params, iter); + let items: Vec<_> = par.collect(); let iter = items.into_con_iter(); ParXap::new(orchestrator, params, iter, xap1) } } - -fn collect_items(iter: Rec) -> Vec -where - T: Send, - E: Fn(&T, &Queue) + Sync, -{ - match iter.try_get_len() { - Some(len) => { - let mut items = Vec::with_capacity(len); - items.extend(iter.into_seq_iter()); - items - } - None => iter.into_seq_iter().collect(), - } -} - -fn collect_items_par(iter: Rec) -> Vec -where - T: Send, - E: Fn(&T, &Queue) + Sync, -{ - todo!() -} diff --git a/src/runner/implementations/pond.rs b/src/runner/implementations/pond.rs index be8bf8ed..fd79f9c1 100644 --- a/src/runner/implementations/pond.rs +++ b/src/runner/implementations/pond.rs @@ -10,6 +10,7 @@ use pond::{Pool, Scope}; /// /// Following constructor of the `pond::Pool` is made available to `PondPool`: /// * [`PondPool::new_threads_unbounded`] +#[derive(Clone)] pub struct PondPool(Pool, NonZeroUsize); impl PondPool { diff --git a/src/runner/implementations/runner_with_pool.rs b/src/runner/implementations/runner_with_pool.rs index 38cf15da..16eba707 100644 --- a/src/runner/implementations/runner_with_pool.rs +++ b/src/runner/implementations/runner_with_pool.rs @@ -94,6 +94,7 @@ use core::marker::PhantomData; /// assert_eq!(&expected, &result); /// } /// ``` +#[derive(Clone)] pub struct RunnerWithPool where P: ParThreadPool, diff --git a/src/runner/implementations/sequential.rs b/src/runner/implementations/sequential.rs index eb09a8e0..8275d09d 100644 --- a/src/runner/implementations/sequential.rs +++ b/src/runner/implementations/sequential.rs @@ -10,7 +10,7 @@ use core::num::NonZeroUsize; /// /// [`max_num_threads`]: ParThreadPool::max_num_threads /// [`with_runner`]: crate::ParIter::with_runner -#[derive(Default)] +#[derive(Default, Clone)] pub struct SequentialPool; impl ParThreadPool for SequentialPool { diff --git a/src/runner/implementations/std_runner.rs b/src/runner/implementations/std_runner.rs index d2c17cac..8bd9bdfc 100644 --- a/src/runner/implementations/std_runner.rs +++ b/src/runner/implementations/std_runner.rs @@ -18,6 +18,7 @@ const MAX_UNSET_NUM_THREADS: NonZeroUsize = NonZeroUsize::new(8).expect(">0"); /// /// [`max_num_threads`]: ParThreadPool::max_num_threads /// [`with_runner`]: crate::ParIter::with_runner +#[derive(Clone)] pub struct StdDefaultPool { max_num_threads: NonZeroUsize, } diff --git a/src/runner/implementations/yastl.rs b/src/runner/implementations/yastl.rs index 77ce5094..92c03bdb 100644 --- a/src/runner/implementations/yastl.rs +++ b/src/runner/implementations/yastl.rs @@ -11,6 +11,7 @@ use yastl::{Pool, Scope, ThreadConfig}; /// Two constructors of the `yastl::Pool` are made available to `YastlPool`: /// * [`YastlPool::new`] /// * [`YastlPool::with_config`] +#[derive(Clone)] pub struct YastlPool(Pool, NonZeroUsize); impl YastlPool { diff --git a/src/runner/parallel_runner.rs b/src/runner/parallel_runner.rs index 6cc9fff7..f2f1281f 100644 --- a/src/runner/parallel_runner.rs +++ b/src/runner/parallel_runner.rs @@ -147,7 +147,7 @@ pub(crate) type ThreadRunnerOf = // auto impl for &mut pool -impl ParallelRunner for &'_ mut O +impl<'a, O> ParallelRunner for &'a mut O where O: ParallelRunner, { From 8fbb8cd5ad3a11a47a93bc91ac57ed0f120d14db Mon Sep 17 00:00:00 2001 From: orxfun Date: Mon, 27 Oct 2025 16:01:39 +0100 Subject: [PATCH 79/96] revise linearize documentation --- src/iter/recursive/rec_par_iter.rs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/iter/recursive/rec_par_iter.rs b/src/iter/recursive/rec_par_iter.rs index d8db3218..492a87a6 100644 --- a/src/iter/recursive/rec_par_iter.rs +++ b/src/iter/recursive/rec_par_iter.rs @@ -19,8 +19,9 @@ where /// overhead. An alternative approach is to eagerly discover all tasks and then perform the parallel /// computation over the flattened input of tasks. /// - /// This might increase performance in certain cases; however, requires storing the flattened tasks. - /// Therefore, it fits best to situations where the input elements are not very large. + /// The `linearize` approach works in two parallelization phases: + /// * first phase to linearize the inputs in parallel over the non-linear data, and + /// * second phase to perform the computation in parallel over the linear data. /// /// See [`into_par_rec`] and [`into_par_rec_exact`] for examples. /// @@ -47,8 +48,9 @@ where /// overhead. An alternative approach is to eagerly discover all tasks and then perform the parallel /// computation over the flattened input of tasks. /// - /// This might increase performance in certain cases; however, requires storing the flattened tasks. - /// Therefore, it fits best to situations where the input elements are not very large. + /// The `linearize` approach works in two parallelization phases: + /// * first phase to linearize the inputs in parallel over the non-linear data, and + /// * second phase to perform the computation in parallel over the linear data. /// /// See [`into_par_rec`] and [`into_par_rec_exact`] for examples. /// @@ -76,8 +78,9 @@ where /// overhead. An alternative approach is to eagerly discover all tasks and then perform the parallel /// computation over the flattened input of tasks. /// - /// This might increase performance in certain cases; however, requires storing the flattened tasks. - /// Therefore, it fits best to situations where the input elements are not very large. + /// The `linearize` approach works in two parallelization phases: + /// * first phase to linearize the inputs in parallel over the non-linear data, and + /// * second phase to perform the computation in parallel over the linear data. /// /// See [`into_par_rec`] and [`into_par_rec_exact`] for examples. /// From 85b8eb9aab36b4cf957cef6744314d54f61fd044 Mon Sep 17 00:00:00 2001 From: orxfun Date: Mon, 27 Oct 2025 16:55:14 +0100 Subject: [PATCH 80/96] fix clippy --- src/executor/fixed_chunk_executor/parallel_executor.rs | 6 +++--- src/runner/parallel_runner.rs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/executor/fixed_chunk_executor/parallel_executor.rs b/src/executor/fixed_chunk_executor/parallel_executor.rs index 4a3283ab..12e75cd2 100644 --- a/src/executor/fixed_chunk_executor/parallel_executor.rs +++ b/src/executor/fixed_chunk_executor/parallel_executor.rs @@ -21,9 +21,9 @@ pub struct FixedChunkRunner { impl Clone for FixedChunkRunner { fn clone(&self) -> Self { Self { - initial_len: self.initial_len.clone(), - resolved_chunk_size: self.resolved_chunk_size.clone(), - max_num_threads: self.max_num_threads.clone(), + initial_len: self.initial_len, + resolved_chunk_size: self.resolved_chunk_size, + max_num_threads: self.max_num_threads, current_chunk_size: self.current_chunk_size.load(Ordering::Relaxed).into(), } } diff --git a/src/runner/parallel_runner.rs b/src/runner/parallel_runner.rs index f2f1281f..6cc9fff7 100644 --- a/src/runner/parallel_runner.rs +++ b/src/runner/parallel_runner.rs @@ -147,7 +147,7 @@ pub(crate) type ThreadRunnerOf = // auto impl for &mut pool -impl<'a, O> ParallelRunner for &'a mut O +impl ParallelRunner for &'_ mut O where O: ParallelRunner, { From 890a6a67011a4f084d277d7bcc18588146ab24b5 Mon Sep 17 00:00:00 2001 From: orxfun Date: Mon, 27 Oct 2025 17:10:45 +0100 Subject: [PATCH 81/96] impact of chunk sizes on performance subsection is added --- README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/README.md b/README.md index 3aca2e35..3888a69e 100644 --- a/README.md +++ b/README.md @@ -190,6 +190,8 @@ For more details, you may see the [parallelization_on_tree](https://github.com/o ## Performance and Benchmarks +*Please also see [impact of ChunkSize on performance](#impact-of-chunksize-on-performance) section.* + You may find some sample parallel programs in [examples](https://github.com/orxfun/orx-parallel/blob/main/examples) directory. These examples allow to express parallel computations as iterator method compositions and run quick experiments with different approaches. Examples use `GenericIterator`. As the name suggests, it is a generalization of sequential iterator, rayon's parallel iterator and orx-parallel's parallel iterator, and hence, allows for convenient experiments. You may play with the code, update the tested computations and run these examples by including **generic_iterator** feature, such as: `cargo run --release --features generic_iterator --example benchmark_collect -- --len 123456 --num-repetitions 10` @@ -457,6 +459,18 @@ This is guaranteed by the fact that both consuming computation calls and configu Additionally, maximum number of threads that can be used by parallel computations can be globally bounded by the environment variable `ORX_PARALLEL_MAX_NUM_THREADS`. Please see the corresponding [example](https://github.com/orxfun/orx-parallel/blob/main/examples/max_num_threads_config.rs) for details. +### Impact of `ChunkSize` on Performance + +It is more straightforward to estimate the impact of number of threads on computation time. The impact of chunk size might be more complicated while it can be significant. + +As a rule of thumb, we want a chunk size that is **just large enough** to mitigate the parallelization overhead but not larger. + +When computation on each item is long, parallelization overhead is negligible, and a safe chunk size choice would then be **1**. + +The default configuration `par.chunk_size(ChunkSize::Auto)` or `par.chunk_size(0)` uses a heuristic to solve this tradeoff. A difficult case for the heuristic (at least for now, see the [discussion](https://github.com/orxfun/orx-parallel/discussions/26)) is when the tasks are significantly heterogeneous. + +The **best way to deal with heterogeneity** is to have `par.chunk_size(1)`. You may of course test larger chunk sizes to optimize the computation for your data. + ## Runner: Pools and Executors From 9c2a85ee0bcaa3df17b6950abf626b635288b95a Mon Sep 17 00:00:00 2001 From: orxfun Date: Mon, 27 Oct 2025 17:22:02 +0100 Subject: [PATCH 82/96] revise impact on chunk size documentation --- README.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 3888a69e..bcb8d9a8 100644 --- a/README.md +++ b/README.md @@ -461,15 +461,19 @@ Additionally, maximum number of threads that can be used by parallel computation ### Impact of `ChunkSize` on Performance -It is more straightforward to estimate the impact of number of threads on computation time. The impact of chunk size might be more complicated while it can be significant. +The impact of the chunk size on performance might be significant. -As a rule of thumb, we want a chunk size that is **just large enough** to mitigate the parallelization overhead but not larger. +Our objective is to minimize the sum of two computational costs: +* parallelization overhead => it gets smaller as chunk size gets greater +* cost of heterogeneity => it gets larger as chunk size gets greater -When computation on each item is long, parallelization overhead is negligible, and a safe chunk size choice would then be **1**. +Firstly, when computation on each item is sufficiently long, parallelization overhead is negligible. Here, we want to make sure that we do not have heterogeneity cost. Therefore, a safe chunk size choice would then be one, `par.chunk_size(1)`. -The default configuration `par.chunk_size(ChunkSize::Auto)` or `par.chunk_size(0)` uses a heuristic to solve this tradeoff. A difficult case for the heuristic (at least for now, see the [discussion](https://github.com/orxfun/orx-parallel/discussions/26)) is when the tasks are significantly heterogeneous. +Otherwise, our choice depends on the use case. As a rule of thumb, we want a chunk size that is **just large enough** to mitigate the parallelization overhead but not larger so that we do not suffer from heterogeneity. -The **best way to deal with heterogeneity** is to have `par.chunk_size(1)`. You may of course test larger chunk sizes to optimize the computation for your data. +The default configuration `par.chunk_size(ChunkSize::Auto)` or `par.chunk_size(0)` uses a heuristic to solve this tradeoff. A difficult case for the current version is when the tasks are significantly heterogeneous (see the [discussion](https://github.com/orxfun/orx-parallel/discussions/26) for future improvements). + +As described above, the **best way to deal with heterogeneity** is to have `par.chunk_size(1)`. You may of course test larger chunk sizes to optimize the computation for your data. ## Runner: Pools and Executors From b4a081412fee9f7d9da8d1839bb48e0387da1360 Mon Sep 17 00:00:00 2001 From: orxfun Date: Tue, 28 Oct 2025 09:17:51 +0100 Subject: [PATCH 83/96] revise documentation --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index bcb8d9a8..19347705 100644 --- a/README.md +++ b/README.md @@ -467,6 +467,10 @@ Our objective is to minimize the sum of two computational costs: * parallelization overhead => it gets smaller as chunk size gets greater * cost of heterogeneity => it gets larger as chunk size gets greater +Parallelization overhead can further be divided into two: +* concurrent state update: This often corresponds to one atomic update per chunk. It might be significant if our computation is very small such as `input.par().sum()`. Otherwise, cost of atomic update could be negligible. +* false sharing: This is relevant only if we are one-to-one mapping an input and collecting the results such as `input.par().map(|x| x.to_string()).collect()`. Here, the performance might suffer from false sharing when the chunk size × output size is not large enough. You may also see [false sharing](https://docs.rs/orx-concurrent-bag/latest/orx_concurrent_bag/#false-sharing) section for `ConcurrentBag`. + Firstly, when computation on each item is sufficiently long, parallelization overhead is negligible. Here, we want to make sure that we do not have heterogeneity cost. Therefore, a safe chunk size choice would then be one, `par.chunk_size(1)`. Otherwise, our choice depends on the use case. As a rule of thumb, we want a chunk size that is **just large enough** to mitigate the parallelization overhead but not larger so that we do not suffer from heterogeneity. From faa1d4afbf649e37ca48d7a18733d3b01c342fe1 Mon Sep 17 00:00:00 2001 From: orxfun Date: Tue, 28 Oct 2025 09:27:38 +0100 Subject: [PATCH 84/96] detail writing part of chunk size performance notes --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 19347705..9efa73c3 100644 --- a/README.md +++ b/README.md @@ -468,8 +468,8 @@ Our objective is to minimize the sum of two computational costs: * cost of heterogeneity => it gets larger as chunk size gets greater Parallelization overhead can further be divided into two: -* concurrent state update: This often corresponds to one atomic update per chunk. It might be significant if our computation is very small such as `input.par().sum()`. Otherwise, cost of atomic update could be negligible. -* false sharing: This is relevant only if we are one-to-one mapping an input and collecting the results such as `input.par().map(|x| x.to_string()).collect()`. Here, the performance might suffer from false sharing when the chunk size × output size is not large enough. You may also see [false sharing](https://docs.rs/orx-concurrent-bag/latest/orx_concurrent_bag/#false-sharing) section for `ConcurrentBag`. +* concurrent state update: This often corresponds to one atomic update per chunk. It may be significant if our computation is very small such as `input.par().sum()`. Otherwise, cost of atomic update could be negligible. +* false sharing: This is relevant only if we are writing results. For instance, when we are one-to-one mapping an input and collecting the results such as `input.par().map(|x| x.to_string()).collect()`, or if are writing with mut references such as `input.par().for_each(|x| *x += 1)`. Here, the performance might suffer from false sharing when the `chunk size × size of output item` is not large enough. You may also see [false sharing](https://docs.rs/orx-concurrent-bag/latest/orx_concurrent_bag/#false-sharing) section for `ConcurrentBag`. Firstly, when computation on each item is sufficiently long, parallelization overhead is negligible. Here, we want to make sure that we do not have heterogeneity cost. Therefore, a safe chunk size choice would then be one, `par.chunk_size(1)`. From 70e8a240969ebd3214d173b9750d9d5e9371aaee Mon Sep 17 00:00:00 2001 From: orxfun Date: Tue, 28 Oct 2025 09:32:53 +0100 Subject: [PATCH 85/96] revise documentation --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9efa73c3..9356bd0b 100644 --- a/README.md +++ b/README.md @@ -471,7 +471,7 @@ Parallelization overhead can further be divided into two: * concurrent state update: This often corresponds to one atomic update per chunk. It may be significant if our computation is very small such as `input.par().sum()`. Otherwise, cost of atomic update could be negligible. * false sharing: This is relevant only if we are writing results. For instance, when we are one-to-one mapping an input and collecting the results such as `input.par().map(|x| x.to_string()).collect()`, or if are writing with mut references such as `input.par().for_each(|x| *x += 1)`. Here, the performance might suffer from false sharing when the `chunk size × size of output item` is not large enough. You may also see [false sharing](https://docs.rs/orx-concurrent-bag/latest/orx_concurrent_bag/#false-sharing) section for `ConcurrentBag`. -Firstly, when computation on each item is sufficiently long, parallelization overhead is negligible. Here, we want to make sure that we do not have heterogeneity cost. Therefore, a safe chunk size choice would then be one, `par.chunk_size(1)`. +In either case, when computation on each item is sufficiently long, parallelization overhead is negligible. Here, we want to make sure that we do not have heterogeneity cost. Therefore, a safe chunk size choice would be one, `par.chunk_size(1)`. Otherwise, our choice depends on the use case. As a rule of thumb, we want a chunk size that is **just large enough** to mitigate the parallelization overhead but not larger so that we do not suffer from heterogeneity. From fd5a4f5cbf6a95b428874f663b0bac8032dd3d9d Mon Sep 17 00:00:00 2001 From: orxfun Date: Tue, 28 Oct 2025 19:18:43 +0100 Subject: [PATCH 86/96] restore no-std --- src/executor/executor_with_diagnostics/shared_state.rs | 2 ++ src/executor/executor_with_diagnostics/thread_executor.rs | 2 ++ src/executor/mod.rs | 2 ++ src/iter/recursive/rec_par_iter.rs | 1 + src/lib.rs | 4 ++-- 5 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/executor/executor_with_diagnostics/shared_state.rs b/src/executor/executor_with_diagnostics/shared_state.rs index b5bc58a1..c28bf3b6 100644 --- a/src/executor/executor_with_diagnostics/shared_state.rs +++ b/src/executor/executor_with_diagnostics/shared_state.rs @@ -1,4 +1,6 @@ +use alloc::vec::Vec; use orx_concurrent_bag::ConcurrentBag; +use std::println; pub struct SharedStateWithDiagnostics { inner: S, diff --git a/src/executor/executor_with_diagnostics/thread_executor.rs b/src/executor/executor_with_diagnostics/thread_executor.rs index 36c47810..7bcfab1b 100644 --- a/src/executor/executor_with_diagnostics/thread_executor.rs +++ b/src/executor/executor_with_diagnostics/thread_executor.rs @@ -1,5 +1,7 @@ use super::shared_state::SharedStateWithDiagnostics; use crate::{ParallelExecutor, ThreadExecutor}; +use alloc::vec; +use alloc::vec::Vec; use orx_concurrent_iter::ConcurrentIter; pub struct ThreadExecutorWithDiagnostics diff --git a/src/executor/mod.rs b/src/executor/mod.rs index e38aaa25..2a6eb441 100644 --- a/src/executor/mod.rs +++ b/src/executor/mod.rs @@ -1,3 +1,4 @@ +#[cfg(feature = "std")] mod executor_with_diagnostics; mod fixed_chunk_executor; pub(crate) mod parallel_compute; @@ -5,6 +6,7 @@ mod parallel_executor; mod thread_compute; mod thread_executor; +#[cfg(feature = "std")] pub use executor_with_diagnostics::ParallelExecutorWithDiagnostics; pub use parallel_executor::ParallelExecutor; pub use thread_executor::ThreadExecutor; diff --git a/src/iter/recursive/rec_par_iter.rs b/src/iter/recursive/rec_par_iter.rs index 492a87a6..1d15c1a6 100644 --- a/src/iter/recursive/rec_par_iter.rs +++ b/src/iter/recursive/rec_par_iter.rs @@ -3,6 +3,7 @@ use crate::{ computational_variants::{Par, ParMap, ParXap}, generic_values::{TransformableValues, runner_results::Infallible}, }; +use alloc::vec::Vec; use orx_concurrent_iter::{IntoConcurrentIter, implementations::ConIterVec}; use orx_concurrent_recursive_iter::{ConcurrentRecursiveIter, Queue}; diff --git a/src/lib.rs b/src/lib.rs index 8b637a3a..5e68d8d3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,11 +10,11 @@ clippy::missing_panics_doc, clippy::todo )] -// #![no_std] +#![no_std] extern crate alloc; -// #[cfg(any(test, feature = "std"))] +#[cfg(any(test, feature = "std"))] extern crate std; mod collect_into; From 073c574916e1c02ef95ca520c91b5a1f07385061 Mon Sep 17 00:00:00 2001 From: orxfun Date: Tue, 28 Oct 2025 19:28:47 +0100 Subject: [PATCH 87/96] fix tests for no-std --- README.md | 7 +++++-- src/lib.rs | 8 ++++---- src/par_iter.rs | 14 ++++++++++---- src/runner/implementations/runner_with_pool.rs | 17 ++++++++++------- 4 files changed, 29 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 9356bd0b..9fd3d240 100644 --- a/README.md +++ b/README.md @@ -519,8 +519,11 @@ let inputs: Vec<_> = (0..42).collect(); let sum = inputs.par().sum(); // equivalent to: -let sum2 = inputs.par().with_pool(StdDefaultPool::default()).sum(); -assert_eq!(sum, sum2); +#[cfg(feature = "std")] +{ + let sum2 = inputs.par().with_pool(StdDefaultPool::default()).sum(); + assert_eq!(sum, sum2); +} #[cfg(feature = "scoped_threadpool")] { diff --git a/src/lib.rs b/src/lib.rs index 5e68d8d3..e4eb11e2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -63,9 +63,7 @@ pub use orx_concurrent_recursive_iter::Queue; // export pub use collect_into::ParCollectInto; -pub use executor::{ - DefaultExecutor, ParallelExecutor, ParallelExecutorWithDiagnostics, ThreadExecutor, -}; +pub use executor::{DefaultExecutor, ParallelExecutor, ThreadExecutor}; pub use into_par_iter::IntoParIter; pub use iter::IntoParIterRec; pub use iter_into_par_iter::IterIntoParIter; @@ -78,10 +76,12 @@ pub use parallelizable::Parallelizable; pub use parallelizable_collection::ParallelizableCollection; pub use parallelizable_collection_mut::ParallelizableCollectionMut; pub use parameters::{ChunkSize, IterationOrder, NumThreads, Params}; +pub use runner::{DefaultPool, DefaultRunner, ParallelRunner, RunnerWithPool, SequentialPool}; pub use special_type_sets::Sum; pub use using::ParIterUsing; -pub use runner::{DefaultPool, DefaultRunner, ParallelRunner, RunnerWithPool, SequentialPool}; +#[cfg(feature = "std")] +pub use executor::ParallelExecutorWithDiagnostics; #[cfg(feature = "pond")] pub use runner::PondPool; diff --git a/src/par_iter.rs b/src/par_iter.rs index 329fde74..4d766fa5 100644 --- a/src/par_iter.rs +++ b/src/par_iter.rs @@ -280,8 +280,11 @@ where /// let sum = inputs.par().sum(); /// /// // equivalent to: - /// let sum2 = inputs.par().with_runner(RunnerWithPool::from(StdDefaultPool::default())).sum(); - /// assert_eq!(sum, sum2); + /// #[cfg(feature = "std")] + /// { + /// let sum2 = inputs.par().with_runner(RunnerWithPool::from(StdDefaultPool::default())).sum(); + /// assert_eq!(sum, sum2); + /// } /// /// #[cfg(feature = "scoped_threadpool")] /// { @@ -345,8 +348,11 @@ where /// let sum = inputs.par().sum(); /// /// // equivalent to: - /// let sum2 = inputs.par().with_pool(StdDefaultPool::default()).sum(); - /// assert_eq!(sum, sum2); + /// #[cfg(feature = "std")] + /// { + /// let sum2 = inputs.par().with_pool(StdDefaultPool::default()).sum(); + /// assert_eq!(sum, sum2); + /// } /// /// #[cfg(feature = "scoped_threadpool")] /// { diff --git a/src/runner/implementations/runner_with_pool.rs b/src/runner/implementations/runner_with_pool.rs index 16eba707..7c037206 100644 --- a/src/runner/implementations/runner_with_pool.rs +++ b/src/runner/implementations/runner_with_pool.rs @@ -1,7 +1,6 @@ -use crate::{ - DefaultExecutor, ParThreadPool, ParallelExecutor, ParallelExecutorWithDiagnostics, - runner::ParallelRunner, -}; +#[cfg(feature = "std")] +use crate::executor::ParallelExecutorWithDiagnostics; +use crate::{DefaultExecutor, ParThreadPool, ParallelExecutor, runner::ParallelRunner}; use core::marker::PhantomData; /// Parallel runner with a given pool of type `P` and parallel executor of `R`. @@ -39,9 +38,12 @@ use core::marker::PhantomData; /// let expected = run_with_runner(runner, input); /// /// // uses native threads -/// let runner = RunnerWithPool::from(StdDefaultPool::default()); -/// let result = run_with_runner(runner, input); -/// assert_eq!(&expected, &result); +/// #[cfg(feature = "std")] +/// { +/// let runner = RunnerWithPool::from(StdDefaultPool::default()); +/// let result = run_with_runner(runner, input); +/// assert_eq!(&expected, &result); +/// } /// /// // uses rayon-core ThreadPool with 8 threads /// #[cfg(feature = "rayon-core")] @@ -228,6 +230,7 @@ where /// // - [3]: 0, 0, 0, [] /// // - [4]: 0, 0, 0, [] /// ``` + #[cfg(feature = "std")] pub fn with_diagnostics(self) -> RunnerWithPool> { RunnerWithPool { pool: self.pool, From 4ce2bfdee0e724b550d16753ab5e4bba66ad2b30 Mon Sep 17 00:00:00 2001 From: orxfun Date: Tue, 28 Oct 2025 19:30:32 +0100 Subject: [PATCH 88/96] revert bench --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 1e689a50..4199728a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,7 +46,7 @@ rayon = "1.11.0" test-case = "3.3.1" [[bench]] -name = "rec_iter_map_sum" +name = "find_iter_into_par" harness = false [package.metadata.docs.rs] From 9e41be319ec0db8b536ac9b9a983b5ef37583609 Mon Sep 17 00:00:00 2001 From: orxfun Date: Tue, 28 Oct 2025 19:34:52 +0100 Subject: [PATCH 89/96] fix pond clone --- src/runner/implementations/pond.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/runner/implementations/pond.rs b/src/runner/implementations/pond.rs index fd79f9c1..be8bf8ed 100644 --- a/src/runner/implementations/pond.rs +++ b/src/runner/implementations/pond.rs @@ -10,7 +10,6 @@ use pond::{Pool, Scope}; /// /// Following constructor of the `pond::Pool` is made available to `PondPool`: /// * [`PondPool::new_threads_unbounded`] -#[derive(Clone)] pub struct PondPool(Pool, NonZeroUsize); impl PondPool { From 7aff5741eb7a069736cdd52facf4568a2907950d Mon Sep 17 00:00:00 2001 From: Ugur Arikan Date: Tue, 28 Oct 2025 19:35:32 +0100 Subject: [PATCH 90/96] Update CI features in workflow configuration --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 914ed8fc..d477f861 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,7 +16,7 @@ jobs: strategy: matrix: toolchain: ["stable"] - features: ["", "--features generic_iterator"] + features: ["", "--all-features", "no-default-features"] steps: - uses: actions/checkout@v4 From 1d6e75146d918c8c8c7a5907cc79da31a5e2ea25 Mon Sep 17 00:00:00 2001 From: Ugur Arikan Date: Tue, 28 Oct 2025 19:37:13 +0100 Subject: [PATCH 91/96] Update ci.yml --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d477f861..ea1c517d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,7 +16,7 @@ jobs: strategy: matrix: toolchain: ["stable"] - features: ["", "--all-features", "no-default-features"] + features: ["", "--all-features", "--no-default-features"] steps: - uses: actions/checkout@v4 From e383d058c51b5cd13d93bed8563156823ea1384b Mon Sep 17 00:00:00 2001 From: orxfun Date: Tue, 28 Oct 2025 19:45:10 +0100 Subject: [PATCH 92/96] fix test --- src/iter/recursive/into_par_rec_iter.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/iter/recursive/into_par_rec_iter.rs b/src/iter/recursive/into_par_rec_iter.rs index ff21b8e8..253cf68a 100644 --- a/src/iter/recursive/into_par_rec_iter.rs +++ b/src/iter/recursive/into_par_rec_iter.rs @@ -210,7 +210,6 @@ where /// } /// /// let sum = [&root].into_par_rec(extend_filtered).map(compute).sum(); - /// assert_eq!(sum, 499458); /// ``` fn into_par_rec( self, @@ -408,7 +407,6 @@ where /// } /// /// let sum = [&root].into_par_rec(extend_filtered).map(compute).sum(); - /// assert_eq!(sum, 499458); /// ``` fn into_par_rec_exact( self, From 14758fb3a44b3bad8233fdb0825ff218b3adc2dc Mon Sep 17 00:00:00 2001 From: orxfun Date: Tue, 28 Oct 2025 22:07:18 +0100 Subject: [PATCH 93/96] exclude poolite from miri tests as it fails --- src/runner/implementations/tests/poolite.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/runner/implementations/tests/poolite.rs b/src/runner/implementations/tests/poolite.rs index 67122a6e..3f63d092 100644 --- a/src/runner/implementations/tests/poolite.rs +++ b/src/runner/implementations/tests/poolite.rs @@ -8,6 +8,8 @@ const N: [usize; 2] = [37, 125]; #[cfg(not(miri))] const N: [usize; 2] = [1025, 4735]; +// `poolite::Builder::new()` fails miri test +#[cfg(not(miri))] #[test_matrix( [0, 1, N[0], N[1]], [1, 4], From ae70b170aeb587bbd3f9b344eda814cb043e9f28 Mon Sep 17 00:00:00 2001 From: orxfun Date: Tue, 28 Oct 2025 22:46:59 +0100 Subject: [PATCH 94/96] exclude scoped pool from miri as it fails --- src/runner/implementations/tests/scoped_pool.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/runner/implementations/tests/scoped_pool.rs b/src/runner/implementations/tests/scoped_pool.rs index bab42948..efab204b 100644 --- a/src/runner/implementations/tests/scoped_pool.rs +++ b/src/runner/implementations/tests/scoped_pool.rs @@ -8,6 +8,8 @@ const N: [usize; 2] = [37, 125]; #[cfg(not(miri))] const N: [usize; 2] = [1025, 4735]; +// `scoped_pool::Pool::new(nt)` fails miri test +#[cfg(not(miri))] #[test_matrix( [0, 1, N[0], N[1]], [1, 4], From 6575743092eae1f388301d9b1c9aed3653bc6ea8 Mon Sep 17 00:00:00 2001 From: orxfun Date: Thu, 30 Oct 2025 22:59:50 +0100 Subject: [PATCH 95/96] clippy fix --- src/env.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/env.rs b/src/env.rs index 24635f71..985c5325 100644 --- a/src/env.rs +++ b/src/env.rs @@ -7,9 +7,8 @@ pub fn max_num_threads_by_env_variable() -> Option { #[cfg(feature = "std")] match std::env::var(MAX_NUM_THREADS_ENV_VARIABLE) { Ok(s) => match s.parse::() { - Ok(0) => None, // consistent with .num_threads(0) representing no bound - Ok(x) => Some(NonZeroUsize::new(x).expect("x>0")), // set to a positive bound - Err(_e) => None, // not a number, ignored assuming no bound + Ok(x) => NonZeroUsize::new(x), // None if 0; Some(x) if x is set to a positive bound + Err(_e) => None, // not a number, ignored assuming no bound }, Err(_e) => None, // not set, no bound } From f0e861e23d67ee1a1f4fcb192b7daa7497fdf34b Mon Sep 17 00:00:00 2001 From: orxfun Date: Fri, 31 Oct 2025 12:13:40 +0100 Subject: [PATCH 96/96] exclude pool tests from miri tests --- README.md | 3 +++ src/par_iter.rs | 4 ++++ src/par_thread_pool.rs | 2 ++ src/runner/implementations/runner_with_pool.rs | 7 +++++++ src/runner/implementations/tests/pond.rs | 2 ++ src/runner/implementations/tests/scoped_threadpool.rs | 2 ++ src/runner/implementations/tests/yastl.rs | 2 ++ 7 files changed, 22 insertions(+) diff --git a/README.md b/README.md index 9fd3d240..a1798984 100644 --- a/README.md +++ b/README.md @@ -525,6 +525,7 @@ let sum = inputs.par().sum(); assert_eq!(sum, sum2); } +#[cfg(not(miri))] #[cfg(feature = "scoped_threadpool")] { let mut pool = scoped_threadpool::Pool::new(8); @@ -533,6 +534,7 @@ let sum = inputs.par().sum(); assert_eq!(sum, sum2); } +#[cfg(not(miri))] #[cfg(feature = "rayon-core")] { let pool = rayon_core::ThreadPoolBuilder::new() @@ -544,6 +546,7 @@ let sum = inputs.par().sum(); assert_eq!(sum, sum2); } +#[cfg(not(miri))] #[cfg(feature = "yastl")] { let pool = YastlPool::new(8); diff --git a/src/par_iter.rs b/src/par_iter.rs index 4d766fa5..01c7158c 100644 --- a/src/par_iter.rs +++ b/src/par_iter.rs @@ -286,6 +286,7 @@ where /// assert_eq!(sum, sum2); /// } /// + /// #[cfg(not(miri))] /// #[cfg(feature = "scoped_threadpool")] /// { /// let mut pool = scoped_threadpool::Pool::new(8); @@ -295,6 +296,7 @@ where /// assert_eq!(sum, sum2); /// } /// + /// #[cfg(not(miri))] /// #[cfg(feature = "rayon-core")] /// { /// let pool = rayon_core::ThreadPoolBuilder::new() @@ -354,6 +356,7 @@ where /// assert_eq!(sum, sum2); /// } /// + /// #[cfg(not(miri))] /// #[cfg(feature = "scoped_threadpool")] /// { /// let mut pool = scoped_threadpool::Pool::new(8); @@ -363,6 +366,7 @@ where /// assert_eq!(sum, sum2); /// } /// + /// #[cfg(not(miri))] /// #[cfg(feature = "rayon-core")] /// { /// let pool = rayon_core::ThreadPoolBuilder::new() diff --git a/src/par_thread_pool.rs b/src/par_thread_pool.rs index cad4278e..3433c661 100644 --- a/src/par_thread_pool.rs +++ b/src/par_thread_pool.rs @@ -40,6 +40,7 @@ use orx_concurrent_bag::ConcurrentBag; /// ``` /// use orx_parallel::*; /// +/// #[cfg(not(miri))] /// #[cfg(feature = "rayon-core")] /// { /// let pool = rayon::ThreadPoolBuilder::new() @@ -72,6 +73,7 @@ use orx_concurrent_bag::ConcurrentBag; /// ``` /// use orx_parallel::*; /// +/// #[cfg(not(miri))] /// #[cfg(feature = "scoped_threadpool")] /// { /// // creating a runner for the computation diff --git a/src/runner/implementations/runner_with_pool.rs b/src/runner/implementations/runner_with_pool.rs index 7c037206..e8b5cfc6 100644 --- a/src/runner/implementations/runner_with_pool.rs +++ b/src/runner/implementations/runner_with_pool.rs @@ -46,6 +46,7 @@ use core::marker::PhantomData; /// } /// /// // uses rayon-core ThreadPool with 8 threads +/// #[cfg(not(miri))] /// #[cfg(feature = "rayon-core")] /// { /// let pool = rayon_core::ThreadPoolBuilder::new() @@ -57,6 +58,7 @@ use core::marker::PhantomData; /// } /// /// // uses scoped-pool Pool with 8 threads +/// #[cfg(not(miri))] /// #[cfg(feature = "scoped-pool")] /// { /// let pool = scoped_pool::Pool::new(8); @@ -65,6 +67,7 @@ use core::marker::PhantomData; /// } /// /// // uses scoped_threadpool Pool with 8 threads +/// #[cfg(not(miri))] /// #[cfg(feature = "scoped_threadpool")] /// { /// let mut pool = scoped_threadpool::Pool::new(8); @@ -73,6 +76,7 @@ use core::marker::PhantomData; /// } /// /// // uses yastl Pool wrapped as YastlPool with 8 threads +/// #[cfg(not(miri))] /// #[cfg(feature = "yastl")] /// { /// let pool = YastlPool::new(8); @@ -81,6 +85,7 @@ use core::marker::PhantomData; /// } /// /// // uses pond Pool wrapped as PondPool with 8 threads +/// #[cfg(not(miri))] /// #[cfg(feature = "pond")] /// { /// let mut pool = PondPool::new_threads_unbounded(8); @@ -89,6 +94,7 @@ use core::marker::PhantomData; /// } /// /// // uses poolite Pool with 8 threads +/// #[cfg(not(miri))] /// #[cfg(feature = "poolite")] /// { /// let pool = poolite::Pool::with_builder(poolite::Builder::new().min(8).max(8)).unwrap(); @@ -153,6 +159,7 @@ where /// let vec: Vec<_> = (0..42).collect(); /// let input = vec.as_slice(); /// + /// #[cfg(not(miri))] /// #[cfg(feature = "rayon-core")] /// { /// let pool = rayon_core::ThreadPoolBuilder::new() diff --git a/src/runner/implementations/tests/pond.rs b/src/runner/implementations/tests/pond.rs index 4727956e..7df43027 100644 --- a/src/runner/implementations/tests/pond.rs +++ b/src/runner/implementations/tests/pond.rs @@ -10,6 +10,8 @@ const N: [usize; 2] = [37, 125]; #[cfg(not(miri))] const N: [usize; 2] = [1025, 4735]; +// TODO: miri test terminates with: the main thread terminated without waiting for all remaining threads +#[cfg(not(miri))] #[test_matrix( [0, 1, N[0], N[1]], [1, 4], diff --git a/src/runner/implementations/tests/scoped_threadpool.rs b/src/runner/implementations/tests/scoped_threadpool.rs index 47f0b2bb..fb31992c 100644 --- a/src/runner/implementations/tests/scoped_threadpool.rs +++ b/src/runner/implementations/tests/scoped_threadpool.rs @@ -8,6 +8,8 @@ const N: [usize; 2] = [37, 125]; #[cfg(not(miri))] const N: [usize; 2] = [1025, 4735]; +// TODO: miri test terminates with: the main thread terminated without waiting for all remaining threads +#[cfg(not(miri))] #[test_matrix( [0, 1, N[0], N[1]], [1, 4], diff --git a/src/runner/implementations/tests/yastl.rs b/src/runner/implementations/tests/yastl.rs index f20e8529..c7f4844b 100644 --- a/src/runner/implementations/tests/yastl.rs +++ b/src/runner/implementations/tests/yastl.rs @@ -11,6 +11,8 @@ const N: [usize; 2] = [37, 125]; #[cfg(not(miri))] const N: [usize; 2] = [1025, 4735]; +// TODO: miri test terminates with: the main thread terminated without waiting for all remaining threads +#[cfg(not(miri))] #[test_matrix( [0, 1, N[0], N[1]], [1, 4],