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
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,14 @@ jobs:
- name: Build-32bit
run: cargo build --verbose --target i686-unknown-linux-musl
- name: Build-wasm
run: cargo build --verbose --target wasm32v1-none
run: cargo build --verbose --no-default-features --target wasm32v1-none

- name: Test
run: cargo test --verbose
- name: Test-32bit
run: cargo test --verbose --target i686-unknown-linux-musl
- name: Check-wasm
run: cargo check --verbose --target wasm32v1-none
run: cargo check --verbose --no-default-features --target wasm32v1-none

- name: Clippy
run: cargo clippy -- -D warnings --verbose
Expand All @@ -55,4 +55,4 @@ jobs:
run: cargo +nightly miri test --verbose

- name: NoStd
run: cargo +nightly no-std-check
run: cargo +nightly no-std-check --no-default-features
24 changes: 13 additions & 11 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "orx-linked-list"
version = "3.8.0"
version = "3.9.0"
edition = "2024"
authors = ["orxfun <[email protected]>"]
description = "A linked list implementation with unique features and an extended list of constant time methods providing high performance traversals and mutations."
Expand All @@ -12,21 +12,23 @@ categories = ["data-structures", "rust-patterns", "no-std"]
[dependencies]
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-fixed-vec = "3.16.0"
orx-split-vec = "3.16.0"
orx-selfref-col = "2.8.0"

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

[dev-dependencies]
clap = { version = "4.5.35", features = ["derive"] }
criterion = "0.5"
rand = "0.9.0"
rand_chacha = "0.9.0"
clap = { version = "4.5.38", features = ["derive"] }
criterion = "0.5.1"
rand = "0.9"
rand_chacha = "0.9"
rayon = { version = "1.10.0" }
test-case = "3.3.1"

[features]
default = []
default = ["orx-parallel"]
validation = []

[[bench]]
Expand Down
23 changes: 20 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,9 @@ A linked list implementation with unique features and an extended list of consta

