Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 11 additions & 10 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ jobs:
strategy:
matrix:
toolchain: ["stable"]
features: ["", "--features serde"]
features: ["--features orx-parallel", "--features serde"]
no_std_features: ["--features serde"]

steps:
- uses: actions/checkout@v4
Expand All @@ -36,24 +37,24 @@ jobs:
run: cargo install cargo-no-std-check

- name: Build
run: cargo build --verbose ${{ matrix.features }}
run: cargo build --no-default-features --verbose ${{ matrix.features }}
- name: Build-32bit
run: cargo build --verbose --target i686-unknown-linux-musl ${{ matrix.features }}
run: cargo build --no-default-features --verbose --target i686-unknown-linux-musl ${{ matrix.features }}
- name: Build-wasm
run: cargo build --verbose --target wasm32v1-none ${{ matrix.features }}
run: cargo build --no-default-features --verbose --target wasm32v1-none ${{ matrix.no_std_features }}

- name: Test
run: cargo test --verbose ${{ matrix.features }}
run: cargo test --no-default-features --verbose ${{ matrix.features }}
- name: Test-32bit
run: cargo test --verbose --target i686-unknown-linux-musl ${{ matrix.features }}
run: cargo test --no-default-features --verbose --target i686-unknown-linux-musl ${{ matrix.features }}
- name: Check-wasm
run: cargo check --verbose --target wasm32v1-none ${{ matrix.features }}
run: cargo check --no-default-features --verbose --target wasm32v1-none ${{ matrix.no_std_features }}

- name: Clippy
run: cargo clippy ${{ matrix.features }} -- -D warnings --verbose
run: cargo clippy --no-default-features ${{ matrix.features }} -- -D warnings --verbose

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

- name: NoStd
run: cargo +nightly no-std-check ${{ matrix.features }}
run: cargo +nightly no-std-check --no-default-features ${{ matrix.no_std_features }}
23 changes: 15 additions & 8 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
[package]
name = "orx-tree"
version = "1.5.0"
version = "1.6.0"
edition = "2024"
authors = ["orxfun <orx.ugur.arikan@gmail.com>"]
description = "A beautiful tree 🌳 with convenient and efficient growth, mutation and traversal features."
description = "A beautiful tree 🌳 with convenient and efficient growth, mutation and traversal features with support for parallel computation."
license = "MIT OR Apache-2.0"
repository = "https://github.com/orxfun/orx-tree/"
keywords = ["tree", "data-structures", "traversal", "traverse", "binarytree"]
Expand All @@ -13,19 +13,26 @@ categories = ["data-structures", "algorithms", "rust-patterns", "no-std"]
orx-iterable = { version = "1.3.0", default-features = false }
orx-pseudo-default = { version = "2.1.0", default-features = false }
orx-pinned-vec = "3.16.0"
orx-split-vec = "3.16.0"
orx-selfref-col = "2.8.0"
orx-self-or = "1.2.0"
serde = { version = "1.0.219", optional = true, default-features = false }

orx-split-vec = { version = "3.17.0", default-features = false }
orx-selfref-col = { version = "2.9.0", default-features = false }
orx-concurrent-iter = { version = "2.1.0", default-features = false }
orx-parallel = { version = "2.1.0", default-features = false, optional = true }

[dev-dependencies]
test-case = { version = "3.3.1", default-features = false }
clap = { version = "4.5.38", features = ["derive"] }
criterion = "0.5.1"
rayon = { version = "1.10.0" }
serde_json = { version = "1.0.140", default-features = false, features = [
"std",
] }
test-case = { version = "3.3.1", default-features = false }

[features]
default = []
std = []
default = ["orx-parallel"]
serde = ["dep:serde"]

