diff --git a/Cargo.lock b/Cargo.lock index 03187ffc6817..88a3b672bebb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4555,11 +4555,13 @@ dependencies = [ "arrow2", "backtrace", "bytemuck", + "criterion", "crossbeam", "document-features", "fixed", "half 2.3.1", "itertools 0.11.0", + "mimalloc", "nohash-hasher", "num-derive", "num-traits", diff --git a/crates/re_log_types/Cargo.toml b/crates/re_log_types/Cargo.toml index 3aec057695b1..bc372442cf78 100644 --- a/crates/re_log_types/Cargo.toml +++ b/crates/re_log_types/Cargo.toml @@ -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 diff --git a/crates/re_log_types/benches/vec_deque_ext.rs b/crates/re_log_types/benches/vec_deque_ext.rs new file mode 100644 index 000000000000..0cce9e2617f4 --- /dev/null +++ b/crates/re_log_types/benches/vec_deque_ext.rs @@ -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 = base.clone(); + v.remove(0); + v + }); + }); + group.bench_function("remove/prefilled/middle", |b| { + let base = create_prefilled(); + b.iter(|| { + let mut v: VecDeque = 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 = 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 = 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 = 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 = 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 = 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 = 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 = base.clone(); + v.swap_remove_front(INITIAL_NUM_ENTRIES - 1); + v + }); + }); + } +} + +// --- + +fn create_prefilled() -> VecDeque { + let mut base: VecDeque = VecDeque::new(); + base.extend(0..INITIAL_NUM_ENTRIES as i64); + base +} diff --git a/crates/re_log_types/src/lib.rs b/crates/re_log_types/src/lib.rs index f8258a29400e..5f15bc123e40 100644 --- a/crates/re_log_types/src/lib.rs +++ b/crates/re_log_types/src/lib.rs @@ -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; @@ -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::{ diff --git a/crates/re_log_types/src/vec_deque_ext.rs b/crates/re_log_types/src/vec_deque_ext.rs new file mode 100644 index 000000000000..5b0ef51ab115 --- /dev/null +++ b/crates/re_log_types/src/vec_deque_ext.rs @@ -0,0 +1,161 @@ +use std::collections::VecDeque; + +// --- + +/// Extends [`VecDeque`] with extra sorting routines. +pub trait VecDequeSortingExt { + /// 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 VecDequeSortingExt for VecDeque { + #[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 = 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 { + /// 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; + + /// 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 VecDequeRemovalExt for VecDeque { + #[inline] + fn swap_remove(&mut self, index: usize) -> Option { + 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 = 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()); +}