Both doubly and singly lists are provided as generic variants of the core struct `List`. It is sufficient to know the four variants:
* [`DoublyList`](https://docs.rs/orx-linked-list/latest/orx_linked_list/type.DoublyList.html) and [`SinglyList`](https://docs.rs/orx-linked-list/latest/orx_linked_list/type.SinglyList.html)
* [`DoublyListLazy`](https://docs.rs/orx-linked-list/latest/orx_linked_list/type.DoublyListLazy.html) and [`SinglyListLazy`](https://docs.rs/orx-linked-list/latest/orx_linked_list/type.SinglyListLazy.html)
* *Lazy* suffix corresponds to lazy memory reclaim and will be explained in the indices section.
* [`DoublyListLazy`](https://docs.rs/orx-linked-list/latest/orx_linked_list/type.DoublyListLazy.html) and [`SinglyListLazy`](https://docs.rs/orx-linked-list/latest/orx_linked_list/type.SinglyListLazy.html) (*Lazy* suffix corresponds to lazy memory reclaim and will be explained in the indices section)

Some notable features are as follows.
> **no-std**: This crate supports **no-std**; however, *std* is added due to the default [**orx-parallel**](https://crates.io/crates/orx-parallel) feature. Please include with **no-default-features** for no-std use cases: `cargo add orx-linked-list --no-default-features`.

## Efficiency

Expand Down Expand Up @@ -64,6 +63,24 @@ The significance of improvement can further be increased by using `DoublyList::i

</details>

## Parallelization

When [orx-parallel](https://crates.io/crates/orx-parallel) feature is used (by default), computations over `LinkedList` elements can be efficiently parallelized.

Parallel computation is defined by chained iterator methods, simply by replacing `iter_x` with `par_x`, and `into_iter_x` by `into_par_x`.

You may find demonstrations in [`demo_parallelization`](https://github.com/orxfun/orx-linked-list/blob/main/examples/demo_parallelization.rs)

Significant performance improvements can be achieved by replacing `iter_x` with `par_x`, as can be tested with the benchmark file [parallelization_ref.rs](https://github.com/orxfun/orx-linked-list/blob/main/benches/parallelization_ref.rs), or with the lightweight benchmark example [`bench_parallelization`](https://github.com/orxfun/orx-linked-list/blob/main/examples/bench_parallelization.rs):

```bash
Sequential computation over std::collections::LinkedList : 12.56s
Sequential computation over DoublyList : 11.78s
Parallelized over DoublyList using orx_parallel : 2.93s
```

*The suffix "_x" indicates that the iterators yield elements in arbitrary order, rather than from front to back. Parallelization of all iterations defined in the next section is in progress.*

## Iterations

Linked lists are all about traversal. Therefore, the linked lists defined in this crate, especially the **DoublyList**, provide various useful ways to iterate over the data:
Expand Down
254 changes: 254 additions & 0 deletions benches/parallelization_owned.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
use criterion::{BenchmarkId, Criterion, criterion_group, criterion_main};
use orx_linked_list::*;
#[cfg(feature = "orx-parallel")]
use orx_parallel::ParIter;
use orx_selfref_col::MemoryPolicy;
use rand::prelude::*;
use rand_chacha::ChaCha8Rng;
use rayon::iter::{IntoParallelIterator, ParallelIterator};

#[derive(Clone)]
enum Action {
PushBack(String),
PushFront(String),
PopBack,
PopFront,
}

fn get_test_data(n: usize) -> Vec<Action> {
let mut rng = ChaCha8Rng::seed_from_u64(56456);
let mut vec: Vec<_> = (0..n)
.map(|_| match rng.random::<f32>() {
x if x < 0.5 => Action::PushBack(rng.random_range(0..n).to_string()),
_ => Action::PushFront(rng.random_range(0..n).to_string()),
})
.collect();
for _ in 0..2 * n {
let action = match rng.random::<f32>() {
x if x < 0.25 => Action::PushBack(rng.random_range(0..n).to_string()),
x if x < 0.50 => Action::PushFront(rng.random_range(0..n).to_string()),
x if x < 0.75 => Action::PopBack,
_ => Action::PopFront,
};
vec.push(action)
}
vec
}

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
}

// variants

fn fill_doubly_list<M: MemoryPolicy<Doubly<String>>>(
actions: &[Action],
list: &mut List<Doubly<String>, M>,
) {
for action in actions {
match action {
Action::PushBack(x) => {
list.push_back(x.clone());
}
Action::PushFront(x) => {
list.push_front(x.clone());
}
Action::PopBack => {
_ = list.pop_back();
}
Action::PopFront => {
_ = list.pop_front();
}
}
}
}

fn doubly_iter<M: MemoryPolicy<Doubly<String>>>(list: List<Doubly<String>, M>) -> Vec<char> {
list.into_iter()
.filter(|x| fibonacci(x.parse::<i64>().unwrap() % 1000) % 2 == 0)
.map(|x| x.chars().last().unwrap())
.collect()
}

fn doubly_iter_x<M: MemoryPolicy<Doubly<String>>>(list: List<Doubly<String>, M>) -> Vec<char> {
list.into_iter_x()
.filter(|x| fibonacci(x.parse::<i64>().unwrap() % 1000) % 2 == 0)
.map(|x| x.chars().last().unwrap())
.collect()
}

#[cfg(feature = "orx-parallel")]
fn doubly_par_x<M: MemoryPolicy<Doubly<String>>>(list: List<Doubly<String>, M>) -> Vec<char> {
list.into_par_x()
.filter(|x| fibonacci(x.parse::<i64>().unwrap() % 1000) % 2 == 0)
.map(|x| x.chars().last().unwrap())
.collect()
}

fn fill_std_linked_list(actions: &[Action], list: &mut std::collections::LinkedList<String>) {
for action in actions {
match action {
Action::PushBack(x) => {
list.push_back(x.clone());
}
Action::PushFront(x) => {
list.push_front(x.clone());
}
Action::PopBack => {
_ = list.pop_back();
}
Action::PopFront => {
_ = list.pop_front();
}
}
}
}

fn std_linked_list(list: std::collections::LinkedList<String>) -> Vec<char> {
list.into_iter()
.filter(|x| fibonacci(x.parse::<i64>().unwrap() % 1000) % 2 == 0)
.map(|x| x.chars().last().unwrap())
.collect()
}

fn std_linked_list_rayon(list: std::collections::LinkedList<String>) -> Vec<char> {
list.into_par_iter()
.filter(|x| fibonacci(x.parse::<i64>().unwrap() % 1000) % 2 == 0)
.map(|x| x.chars().last().unwrap())
.collect()
}

fn fill_vec_deque(actions: &[Action], list: &mut std::collections::VecDeque<String>) {
for action in actions {
match action {
Action::PushBack(x) => {
list.push_back(x.clone());
}
Action::PushFront(x) => {
list.push_front(x.clone());
}
Action::PopBack => {
_ = list.pop_back();
}
Action::PopFront => {
_ = list.pop_front();
}
}
}
}

fn std_vec_deque(list: std::collections::VecDeque<String>) -> Vec<char> {
list.into_iter()
.filter(|x| fibonacci(x.parse::<i64>().unwrap() % 1000) % 2 == 0)
.map(|x| x.chars().last().unwrap())
.collect()
}

fn std_vec_deque_rayon(list: std::collections::VecDeque<String>) -> Vec<char> {
list.into_par_iter()
.filter(|x| fibonacci(x.parse::<i64>().unwrap() % 1000) % 2 == 0)
.map(|x| x.chars().last().unwrap())
.collect()
}

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

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

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

let mut std_list = std::collections::LinkedList::new();
fill_std_linked_list(&data, &mut std_list);
let expected = std_linked_list(std_list.clone());

group.bench_with_input(BenchmarkId::new("LinkedList::into_iter", n), n, |b, _| {
let mut std_list = std::collections::LinkedList::new();
fill_std_linked_list(&data, &mut std_list);
let result = std_linked_list(std_list.clone());
assert_eq!(result, expected);

b.iter(|| std_linked_list(std_list.clone()))
});

group.bench_with_input(
BenchmarkId::new("LinkedList::into_par_iter (rayon)", n),
n,
|b, _| {
let mut std_list = std::collections::LinkedList::new();
fill_std_linked_list(&data, &mut std_list);
let result = std_linked_list_rayon(std_list.clone());
assert_eq!(result, expected);

b.iter(|| std_linked_list_rayon(std_list.clone()))
},
);

group.bench_with_input(BenchmarkId::new("VecDeque::into_iter", n), n, |b, _| {
let mut list = std::collections::VecDeque::new();
fill_vec_deque(&data, &mut list);
let result = std_vec_deque(list.clone());
assert_eq!(result, expected);

b.iter(|| std_vec_deque(list.clone()))
});

group.bench_with_input(
BenchmarkId::new("VecDeque::into_par_iter (rayon)", n),
n,
|b, _| {
let mut list = std::collections::VecDeque::new();
fill_vec_deque(&data, &mut list);
let result = std_vec_deque_rayon(list.clone());
assert_eq!(result, expected);

b.iter(|| std_vec_deque_rayon(list.clone()))
},
);

group.bench_with_input(BenchmarkId::new("DoublyList::into_iter", n), n, |b, _| {
let mut list = DoublyList::new();
fill_doubly_list(&data, &mut list);
let result = doubly_iter(list.clone());
assert_eq!(result, expected);

b.iter(|| doubly_iter(list.clone()))
});

group.bench_with_input(BenchmarkId::new("DoublyList::into_iter_x", n), n, |b, _| {
let mut list = DoublyList::new();
fill_doubly_list(&data, &mut list);
let result = doubly_iter_x(list.clone());
assert_eq!(result, expected);

b.iter(|| doubly_iter_x(list.clone()))
});

#[cfg(feature = "orx-parallel")]
group.bench_with_input(
BenchmarkId::new("DoublyList::into_par_x (orx-parallel)", n),
n,
|b, _| {
let mut list = DoublyList::new();
fill_doubly_list(&data, &mut list);
let result = doubly_par_x(list.clone());
assert_eq!(result, expected);

b.iter(|| doubly_par_x(list.clone()))
},
);
}

group.finish();
}

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