Skip to content

Commit

Permalink
GC improvements 2: VecDeque extensions & benchmarks (#4396)
Browse files Browse the repository at this point in the history
What the title says: just introducing new tools that will be used by all
the ringbuffer looking things in the revamped datastore and upcoming
query cache.

---

Part of the GC improvements series:
- #4394
- #4395
- #4396
- #4397
- #4398
- #4399
- #4400
- #4401
  • Loading branch information
teh-cmc authored Dec 2, 2023
1 parent 75c232b commit 7da6b27
Show file tree
Hide file tree
Showing 5 changed files with 307 additions and 0 deletions.
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions crates/re_log_types/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -80,4 +80,11 @@ crossbeam.workspace = true


[dev-dependencies]
criterion.workspace = true
mimalloc.workspace = true
similar-asserts.workspace = true


[[bench]]
name = "vec_deque_ext"
harness = false
135 changes: 135 additions & 0 deletions crates/re_log_types/benches/vec_deque_ext.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
//! Simple benchmark suite to keep track of how the different removal methods for [`VecDeque`]
//! behave in practice.
#[global_allocator]
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;

use std::collections::VecDeque;

use criterion::{criterion_group, criterion_main, Criterion};

use re_log_types::VecDequeRemovalExt as _;

// ---

criterion_group!(benches, remove, swap_remove, swap_remove_front);
criterion_main!(benches);

// ---

// `cargo test` also runs the benchmark setup code, so make sure they run quickly:
#[cfg(debug_assertions)]
mod constants {
pub const INITIAL_NUM_ENTRIES: usize = 1;
}

#[cfg(not(debug_assertions))]
mod constants {
pub const INITIAL_NUM_ENTRIES: usize = 20_000;
}

#[allow(clippy::wildcard_imports)]
use self::constants::*;

// ---

fn remove(c: &mut Criterion) {
{
let mut group = c.benchmark_group("flat_vec_deque");
group.throughput(criterion::Throughput::Elements(1));
group.bench_function("remove/prefilled/front", |b| {
let base = create_prefilled();
b.iter(|| {
let mut v: VecDeque<i64> = base.clone();
v.remove(0);
v
});
});
group.bench_function("remove/prefilled/middle", |b| {
let base = create_prefilled();
b.iter(|| {
let mut v: VecDeque<i64> = base.clone();
v.remove(INITIAL_NUM_ENTRIES / 2);
v
});
});
group.bench_function("remove/prefilled/back", |b| {
let base = create_prefilled();
b.iter(|| {
let mut v: VecDeque<i64> = base.clone();
v.remove(INITIAL_NUM_ENTRIES - 1);
v
});
});
}
}

fn swap_remove(c: &mut Criterion) {
{
let mut group = c.benchmark_group("flat_vec_deque");
group.throughput(criterion::Throughput::Elements(1));
group.bench_function("swap_remove/prefilled/front", |b| {
let base = create_prefilled();
b.iter(|| {
let mut v: VecDeque<i64> = base.clone();
v.swap_remove(0);
v
});
});
group.bench_function("swap_remove/prefilled/middle", |b| {
let base = create_prefilled();
b.iter(|| {
let mut v: VecDeque<i64> = base.clone();
v.swap_remove(INITIAL_NUM_ENTRIES / 2);
v
});
});
group.bench_function("swap_remove/prefilled/back", |b| {
let base = create_prefilled();
b.iter(|| {
let mut v: VecDeque<i64> = base.clone();
v.swap_remove(INITIAL_NUM_ENTRIES - 1);
v
});
});
}
}

fn swap_remove_front(c: &mut Criterion) {
{
let mut group = c.benchmark_group("flat_vec_deque");
group.throughput(criterion::Throughput::Elements(1));
group.bench_function("swap_remove_front/prefilled/front", |b| {
let base = create_prefilled();
b.iter(|| {
let mut v: VecDeque<i64> = base.clone();
v.swap_remove_front(0);
v
});
});
group.bench_function("swap_remove_front/prefilled/middle", |b| {
let base = create_prefilled();
b.iter(|| {
let mut v: VecDeque<i64> = base.clone();
v.swap_remove_front(INITIAL_NUM_ENTRIES / 2);
v
});
});
group.bench_function("swap_remove_front/prefilled/back", |b| {
let base = create_prefilled();
b.iter(|| {
let mut v: VecDeque<i64> = base.clone();
v.swap_remove_front(INITIAL_NUM_ENTRIES - 1);
v
});
});
}
}

// ---

fn create_prefilled() -> VecDeque<i64> {
let mut base: VecDeque<i64> = VecDeque::new();
base.extend(0..INITIAL_NUM_ENTRIES as i64);
base
}
2 changes: 2 additions & 0 deletions crates/re_log_types/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ mod time;
pub mod time_point;
mod time_range;
mod time_real;
mod vec_deque_ext;

#[cfg(not(target_arch = "wasm32"))]
mod data_table_batcher;
Expand All @@ -55,6 +56,7 @@ pub use self::time::{Duration, Time, TimeZone};
pub use self::time_point::{TimeInt, TimePoint, TimeType, Timeline, TimelineName};
pub use self::time_range::{TimeRange, TimeRangeF};
pub use self::time_real::TimeReal;
pub use self::vec_deque_ext::{VecDequeRemovalExt, VecDequeSortingExt};

#[cfg(not(target_arch = "wasm32"))]
pub use self::data_table_batcher::{
Expand Down
161 changes: 161 additions & 0 deletions crates/re_log_types/src/vec_deque_ext.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
use std::collections::VecDeque;

// ---

/// Extends [`VecDeque`] with extra sorting routines.
pub trait VecDequeSortingExt<T> {
/// Sorts `self`.
///
/// Makes sure to render `self` contiguous first, if needed.
fn sort(&mut self);

/// Check whether `self` is sorted.
///
/// `self` doesn't need to be contiguous.
fn is_sorted(&self) -> bool;
}

impl<T: Clone + PartialOrd + Ord> VecDequeSortingExt<T> for VecDeque<T> {
#[inline]
fn sort(&mut self) {
self.make_contiguous();
let (values, &mut []) = self.as_mut_slices() else {
unreachable!();
};
values.sort();
}

#[inline]
fn is_sorted(&self) -> bool {
if self.is_empty() {
return true;
}

let (left, right) = self.as_slices();

let left_before_right = || {
if let (Some(left_last), Some(right_first)) = (left.last(), right.first()) {
left_last <= right_first
} else {
true
}
};
let left_is_sorted = || !left.windows(2).any(|values| values[0] > values[1]);
let right_is_sorted = || !right.windows(2).any(|values| values[0] > values[1]);

left_before_right() && left_is_sorted() && right_is_sorted()
}
}

#[test]
fn is_sorted() {
let mut v: VecDeque<i64> = vec![].into();

assert!(v.is_sorted());

v.extend([1, 2, 3]);
assert!(v.is_sorted());

v.push_front(4);
assert!(!v.is_sorted());

v.rotate_left(1);
assert!(v.is_sorted());

v.extend([7, 6, 5]);
assert!(!v.is_sorted());

v.sort();
assert!(v.is_sorted());
}

// ---

/// Extends [`VecDeque`] with extra removal routines.
pub trait VecDequeRemovalExt<T> {
/// Removes an element from anywhere in the deque and returns it, replacing it with
/// whichever end element that this is closer to the removal point.
///
/// If `index` points to the front or back of the queue, the removal is guaranteed to preserve
/// ordering; otherwise it doesn't.
/// In either case, this is *O*(1).
///
/// Returns `None` if `index` is out of bounds.
///
/// Element at index 0 is the front of the queue.
fn swap_remove(&mut self, index: usize) -> Option<T>;

/// Splits the deque into two at the given index.
///
/// Returns a newly allocated `VecDeque`. `self` contains elements `[0, at)`,
/// and the returned deque contains elements `[at, len)`.
///
/// If `at` is equal or greater than the length, the returned `VecDeque` is empty.
///
/// Note that the capacity of `self` does not change.
///
/// Element at index 0 is the front of the queue.
fn split_off_or_default(&mut self, at: usize) -> Self;
}

impl<T: Clone> VecDequeRemovalExt<T> for VecDeque<T> {
#[inline]
fn swap_remove(&mut self, index: usize) -> Option<T> {
if self.is_empty() {
return None;
}

if index == 0 {
let v = self.get(0).cloned();
self.rotate_left(1);
self.truncate(self.len() - 1);
v
} else if index + 1 == self.len() {
let v = self.get(index).cloned();
self.truncate(self.len() - 1);
v
} else if index < self.len() / 2 {
self.swap_remove_front(index)
} else {
self.swap_remove_back(index)
}
}

#[inline]
fn split_off_or_default(&mut self, at: usize) -> Self {
if at >= self.len() {
return Default::default();
}
self.split_off(at)
}
}

#[test]
fn swap_remove() {
let mut v: VecDeque<i64> = vec![].into();

assert!(v.swap_remove(0).is_none());
assert!(v.is_sorted());

v.push_front(1);
assert!(v.is_sorted());

assert!(v.swap_remove(1).is_none());
assert_eq!(Some(1), v.swap_remove(0));
assert!(v.is_sorted());

v.extend([4, 5, 6, 7]);
assert!(v.is_sorted());

assert_eq!(Some(4), v.swap_remove(0));
assert!(v.is_sorted());

assert_eq!(Some(7), v.swap_remove(2));
assert!(v.is_sorted());

assert_eq!(Some(6), v.swap_remove(1));
assert!(v.is_sorted());

assert_eq!(Some(5), v.swap_remove(0));
assert!(v.is_sorted());
}

0 comments on commit 7da6b27

Please sign in to comment.