Skip to content

Commit

Permalink
Merge pull request #138 from marvin-hansen/main
Browse files Browse the repository at this point in the history
  • Loading branch information
marvin-hansen authored Nov 20, 2024
2 parents d657ac8 + 1d669fa commit 58dadb8
Show file tree
Hide file tree
Showing 7 changed files with 437 additions and 45 deletions.
1 change: 1 addition & 0 deletions dcl_data_structures/benches/bench_main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ criterion_main! {
benchmarks::bench_grid_array::array_grid,
benchmarks::bench_window_arr::window_array_backed,
benchmarks::bench_window_vec::window_vector_backed,
benchmarks::bench_window_comp::window_impl_comp,
}
120 changes: 120 additions & 0 deletions dcl_data_structures/benches/benchmarks/bench_window_comp.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
// SPDX-License-Identifier: MIT
// Copyright (c) "2023" . The DeepCausality Authors. All Rights Reserved.

use criterion::{black_box, criterion_group, BenchmarkId, Criterion};
use dcl_data_structures::prelude::{ArrayStorage, VectorStorage, WindowStorage};

const SIZE: usize = 4;
const CAPACITY: usize = 1200;
const MULT: usize = 12;

fn array_operations(c: &mut Criterion) {
let mut group = c.benchmark_group("array_operations");

// Basic push operation
group.bench_function("push_single", |b| {
let mut storage = ArrayStorage::<i32, SIZE, CAPACITY>::new();
b.iter(|| {
storage.push(black_box(42));
});
});

// Push operation with rewind
group.bench_function("push_with_rewind", |b| {
let mut storage = ArrayStorage::<i32, SIZE, CAPACITY>::new();
for _ in 0..CAPACITY - 1 {
storage.push(42);
}
b.iter(|| {
storage.push(black_box(42));
});
});

// Sequential operations
group.bench_function("sequential_ops", |b| {
let mut storage = ArrayStorage::<i32, SIZE, CAPACITY>::new();
for i in 0..SIZE {
storage.push(i as i32);
}
b.iter(|| {
storage.push(black_box(42));
black_box(storage.first().unwrap());
black_box(storage.last().unwrap());
black_box(storage.get_slice());
});
});

// Batch operations
for size in [10, 100, 1000].iter() {
group.bench_with_input(BenchmarkId::new("batch_push", size), size, |b, &size| {
let mut storage = ArrayStorage::<i32, SIZE, CAPACITY>::new();
b.iter(|| {
for i in 0..size {
storage.push(black_box(i as i32));
}
});
});
}

// Memory access patterns
group.bench_function("memory_access", |b| {
let mut storage = ArrayStorage::<i32, SIZE, CAPACITY>::new();
for i in 0..100 {
storage.push(i);
}
b.iter(|| {
for i in 0..10 {
storage.push(black_box(i));
black_box(storage.get_slice());
}
});
});

group.finish();
}

fn vector_operations(c: &mut Criterion) {
let mut group = c.benchmark_group("vector_operations");

// Basic push operation
group.bench_function("push_single", |b| {
let mut storage = VectorStorage::new(SIZE, MULT);
b.iter(|| {
storage.push(black_box(42));
});
});

// Sequential operations
group.bench_function("sequential_ops", |b| {
let mut storage = VectorStorage::new(SIZE, MULT);
for i in 0..SIZE {
storage.push(i as i32);
}
b.iter(|| {
storage.push(black_box(42));
black_box(storage.first().unwrap());
black_box(storage.last().unwrap());
black_box(storage.get_slice());
});
});

// Batch operations
for size in [10, 100, 1000].iter() {
group.bench_with_input(BenchmarkId::new("batch_push", size), size, |b, &size| {
let mut storage = VectorStorage::new(SIZE, MULT);
b.iter(|| {
for i in 0..size {
storage.push(black_box(i as i32));
}
});
});
}

group.finish();
}

criterion_group! {
name = window_impl_comp;
config = Criterion::default().sample_size(100);
targets = array_operations, vector_operations,
}
1 change: 1 addition & 0 deletions dcl_data_structures/benches/benchmarks/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@
// Copyright (c) "2023" . The DeepCausality Authors. All Rights Reserved.
pub mod bench_grid_array;
pub mod bench_window_arr;
pub mod bench_window_comp;
pub mod bench_window_vec;
mod fields;
46 changes: 43 additions & 3 deletions dcl_data_structures/docs/SlidingWindow.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,46 @@ then it's best to run an optimizer to find the best value for M that maximizes t

## Performance

Both implementations perform well on inserts with the array backed implementation
being about 1/3 faster than the vector backed implementation. Read operations are basically free O(1) since
the sliding window is just a slice over the backing data structure.
Both implementations perform well by default, but the array backed implementation is more performant so
chose this one for performance-critical applications.

Detailed performance comparison:

Single Push Operation:
* ArrayStorage: ~891 ps
* VectorStorage: ~3.69 ns

VectorStorage is about 4x slower

Sequential Operations:
* ArrayStorage: ~2.24 ns
* VectorStorage: ~3.71 ns

