Skip to content

Commit 6d3f76a

Browse files
authored
Merge pull request #161 from orxfun/parallelization
Parallelization
2 parents 847af89 + 4bbc6a7 commit 6d3f76a

File tree

11 files changed

+754
-22
lines changed

11 files changed

+754
-22
lines changed

.github/workflows/ci.yml

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ jobs:
1616
strategy:
1717
matrix:
1818
toolchain: ["stable"]
19-
features: ["", "--features serde"]
19+
features: ["--features orx-parallel", "--features serde"]
20+
no_std_features: ["--features serde"]
2021

2122
steps:
2223
- uses: actions/checkout@v4
@@ -36,24 +37,24 @@ jobs:
3637
run: cargo install cargo-no-std-check
3738

3839
- name: Build
39-
run: cargo build --verbose ${{ matrix.features }}
40+
run: cargo build --no-default-features --verbose ${{ matrix.features }}
4041
- name: Build-32bit
41-
run: cargo build --verbose --target i686-unknown-linux-musl ${{ matrix.features }}
42+
run: cargo build --no-default-features --verbose --target i686-unknown-linux-musl ${{ matrix.features }}
4243
- name: Build-wasm
43-
run: cargo build --verbose --target wasm32v1-none ${{ matrix.features }}
44+
run: cargo build --no-default-features --verbose --target wasm32v1-none ${{ matrix.no_std_features }}
4445

4546
- name: Test
46-
run: cargo test --verbose ${{ matrix.features }}
47+
run: cargo test --no-default-features --verbose ${{ matrix.features }}
4748
- name: Test-32bit
48-
run: cargo test --verbose --target i686-unknown-linux-musl ${{ matrix.features }}
49+
run: cargo test --no-default-features --verbose --target i686-unknown-linux-musl ${{ matrix.features }}
4950
- name: Check-wasm
50-
run: cargo check --verbose --target wasm32v1-none ${{ matrix.features }}
51+
run: cargo check --no-default-features --verbose --target wasm32v1-none ${{ matrix.no_std_features }}
5152

5253
- name: Clippy
53-
run: cargo clippy ${{ matrix.features }} -- -D warnings --verbose
54+
run: cargo clippy --no-default-features ${{ matrix.features }} -- -D warnings --verbose
5455

5556
- name: Miri
56-
run: cargo +nightly miri test --verbose ${{ matrix.features }}
57+
run: cargo +nightly miri test --lib --bins --tests --no-default-features --verbose ${{ matrix.features }}
5758

5859
- name: NoStd
59-
run: cargo +nightly no-std-check ${{ matrix.features }}
60+
run: cargo +nightly no-std-check --no-default-features ${{ matrix.no_std_features }}

Cargo.toml

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
[package]
22
name = "orx-tree"
3-
version = "1.5.0"
3+
version = "1.6.0"
44
edition = "2024"
55
authors = ["orxfun <[email protected]>"]
6-
description = "A beautiful tree 🌳 with convenient and efficient growth, mutation and traversal features."
6+
description = "A beautiful tree 🌳 with convenient and efficient growth, mutation and traversal features with support for parallel computation."
77
license = "MIT OR Apache-2.0"
88
repository = "https://github.com/orxfun/orx-tree/"
99
keywords = ["tree", "data-structures", "traversal", "traverse", "binarytree"]
@@ -13,19 +13,26 @@ categories = ["data-structures", "algorithms", "rust-patterns", "no-std"]
1313
orx-iterable = { version = "1.3.0", default-features = false }
1414
orx-pseudo-default = { version = "2.1.0", default-features = false }
1515
orx-pinned-vec = "3.16.0"
16-
orx-split-vec = "3.16.0"
17-
orx-selfref-col = "2.8.0"
1816
orx-self-or = "1.2.0"
1917
serde = { version = "1.0.219", optional = true, default-features = false }
20-
18+
orx-split-vec = { version = "3.17.0", default-features = false }
19+
orx-selfref-col = { version = "2.9.0", default-features = false }
20+
orx-concurrent-iter = { version = "2.1.0", default-features = false }
21+
orx-parallel = { version = "2.1.0", default-features = false, optional = true }
2122

