diff --git a/stacker/Cargo.toml b/stacker/Cargo.toml index 1702940cd0..c9eec6dc34 100644 --- a/stacker/Cargo.toml +++ b/stacker/Cargo.toml @@ -13,10 +13,11 @@ common = { version = "0.7", path = "../common/", package = "tantivy-common" } ahash = { version = "0.8.11", default-features = false, optional = true } rand_distr = "0.4.3" + [[bench]] harness = false -name = "crit_bench" -path = "benches/crit_bench.rs" +name = "bench" +path = "benches/bench.rs" [[example]] name = "hashmap" @@ -25,9 +26,9 @@ path = "example/hashmap.rs" [dev-dependencies] rand = "0.8.5" zipf = "7.0.0" -criterion = { git = "https://github.com/PSeitz/criterion.rs/", rev = "e6f98ee"} # This fork includes stack randomization to reduce caching effects rustc-hash = "1.1.0" proptest = "1.2.0" +binggan = { version = "0.12.0" } [features] compare_hash_only = ["ahash"] # Compare hash only, not the key in the Hashmap diff --git a/stacker/benches/bench.rs b/stacker/benches/bench.rs new file mode 100644 index 0000000000..346586969f --- /dev/null +++ b/stacker/benches/bench.rs @@ -0,0 +1,206 @@ +use binggan::plugins::PeakMemAllocPlugin; +use binggan::{black_box, BenchRunner, PeakMemAlloc, INSTRUMENTED_SYSTEM}; +use rand::SeedableRng; +use rustc_hash::FxHashMap; +use tantivy_stacker::{ArenaHashMap, ExpUnrolledLinkedList, MemoryArena}; + +const ALICE: &str = include_str!("../../benches/alice.txt"); + +#[global_allocator] +pub static GLOBAL: &PeakMemAlloc = &INSTRUMENTED_SYSTEM; + +fn bench_vint() { + let mut runner = BenchRunner::new(); + // Set the peak mem allocator. This will enable peak memory reporting. + runner.add_plugin(PeakMemAllocPlugin::new(GLOBAL)); + + { + let input_bytes = ALICE.len(); + + let alice_terms_as_bytes: Vec<&[u8]> = ALICE + .split_ascii_whitespace() + .map(|el| el.as_bytes()) + .collect(); + + let alice_terms_as_bytes_with_docid: Vec<(u32, &[u8])> = ALICE + .split_ascii_whitespace() + .map(|el| el.as_bytes()) + .enumerate() + .map(|(docid, el)| (docid as u32, el)) + .collect(); + + // Alice benchmark + let mut group = runner.new_group(); + group.set_name(format!("alice (num terms: {})", ALICE.len())); + group.set_input_size(input_bytes); + group.register_with_input("hashmap", &alice_terms_as_bytes, move |data| { + black_box(create_hash_map(data.iter())); + Some(()) + }); + group.register_with_input( + "hasmap with postings", + &alice_terms_as_bytes_with_docid, + move |data| { + black_box(create_hash_map_with_expull(data.iter().cloned())); + Some(()) + }, + ); + group.register_with_input( + "fxhashmap ref postings", + &alice_terms_as_bytes, + move |data| { + black_box(create_fx_hash_ref_map_with_expull(data.iter().cloned())); + Some(()) + }, + ); + group.register_with_input( + "fxhasmap owned postings", + &alice_terms_as_bytes, + move |data| { + black_box(create_fx_hash_owned_map_with_expull(data.iter().cloned())); + Some(()) + }, + ); + group.run(); + } + + { + for (num_numbers, num_numbers_label) in [ + (100_000u64, "100k"), + (1_000_000, "1mio"), + (2_000_000, "2mio"), + (5_000_000, "5mio"), + ] { + // benchmark unique numbers + { + let numbers: Vec<[u8; 8]> = (0..num_numbers).map(|el| el.to_le_bytes()).collect(); + let numbers_with_doc: Vec<_> = numbers + .iter() + .enumerate() + .map(|(docid, el)| (docid as u32, el)) + .collect(); + + let input_bytes = numbers.len() * 8; + let mut group = runner.new_group(); + group.set_name(format!("numbers unique {}", num_numbers_label)); + group.set_input_size(input_bytes); + group.register_with_input("only hashmap", &numbers, move |data| { + black_box(create_hash_map(data.iter())); + Some(()) + }); + group.register_with_input("hasmap with postings", &numbers_with_doc, move |data| { + black_box(create_hash_map_with_expull(data.iter().cloned())); + Some(()) + }); + group.run(); + } + // benchmark zipfs distribution numbers + { + use rand::distributions::Distribution; + use rand::rngs::StdRng; + let mut rng = StdRng::from_seed([3u8; 32]); + let zipf = zipf::ZipfDistribution::new(10_000, 1.03).unwrap(); + let numbers: Vec<[u8; 8]> = (0..num_numbers) + .map(|_| zipf.sample(&mut rng).to_le_bytes()) + .collect(); + let numbers_with_doc: Vec<_> = numbers + .iter() + .enumerate() + .map(|(docid, el)| (docid as u32, el)) + .collect(); + + let input_bytes = numbers.len() * 8; + let mut group = runner.new_group(); + group.set_name(format!("zipfs numbers {}", num_numbers_label)); + group.set_input_size(input_bytes); + group.register_with_input("hashmap", &numbers, move |data| { + black_box(create_hash_map(data.iter())); + Some(()) + }); + group.register_with_input("hasmap with postings", &numbers_with_doc, move |data| { + black_box(create_hash_map_with_expull(data.iter().cloned())); + Some(()) + }); + group.run(); + } + } + } +} + +fn main() { + bench_vint(); +} + +const HASHMAP_CAPACITY: usize = 1 << 15; + +/// Only records the doc ids +#[derive(Clone, Default, Copy)] +pub struct DocIdRecorder { + stack: ExpUnrolledLinkedList, +} +impl DocIdRecorder { + fn new_doc(&mut self, doc: u32, arena: &mut MemoryArena) { + self.stack.writer(arena).write_u32_vint(doc); + } +} + +fn create_hash_map>(terms: impl Iterator) -> ArenaHashMap { + let mut map = ArenaHashMap::with_capacity(HASHMAP_CAPACITY); + for term in terms { + map.mutate_or_create(term.as_ref(), |val| { + if let Some(mut val) = val { + val += 1; + val + } else { + 1u64 + } + }); + } + + map +} + +fn create_hash_map_with_expull>( + terms: impl Iterator, +) -> ArenaHashMap { + let mut memory_arena = MemoryArena::default(); + let mut map = ArenaHashMap::with_capacity(HASHMAP_CAPACITY); + for (i, term) in terms { + map.mutate_or_create(term.as_ref(), |val: Option| { + if let Some(mut rec) = val { + rec.new_doc(i, &mut memory_arena); + rec + } else { + DocIdRecorder::default() + } + }); + } + + map +} + +fn create_fx_hash_ref_map_with_expull( + terms: impl Iterator, +) -> FxHashMap<&'static [u8], Vec> { + let terms = terms.enumerate(); + let mut map = FxHashMap::with_capacity_and_hasher(HASHMAP_CAPACITY, Default::default()); + for (i, term) in terms { + map.entry(term.as_ref()) + .or_insert_with(Vec::new) + .push(i as u32); + } + map +} + +fn create_fx_hash_owned_map_with_expull( + terms: impl Iterator, +) -> FxHashMap, Vec> { + let terms = terms.enumerate(); + let mut map = FxHashMap::with_capacity_and_hasher(HASHMAP_CAPACITY, Default::default()); + for (i, term) in terms { + map.entry(term.as_ref().to_vec()) + .or_insert_with(Vec::new) + .push(i as u32); + } + map +} diff --git a/stacker/benches/crit_bench.rs b/stacker/benches/crit_bench.rs deleted file mode 100644 index a7618883b6..0000000000 --- a/stacker/benches/crit_bench.rs +++ /dev/null @@ -1,175 +0,0 @@ -#![allow(dead_code)] -extern crate criterion; - -use criterion::*; -use rand::SeedableRng; -use rustc_hash::FxHashMap; -use tantivy_stacker::{ArenaHashMap, ExpUnrolledLinkedList, MemoryArena}; - -const ALICE: &str = include_str!("../../benches/alice.txt"); - -fn bench_hashmap_throughput(c: &mut Criterion) { - let plot_config = PlotConfiguration::default().summary_scale(AxisScale::Linear); - - let mut group = c.benchmark_group("CreateHashMap"); - group.plot_config(plot_config); - - let input_bytes = ALICE.len() as u64; - - let alice_terms_as_bytes: Vec<&[u8]> = ALICE - .split_ascii_whitespace() - .map(|el| el.as_bytes()) - .collect(); - - let alice_terms_as_bytes_with_docid: Vec<(u32, &[u8])> = ALICE - .split_ascii_whitespace() - .map(|el| el.as_bytes()) - .enumerate() - .map(|(docid, el)| (docid as u32, el)) - .collect(); - - group.throughput(Throughput::Bytes(input_bytes)); - - group.bench_with_input( - BenchmarkId::new("alice".to_string(), input_bytes), - &alice_terms_as_bytes, - |b, i| b.iter(|| create_hash_map(i.iter())), - ); - group.bench_with_input( - BenchmarkId::new("alice_expull".to_string(), input_bytes), - &alice_terms_as_bytes_with_docid, - |b, i| b.iter(|| create_hash_map_with_expull(i.iter().cloned())), - ); - - group.bench_with_input( - BenchmarkId::new("alice_fx_hashmap_ref_expull".to_string(), input_bytes), - &alice_terms_as_bytes, - |b, i| b.iter(|| create_fx_hash_ref_map_with_expull(i.iter().cloned())), - ); - - group.bench_with_input( - BenchmarkId::new("alice_fx_hashmap_owned_expull".to_string(), input_bytes), - &alice_terms_as_bytes, - |b, i| b.iter(|| create_fx_hash_owned_map_with_expull(i.iter().cloned())), - ); - - // numbers - let input_bytes = 1_000_000 * 8; - group.throughput(Throughput::Bytes(input_bytes)); - let numbers: Vec<[u8; 8]> = (0..1_000_000u64).map(|el| el.to_le_bytes()).collect(); - - group.bench_with_input( - BenchmarkId::new("numbers".to_string(), input_bytes), - &numbers, - |b, i| b.iter(|| create_hash_map(i.iter().cloned())), - ); - - let numbers_with_doc: Vec<_> = numbers - .iter() - .enumerate() - .map(|(docid, el)| (docid as u32, el)) - .collect(); - - group.bench_with_input( - BenchmarkId::new("ids_expull".to_string(), input_bytes), - &numbers_with_doc, - |b, i| b.iter(|| create_hash_map_with_expull(i.iter().cloned())), - ); - - // numbers zipf - use rand::distributions::Distribution; - use rand::rngs::StdRng; - let mut rng = StdRng::from_seed([3u8; 32]); - let zipf = zipf::ZipfDistribution::new(10_000, 1.03).unwrap(); - - let input_bytes = 1_000_000 * 8; - group.throughput(Throughput::Bytes(input_bytes)); - let zipf_numbers: Vec<[u8; 8]> = (0..1_000_000u64) - .map(|_| zipf.sample(&mut rng).to_le_bytes()) - .collect(); - - group.bench_with_input( - BenchmarkId::new("numbers_zipf".to_string(), input_bytes), - &zipf_numbers, - |b, i| b.iter(|| create_hash_map(i.iter().cloned())), - ); - - group.finish(); -} - -const HASHMAP_SIZE: usize = 1 << 15; - -/// Only records the doc ids -#[derive(Clone, Default, Copy)] -pub struct DocIdRecorder { - stack: ExpUnrolledLinkedList, -} -impl DocIdRecorder { - fn new_doc(&mut self, doc: u32, arena: &mut MemoryArena) { - self.stack.writer(arena).write_u32_vint(doc); - } -} - -fn create_hash_map>(terms: impl Iterator) -> ArenaHashMap { - let mut map = ArenaHashMap::with_capacity(HASHMAP_SIZE); - for term in terms { - map.mutate_or_create(term.as_ref(), |val| { - if let Some(mut val) = val { - val += 1; - val - } else { - 1u64 - } - }); - } - - map -} - -fn create_hash_map_with_expull>( - terms: impl Iterator, -) -> ArenaHashMap { - let mut memory_arena = MemoryArena::default(); - let mut map = ArenaHashMap::with_capacity(HASHMAP_SIZE); - for (i, term) in terms { - map.mutate_or_create(term.as_ref(), |val: Option| { - if let Some(mut rec) = val { - rec.new_doc(i, &mut memory_arena); - rec - } else { - DocIdRecorder::default() - } - }); - } - - map -} - -fn create_fx_hash_ref_map_with_expull( - terms: impl Iterator, -) -> FxHashMap<&'static [u8], Vec> { - let terms = terms.enumerate(); - let mut map = FxHashMap::with_capacity_and_hasher(HASHMAP_SIZE, Default::default()); - for (i, term) in terms { - map.entry(term.as_ref()) - .or_insert_with(Vec::new) - .push(i as u32); - } - map -} - -fn create_fx_hash_owned_map_with_expull( - terms: impl Iterator, -) -> FxHashMap, Vec> { - let terms = terms.enumerate(); - let mut map = FxHashMap::with_capacity_and_hasher(HASHMAP_SIZE, Default::default()); - for (i, term) in terms { - map.entry(term.as_ref().to_vec()) - .or_insert_with(Vec::new) - .push(i as u32); - } - map -} - -criterion_group!(block_benches, bench_hashmap_throughput,); -criterion_main!(block_benches);