#![allow(missing_docs, unreachable_pub)] use criterion::{ criterion_group, criterion_main, measurement::WallTime, BenchmarkGroup, Criterion, }; use prop::test_runner::TestRng; use proptest::{ prelude::*, strategy::ValueTree, test_runner::{basic_result_cache, TestRunner}, }; use reth_trie_common::{ prefix_set::{PrefixSet, PrefixSetMut}, Nibbles, }; use std::{collections::BTreeSet, hint::black_box}; /// Abstraction for aggregating nibbles and freezing it to a type /// that can be later used for benching. pub trait PrefixSetMutAbstraction: Default { type Frozen; fn insert(&mut self, key: Nibbles); fn freeze(self) -> Self::Frozen; } /// Abstractions used for benching pub trait PrefixSetAbstraction: Default { fn contains(&mut self, key: Nibbles) -> bool; } impl PrefixSetMutAbstraction for PrefixSetMut { type Frozen = PrefixSet; fn insert(&mut self, key: Nibbles) { Self::insert(self, key) } fn freeze(self) -> Self::Frozen { Self::freeze(self) } } impl PrefixSetAbstraction for PrefixSet { fn contains(&mut self, key: Nibbles) -> bool { Self::contains(self, &key) } } pub fn prefix_set_lookups(c: &mut Criterion) { let mut group = c.benchmark_group("Prefix Set Lookups"); for size in [10, 100, 1_000, 10_000] { // Too slow. #[allow(unexpected_cfgs)] if cfg!(codspeed) && size > 1_000 { continue; } let test_data = generate_test_data(size); use implementations::*; prefix_set_bench::( &mut group, "`BTreeSet` with `Iterator:any` lookup", test_data.clone(), size, ); prefix_set_bench::( &mut group, "`BTreeSet` with `BTreeSet:range` lookup", test_data.clone(), size, ); prefix_set_bench::( &mut group, "`Vec` with custom cursor lookup", test_data.clone(), size, ); prefix_set_bench::( &mut group, "`Vec` with binary search lookup", test_data.clone(), size, ); } } fn prefix_set_bench( group: &mut BenchmarkGroup<'_, WallTime>, description: &str, (preload, input, expected): (Vec, Vec, Vec), size: usize, ) where T: PrefixSetMutAbstraction, T::Frozen: PrefixSetAbstraction, { let setup = || { let mut prefix_set = T::default(); for key in &preload { prefix_set.insert(key.clone()); } (prefix_set.freeze(), input.clone(), expected.clone()) }; let group_id = format!("prefix set | size: {size} | {description}"); group.bench_function(group_id, |b| { b.iter_with_setup(setup, |(mut prefix_set, input, expected)| { for (idx, key) in input.into_iter().enumerate() { let result = black_box(prefix_set.contains(key)); assert_eq!(result, expected[idx]); } }); }); } fn generate_test_data(size: usize) -> (Vec, Vec, Vec) { use prop::collection::vec; let config = ProptestConfig { result_cache: basic_result_cache, ..Default::default() }; let rng = TestRng::deterministic_rng(config.rng_algorithm); let mut runner = TestRunner::new_with_rng(config, rng); let vec_of_nibbles = |range| vec(any_with::(range), size); let mut preload = vec_of_nibbles(32usize.into()).new_tree(&mut runner).unwrap().current(); preload.sort(); preload.dedup(); let mut input = vec_of_nibbles((0..=32usize).into()).new_tree(&mut runner).unwrap().current(); input.sort(); input.dedup(); let expected = input .iter() .map(|prefix| preload.iter().any(|key| key.has_prefix(prefix))) .collect::>(); (preload, input, expected) } criterion_group!(prefix_set, prefix_set_lookups); criterion_main!(prefix_set); mod implementations { use super::*; use std::ops::Bound; #[derive(Default)] pub struct BTreeAnyPrefixSet { keys: BTreeSet, } impl PrefixSetMutAbstraction for BTreeAnyPrefixSet { type Frozen = Self; fn insert(&mut self, key: Nibbles) { self.keys.insert(key); } fn freeze(self) -> Self::Frozen { self } } impl PrefixSetAbstraction for BTreeAnyPrefixSet { fn contains(&mut self, key: Nibbles) -> bool { self.keys.iter().any(|k| k.has_prefix(&key)) } } #[derive(Default)] pub struct BTreeRangeLastCheckedPrefixSet { keys: BTreeSet, last_checked: Option, } impl PrefixSetMutAbstraction for BTreeRangeLastCheckedPrefixSet { type Frozen = Self; fn insert(&mut self, key: Nibbles) { self.keys.insert(key); } fn freeze(self) -> Self::Frozen { self } } impl PrefixSetAbstraction for BTreeRangeLastCheckedPrefixSet { fn contains(&mut self, prefix: Nibbles) -> bool { let range = match self.last_checked.as_ref() { // presumably never hit Some(last) if prefix < *last => (Bound::Unbounded, Bound::Excluded(last)), Some(last) => (Bound::Included(last), Bound::Unbounded), None => (Bound::Unbounded, Bound::Unbounded), }; for key in self.keys.range::(range) { if key.has_prefix(&prefix) { self.last_checked = Some(prefix); return true } if key > &prefix { self.last_checked = Some(prefix); return false } } false } } #[derive(Default)] pub struct VecBinarySearchPrefixSet { keys: Vec, sorted: bool, } impl PrefixSetMutAbstraction for VecBinarySearchPrefixSet { type Frozen = Self; fn insert(&mut self, key: Nibbles) { self.sorted = false; self.keys.push(key); } fn freeze(self) -> Self::Frozen { self } } impl PrefixSetAbstraction for VecBinarySearchPrefixSet { fn contains(&mut self, prefix: Nibbles) -> bool { if !self.sorted { self.keys.sort(); self.sorted = true; } match self.keys.binary_search(&prefix) { Ok(_) => true, Err(idx) => match self.keys.get(idx) { Some(key) => key.has_prefix(&prefix), None => false, // prefix > last key }, } } } #[derive(Default)] pub struct VecCursorPrefixSet { keys: Vec, sorted: bool, index: usize, } impl PrefixSetMutAbstraction for VecCursorPrefixSet { type Frozen = Self; fn insert(&mut self, nibbles: Nibbles) { self.sorted = false; self.keys.push(nibbles); } fn freeze(self) -> Self::Frozen { self } } impl PrefixSetAbstraction for VecCursorPrefixSet { fn contains(&mut self, prefix: Nibbles) -> bool { if !self.sorted { self.keys.sort(); self.sorted = true; } let prefix = prefix; while self.index > 0 && self.keys[self.index] > prefix { self.index -= 1; } for (idx, key) in self.keys[self.index..].iter().enumerate() { if key.has_prefix(&prefix) { self.index += idx; return true } if key > &prefix { self.index += idx; return false } } false } } #[derive(Default)] pub struct VecBinarySearchWithLastFoundPrefixSet { keys: Vec, last_found_idx: usize, sorted: bool, } impl PrefixSetMutAbstraction for VecBinarySearchWithLastFoundPrefixSet { type Frozen = Self; fn insert(&mut self, key: Nibbles) { self.sorted = false; self.keys.push(key); } fn freeze(self) -> Self::Frozen { self } } impl PrefixSetAbstraction for VecBinarySearchWithLastFoundPrefixSet { fn contains(&mut self, prefix: Nibbles) -> bool { if !self.sorted { self.keys.sort(); self.sorted = true; } while self.last_found_idx > 0 && self.keys[self.last_found_idx] > prefix { self.last_found_idx -= 1; } match self.keys[self.last_found_idx..].binary_search(&prefix) { Ok(_) => true, Err(idx) => match self.keys.get(idx) { Some(key) => { self.last_found_idx = idx; key.has_prefix(&prefix) } None => false, // prefix > last key }, } } } }