2223
[dev-dependencies]
23-
test-case = { version = "3.3.1", default-features = false }
24+
clap = { version = "4.5.38", features = ["derive"] }
25+
criterion = "0.5.1"
26+
rayon = { version = "1.10.0" }
2427
serde_json = { version = "1.0.140", default-features = false, features = [
2528
"std",
2629
] }
30+
test-case = { version = "3.3.1", default-features = false }
2731

2832
[features]
29-
default = []
30-
std = []
33+
default = ["orx-parallel"]
3134
serde = ["dep:serde"]
35+
36+
[[bench]]
37+
name = "parallelization_ref"
38+
harness = false

README.md

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
[![orx-tree crate](https://img.shields.io/crates/d/orx-tree.svg)](https://crates.io/crates/orx-tree)
55
[![orx-tree documentation](https://docs.rs/orx-tree/badge.svg)](https://docs.rs/orx-tree)
66

7-
A beautiful tree 🌳 with convenient and efficient growth, mutation and traversal features.
7+
A beautiful tree 🌳 with convenient and efficient growth, mutation and traversal features with support for parallel computation.
88

99
## Features
1010

@@ -130,9 +130,18 @@ Alternatively, we can turn a mutable node into an [`into_walk`](https://docs.rs/
130130
* We can iterate over the removed nodes in the order of the generic traversal parameter and use the data however we need.
131131
* Or we can attach the removed subtree at a desired position of another tree by passing it to methods such as [`push_child_tree(subtree)`](https://docs.rs/orx-tree/latest/orx_tree/struct.NodeMut.html#method.push_child_tree).
132132

133-
## Opt-in Features
133+
## Features
134+
135+
* **orx-parallel**: Tree allows efficient parallel processing through [concurrent iterators](https://crates.io/crates/orx-concurrent-iter) and [parallel iterators](https://crates.io/crates/orx-parallel).
136+
* This feature is added as default and requires **std**; hence, please use `cargo add orx-tree --no-default-features` for **no-std** use cases.
137+
* Currently, parallel iteration over all nodes of the tree in arbitrary order is supported by methods [`par`](https://docs.rs/orx-tree/latest/orx_tree/struct.Tree.html#method.par) and [`into_par`](https://docs.rs/orx-tree/latest/orx_tree/struct.Tree.html#method.into_par).
138+
* Parallelization of all walks or traversals in particular order are under development.
139+
* Parallelization examples can be found in [`demo_parallelization`](https://github.com/orxfun/orx-tree/blob/main/examples/demo_parallelization.rs) example.
140+
* Importantly note that the tree defines its own concurrent iterators, and hence, allows for efficient computation, which is often not possible with generic implementations such as rayon's `par_bridge`. In order to check the impact in performance, you may use the lightweight benchmark example [`bench_parallelization`](https://github.com/orxfun/orx-linked-list/blob/main/examples/bench_parallelization.rs):
141+
* `Sequential computation over Tree : 18.96s`
142+
* `Parallelized over Tree using orx-parallel : 6.02s`
143+
* `Parallelized over Tree using rayon's par-bridge : 81.10s`
134144

135-
* **std**: This is a no-std crate by default, and hence, "std" feature needs to be included when necessary.
136145
* **serde**: Tree implements `Serialize` and `Deserialize` traits; the "serde" feature needs to be added when required. It uses a linearized representation of the tree as a [`DepthFirstSequence`](https://docs.rs/orx-tree/latest/orx_tree/struct.DepthFirstSequence.html). You may find de-serialization examples in the corresponding [test file](https://github.com/orxfun/orx-tree/blob/main/tests/serde.rs).
137146

138147
# Examples

benches/parallelization_owned.rs

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
use criterion::{BenchmarkId, Criterion, criterion_group, criterion_main};
2+
#[cfg(feature = "orx-parallel")]
3+
use orx_parallel::ParIter;
4+
use orx_tree::*;
5+
use rayon::iter::{ParallelBridge, ParallelIterator};
6+
7+
fn build_tree(n: usize) -> DynTree<String> {
8+
let mut tree = DynTree::new(0.to_string());
9+
let mut dfs = Traversal.dfs().over_nodes();
10+
while tree.len() < n {
11+
let root = tree.root();
12+
let x: Vec<_> = root.leaves_with(&mut dfs).map(|x| x.idx()).collect();
13+
for idx in x.iter() {
14+
let count = tree.len();
15+
let mut node = tree.node_mut(idx);
16+
let num_children = 20;
17+
for j in 0..num_children {
18+
node.push_child((count + j).to_string());
19+
}
20+
}
21+
}
22+
tree
23+
}
24+
25+
fn fibonacci(n: i64) -> i64 {
26+
let mut a = 0;
27+
let mut b = 1;
28+
for _ in 0..n {
29+
let c = a + b;
30+
a = b;
31+
b = c;
32+
}
33+
a
34+
}
35+
36+
fn tree_into_iter(tree: DynTree<String>) -> i64 {
37+
tree.into_iter()
38+
.map(|x| x.parse::<usize>().unwrap())
39+
.map(|x| fibonacci(x as i64 % 500))
40+
.sum()
41+
}
42+
43+
fn tree_into_dfs(mut tree: DynTree<String>) -> i64 {
44+
tree.root_mut()
45+
.into_walk::<Dfs>()
46+
.map(|x| x.parse::<usize>().unwrap())
47+
.map(|x| fibonacci(x as i64 % 500))
48+
.sum()
49+
}
50+
51+
fn tree_into_bfs(mut tree: DynTree<String>) -> i64 {
52+
tree.root_mut()
53+
.into_walk::<Bfs>()
54+
.map(|x| x.parse::<usize>().unwrap())
55+
.map(|x| fibonacci(x as i64 % 500))
56+
.sum()
57+
}
58+
59+
fn tree_into_par_x(tree: DynTree<String>) -> i64 {
60+
tree.into_par()
61+
.map(|x| x.parse::<usize>().unwrap())
62+
.map(|x| fibonacci(x as i64 % 500))
63+
.sum()
64+
}
65+
66+
fn tree_into_iter_rayon(tree: DynTree<String>) -> i64 {
67+
tree.into_iter()
68+
.par_bridge()
69+
.map(|x| x.parse::<usize>().unwrap())
70+
.map(|x| fibonacci(x as i64 % 500))
71+
.sum()
72+
}
73+
74+
fn bench(c: &mut Criterion) {
75+
let treatments = vec![1_024 * 64];
76+
77+
let mut group = c.benchmark_group("parallelization_owned");
78+
79+
for n in &treatments {
80+
let data = build_tree(*n);
81+
82+
let expected = tree_into_iter(data.clone());
83+
84+
group.bench_with_input(BenchmarkId::new("Tree::into_iter()", n), n, |b, _| {
85+
let result = tree_into_iter(data.clone());
86+
assert_eq!(result, expected);
87+
b.iter(|| tree_into_iter(data.clone()))
88+
});
89+
90+
group.bench_with_input(
91+
BenchmarkId::new("Tree::root().into_walk::<Dfs>()", n),
92+
n,
93+
|b, _| {
94+
let result = tree_into_dfs(data.clone());
95+
assert_eq!(result, expected);
96+
b.iter(|| tree_into_dfs(data.clone()))
97+
},
98+
);
99+
100+
group.bench_with_input(
101+
BenchmarkId::new("Tree::root().into_walk::<Bfs>()", n),
102+
n,
103+
|b, _| {
104+
let result = tree_into_bfs(data.clone());
105+
assert_eq!(result, expected);
106+
b.iter(|| tree_into_bfs(data.clone()))
107+
},
108+
);
109+
110+
group.bench_with_input(
111+
BenchmarkId::new("Tree::into_par_x() - orx-parallel", n),
112+
n,
113+
|b, _| {
114+
let result = tree_into_par_x(data.clone());
115+
assert_eq!(result, expected);
116+
b.iter(|| tree_into_par_x(data.clone()))
117+
},
118+
);
119+
120+
group.bench_with_input(
121+
BenchmarkId::new("Tree::into_iter().par_bridge() - rayon", n),
122+
n,
123+
|b, _| {
124+
let result = tree_into_iter_rayon(data.clone());
125+
assert_eq!(result, expected);
126+
b.iter(|| tree_into_iter_rayon(data.clone()))
127+
},
128+
);
129+
}
130+
131+
group.finish();
132+
}
133+
134+
criterion_group!(benches, bench);
135+
criterion_main!(benches);

benches/parallelization_ref.rs

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
use criterion::{BenchmarkId, Criterion, criterion_group, criterion_main};
2+
#[cfg(feature = "orx-parallel")]
3+
use orx_parallel::ParIter;
4+
use orx_tree::*;
5+
use rayon::iter::{ParallelBridge, ParallelIterator};
6+
7+
fn build_tree(n: usize) -> DynTree<String> {
8+
let mut tree = DynTree::new(0.to_string());
9+
let mut dfs = Traversal.dfs().over_nodes();
10+
while tree.len() < n {
11+
let root = tree.root();
12+
let x: Vec<_> = root.leaves_with(&mut dfs).map(|x| x.idx()).collect();
13+
for idx in x.iter() {
14+
let count = tree.len();
15+
let mut node = tree.node_mut(idx);
16+
let num_children = 20;
17+
for j in 0..num_children {
18+
node.push_child((count + j).to_string());
19+
}
20+
}
21+
}
22+
tree
23+
}
24+
25+
fn fibonacci(n: i64) -> i64 {
26+
let mut a = 0;
27+
let mut b = 1;
28+
for _ in 0..n {
29+
let c = a + b;
30+
a = b;
31+
b = c;
32+
}
33+
a
34+
}
35+
36+
fn tree_iter(tree: &DynTree<String>) -> i64 {
37+
tree.iter()
38+
.map(|x| x.parse::<usize>().unwrap())
39+
.map(|x| fibonacci(x as i64 % 500))
40+
.sum()
41+
}
42+
43+
fn tree_dfs(tree: &DynTree<String>) -> i64 {
44+
tree.root()
45+
.walk::<Dfs>()
46+
.map(|x| x.parse::<usize>().unwrap())
47+
.map(|x| fibonacci(x as i64 % 500))
48+
.sum()
49+
}
50+
51+
fn tree_bfs(tree: &DynTree<String>) -> i64 {
52+
tree.root()
53+
.walk::<Bfs>()
54+
.map(|x| x.parse::<usize>().unwrap())
55+
.map(|x| fibonacci(x as i64 % 500))
56+
.sum()
57+
}
58+
59+
fn tree_par_x(tree: &DynTree<String>) -> i64 {
60+
tree.par()
61+
.map(|x| x.parse::<usize>().unwrap())
62+
.map(|x| fibonacci(x as i64 % 500))
63+
.sum()
64+
}
65+
66+
fn tree_iter_rayon(tree: &DynTree<String>) -> i64 {
67+
tree.iter()
68+
.par_bridge()
69+
.map(|x| x.parse::<usize>().unwrap())
70+
.map(|x| fibonacci(x as i64 % 500))
71+
.sum()
72+
}
73+
74+
fn bench(c: &mut Criterion) {
75+
let treatments = vec![1_024 * 64];
76+
77+
let mut group = c.benchmark_group("parallelization_ref");
78+
79+
for n in &treatments {
80+
let data = build_tree(*n);
81+
82+
let expected = tree_iter(&data);
83+
84+
group.bench_with_input(BenchmarkId::new("Tree::iter()", n), n, |b, _| {
85+
let result = tree_iter(&data);
86+
assert_eq!(result, expected);
87+
b.iter(|| tree_iter(&data))
88+
});
89+
90+
group.bench_with_input(
91+
BenchmarkId::new("Tree::root().walk::<Dfs>()", n),
92+
n,
93+
|b, _| {
94+
let result = tree_dfs(&data);
95+
assert_eq!(result, expected);
96+
b.iter(|| tree_dfs(&data))
97+
},
98+
);
99+
100+
group.bench_with_input(
101+
BenchmarkId::new("Tree::root().walk::<Bfs>()", n),
102+
n,
103+
|b, _| {
104+
let result = tree_bfs(&data);
105+
assert_eq!(result, expected);
106+
b.iter(|| tree_bfs(&data))
107+
},
108+
);
109+
110+
group.bench_with_input(
111+
BenchmarkId::new("Tree::par_x() - orx-parallel", n),
112+
n,
113+
|b, _| {
114+
let result = tree_par_x(&data);
115+
assert_eq!(result, expected);
116+
b.iter(|| tree_par_x(&data))
117+
},
118+
);
119+
120+
group.bench_with_input(
121+
BenchmarkId::new("Tree::iter().par_bridge() - rayon", n),
122+
n,
123+
|b, _| {
124+
let result = tree_iter_rayon(&data);
125+
assert_eq!(result, expected);
126+
b.iter(|| tree_iter_rayon(&data))
127+
},
128+
);
129+
}
130+
131+
group.finish();
132+
}
133+
134+
criterion_group!(benches, bench);
135+
criterion_main!(benches);

0 commit comments

Comments
 (0)