VectorStorage is about 1.7x slower

Batch Operations (10/100/1000 elements):
* ArrayStorage: 9.5 ns / 102 ns / 951 ns
* VectorStorage: 36.5 ns / 375 ns / 3700 ns

VectorStorage is about 3.8x slower across all batch sizes

The performance difference between ArrayStorage and VectorStorage can be attributed to:

Memory Layout:
* ArrayStorage uses a fixed-size array ([T; CAPACITY])
* VectorStorage uses a dynamically growing Vec

The fixed-size array provides better cache locality and fewer allocations
* Bounds Checking:
* ArrayStorage's size is known at compile time
* VectorStorage requires runtime bounds checking

Memory Management:
* ArrayStorage allocates all memory upfront
* VectorStorage may need to reallocate and grow

Despite being slower, VectorStorage still has its advantages:

* More flexible since it doesn't require compile-time size constants
* Can handle varying window sizes
* More memory efficient when the actual data size is much smaller than the capacity
91 changes: 81 additions & 10 deletions dcl_data_structures/src/window_type/storage.rs
Original file line number Diff line number Diff line change
@@ -1,39 +1,96 @@
// SPDX-License-Identifier: MIT
// Copyright (c) "2023" . The DeepCausality Authors. All Rights Reserved.

/// Trait defining the interface for a sliding window data structure
///
/// # Type Parameters
/// * `T` - The type of elements stored in the window, must implement PartialEq + Copy + Default
///
/// # Implementation Note
/// This trait provides both required methods that must be implemented by all window types
/// and default implementations for common window operations.
pub trait WindowStorage<T>
where
T: PartialEq + Copy + Default,
{
/// Pushes a new element to the beginning of the sliding window.
/// If the window is filled, the last element will be dropped.
/// Pushes a new element to the beginning of the sliding window
///
/// # Args
/// * `value` - The value to be pushed into the window
///
/// # Implementation Note
/// When the window is filled, the oldest element will be dropped to maintain the window size
fn push(&mut self, value: T);

/// Returns the first (oldest) element in the sliding window
///
/// # Returns
/// * `Ok(T)` - The first element in the window
/// * `Err(String)` - If the window is empty
fn first(&self) -> Result<T, String>;

/// Returns the last (newest) element in the sliding window
///
/// # Returns
/// * `Ok(T)` - The last element in the window
/// * `Err(String)` - If the window is not yet filled
fn last(&self) -> Result<T, String>;
/// Returns tail cursor

/// Returns the current tail position of the window
///
/// # Returns
/// * `usize` - The current tail position
fn tail(&self) -> usize;
/// Returns size

/// Returns the size of the sliding window
///
/// # Returns
/// * `usize` - The configured size of the window
fn size(&self) -> usize;
/// Returns sliding window as slice

/// Returns a slice of the current window contents
///
/// # Returns
/// * `&[T]` - A slice containing the current window elements
fn get_slice(&self) -> &[T];

//
// Default implementations. Override as required.
//

/// Returns true if the window is empty.
/// Returns true if the window is empty
///
/// # Returns
/// * `bool` - True if the window is empty, false otherwise
///
/// # Implementation Note
/// Default implementation checks if tail position is 0
fn empty(&self) -> bool {
self.tail() == 0
}

/// Returns true if the window is filled.
/// Returns true if the window is filled
///
/// # Returns
/// * `bool` - True if the window is filled, false otherwise
///
/// # Implementation Note
/// Default implementation checks if tail position is at least the window size
fn filled(&self) -> bool {
self.tail() >= self.size()
}

/// Returns the sliding window as a fixed size static array.
/// Returns the sliding window as a fixed size static array
///
/// # Type Parameters
/// * `S` - The size of the returned array
///
/// # Returns
/// * `Ok([T; S])` - The window contents as a fixed-size array
/// * `Err(String)` - If the window is not yet filled
///
/// # Implementation Note
/// Default implementation copies window contents into a new fixed-size array
fn arr<const S: usize>(&self) -> Result<[T; S], String> {
if !self.filled() {
return Err(
Expand All @@ -49,7 +106,14 @@ where
Ok(arr)
}

/// Returns the sliding window as a slice.
/// Returns the sliding window as a slice
///
/// # Returns
/// * `Ok(&[T])` - A slice of the window contents
/// * `Err(String)` - If the window is not yet filled
///
/// # Implementation Note
/// Default implementation returns the slice only if the window is filled
fn slice(&self) -> Result<&[T], String> {
return if !self.filled() {
Err(
Expand All @@ -61,7 +125,14 @@ where
};
}

/// Returns the sliding window as a vector.
/// Returns the sliding window as a vector
///
/// # Returns
/// * `Ok(Vec<T>)` - A vector containing the window contents
/// * `Err(String)` - If the window is not yet filled
///
/// # Implementation Note
/// Default implementation converts the window slice to a vector
fn vec(&self) -> Result<Vec<T>, String> {
return if !self.filled() {
Err(
Expand Down
Loading

0 comments on commit 58dadb8

Please sign in to comment.