[[bench]]
name = "parallelization_ref"
harness = false
15 changes: 12 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
[![orx-tree crate](https://img.shields.io/crates/d/orx-tree.svg)](https://crates.io/crates/orx-tree)
[![orx-tree documentation](https://docs.rs/orx-tree/badge.svg)](https://docs.rs/orx-tree)

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

## Features

Expand Down Expand Up @@ -130,9 +130,18 @@ Alternatively, we can turn a mutable node into an [`into_walk`](https://docs.rs/
* We can iterate over the removed nodes in the order of the generic traversal parameter and use the data however we need.
* 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).

## Opt-in Features
## Features

* **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).
* This feature is added as default and requires **std**; hence, please use `cargo add orx-tree --no-default-features` for **no-std** use cases.
* 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).
* Parallelization of all walks or traversals in particular order are under development.
* Parallelization examples can be found in [`demo_parallelization`](https://github.com/orxfun/orx-tree/blob/main/examples/demo_parallelization.rs) example.
* 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):
* `Sequential computation over Tree : 18.96s`
* `Parallelized over Tree using orx-parallel : 6.02s`
* `Parallelized over Tree using rayon's par-bridge : 81.10s`

* **std**: This is a no-std crate by default, and hence, "std" feature needs to be included when necessary.
* **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).

# Examples
Expand Down
135 changes: 135 additions & 0 deletions benches/parallelization_owned.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
use criterion::{BenchmarkId, Criterion, criterion_group, criterion_main};
#[cfg(feature = "orx-parallel")]
use orx_parallel::ParIter;
use orx_tree::*;
use rayon::iter::{ParallelBridge, ParallelIterator};

fn build_tree(n: usize) -> DynTree<String> {
let mut tree = DynTree::new(0.to_string());
let mut dfs = Traversal.dfs().over_nodes();
while tree.len() < n {
let root = tree.root();
let x: Vec<_> = root.leaves_with(&mut dfs).map(|x| x.idx()).collect();
for idx in x.iter() {
let count = tree.len();
let mut node = tree.node_mut(idx);
let num_children = 20;
for j in 0..num_children {
node.push_child((count + j).to_string());
}
}
}
tree
}

fn fibonacci(n: i64) -> i64 {
let mut a = 0;
let mut b = 1;
for _ in 0..n {
let c = a + b;
a = b;
b = c;
}
a
}

fn tree_into_iter(tree: DynTree<String>) -> i64 {
tree.into_iter()
.map(|x| x.parse::<usize>().unwrap())
.map(|x| fibonacci(x as i64 % 500))
.sum()
}

fn tree_into_dfs(mut tree: DynTree<String>) -> i64 {
tree.root_mut()
.into_walk::<Dfs>()
.map(|x| x.parse::<usize>().unwrap())
.map(|x| fibonacci(x as i64 % 500))
.sum()
}

fn tree_into_bfs(mut tree: DynTree<String>) -> i64 {
tree.root_mut()
.into_walk::<Bfs>()
.map(|x| x.parse::<usize>().unwrap())
.map(|x| fibonacci(x as i64 % 500))
.sum()
}

fn tree_into_par_x(tree: DynTree<String>) -> i64 {
tree.into_par()
.map(|x| x.parse::<usize>().unwrap())
.map(|x| fibonacci(x as i64 % 500))
.sum()
}

fn tree_into_iter_rayon(tree: DynTree<String>) -> i64 {
tree.into_iter()
.par_bridge()
.map(|x| x.parse::<usize>().unwrap())
.map(|x| fibonacci(x as i64 % 500))
.sum()
}

fn bench(c: &mut Criterion) {
let treatments = vec![1_024 * 64];

let mut group = c.benchmark_group("parallelization_owned");

for n in &treatments {
let data = build_tree(*n);

let expected = tree_into_iter(data.clone());

group.bench_with_input(BenchmarkId::new("Tree::into_iter()", n), n, |b, _| {
let result = tree_into_iter(data.clone());
assert_eq!(result, expected);
b.iter(|| tree_into_iter(data.clone()))
});

group.bench_with_input(
BenchmarkId::new("Tree::root().into_walk::<Dfs>()", n),
n,
|b, _| {
let result = tree_into_dfs(data.clone());
assert_eq!(result, expected);
b.iter(|| tree_into_dfs(data.clone()))
},
);

group.bench_with_input(
BenchmarkId::new("Tree::root().into_walk::<Bfs>()", n),
n,
|b, _| {
let result = tree_into_bfs(data.clone());
assert_eq!(result, expected);
b.iter(|| tree_into_bfs(data.clone()))
},
);

group.bench_with_input(
BenchmarkId::new("Tree::into_par_x() - orx-parallel", n),
n,
|b, _| {
let result = tree_into_par_x(data.clone());
assert_eq!(result, expected);
b.iter(|| tree_into_par_x(data.clone()))
},
);

group.bench_with_input(
BenchmarkId::new("Tree::into_iter().par_bridge() - rayon", n),
n,
|b, _| {
let result = tree_into_iter_rayon(data.clone());
assert_eq!(result, expected);
b.iter(|| tree_into_iter_rayon(data.clone()))
},
);
}

group.finish();
}

criterion_group!(benches, bench);
criterion_main!(benches);
135 changes: 135 additions & 0 deletions benches/parallelization_ref.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
use criterion::{BenchmarkId, Criterion, criterion_group, criterion_main};
#[cfg(feature = "orx-parallel")]
use orx_parallel::ParIter;
use orx_tree::*;
use rayon::iter::{ParallelBridge, ParallelIterator};

fn build_tree(n: usize) -> DynTree<String> {
let mut tree = DynTree::new(0.to_string());
let mut dfs = Traversal.dfs().over_nodes();
while tree.len() < n {
let root = tree.root();
let x: Vec<_> = root.leaves_with(&mut dfs).map(|x| x.idx()).collect();
for idx in x.iter() {
let count = tree.len();
let mut node = tree.node_mut(idx);
let num_children = 20;
for j in 0..num_children {
node.push_child((count + j).to_string());
}
}
}
tree
}

fn fibonacci(n: i64) -> i64 {
let mut a = 0;
let mut b = 1;
for _ in 0..n {
let c = a + b;
a = b;
b = c;
}
a
}

fn tree_iter(tree: &DynTree<String>) -> i64 {
tree.iter()
.map(|x| x.parse::<usize>().unwrap())
.map(|x| fibonacci(x as i64 % 500))
.sum()
}

fn tree_dfs(tree: &DynTree<String>) -> i64 {
tree.root()
.walk::<Dfs>()
.map(|x| x.parse::<usize>().unwrap())
.map(|x| fibonacci(x as i64 % 500))
.sum()
}

fn tree_bfs(tree: &DynTree<String>) -> i64 {
tree.root()
.walk::<Bfs>()
.map(|x| x.parse::<usize>().unwrap())
.map(|x| fibonacci(x as i64 % 500))
.sum()
}

fn tree_par_x(tree: &DynTree<String>) -> i64 {
tree.par()
.map(|x| x.parse::<usize>().unwrap())
.map(|x| fibonacci(x as i64 % 500))
.sum()
}

fn tree_iter_rayon(tree: &DynTree<String>) -> i64 {
tree.iter()
.par_bridge()
.map(|x| x.parse::<usize>().unwrap())
.map(|x| fibonacci(x as i64 % 500))
.sum()
}

fn bench(c: &mut Criterion) {
let treatments = vec![1_024 * 64];

let mut group = c.benchmark_group("parallelization_ref");

for n in &treatments {
let data = build_tree(*n);

let expected = tree_iter(&data);

group.bench_with_input(BenchmarkId::new("Tree::iter()", n), n, |b, _| {
let result = tree_iter(&data);
assert_eq!(result, expected);
b.iter(|| tree_iter(&data))
});

group.bench_with_input(
BenchmarkId::new("Tree::root().walk::<Dfs>()", n),
n,
|b, _| {
let result = tree_dfs(&data);
assert_eq!(result, expected);
b.iter(|| tree_dfs(&data))
},
);

group.bench_with_input(
BenchmarkId::new("Tree::root().walk::<Bfs>()", n),
n,
|b, _| {
let result = tree_bfs(&data);
assert_eq!(result, expected);
b.iter(|| tree_bfs(&data))
},
);

group.bench_with_input(
BenchmarkId::new("Tree::par_x() - orx-parallel", n),
n,
|b, _| {
let result = tree_par_x(&data);
assert_eq!(result, expected);
b.iter(|| tree_par_x(&data))
},
);

group.bench_with_input(
BenchmarkId::new("Tree::iter().par_bridge() - rayon", n),
n,
|b, _| {
let result = tree_iter_rayon(&data);
assert_eq!(result, expected);
b.iter(|| tree_iter_rayon(&data))
},
);
}

group.finish();
}

criterion_group!(benches, bench);
criterion_main!(benches);
Loading