Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
326 changes: 198 additions & 128 deletions benchmarks/btreemap/canbench_results.yml

Large diffs are not rendered by default.

155 changes: 155 additions & 0 deletions benchmarks/btreemap/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,161 @@ pub fn btreemap_v2_get_10mib_values() -> BenchResult {
})
}

// Batch insertion. Every `insert_many` benchmark has an `insert_loop` counterpart over
// exactly the same keys and values, so the two are directly comparable. The pairs are kept
// adjacent below; the one exception is `insert_many_seq`, whose counterpart is the
// pre-existing `btreemap_v2_insert_seq_u64_u64`.

#[bench(raw)]
pub fn btreemap_v2_insert_many_seq_u64_u64() -> BenchResult {
let mut btree = BTreeMap::new(DefaultMemoryImpl::default());
let count = 10_000u64;
let mut rng = Rng::from_seed(0);

// Same keys and values as `btreemap_v2_insert_seq_u64_u64`.
let items: Vec<(u64, u64)> = (0..count).map(|i| (i, rng.rand_u64())).collect();
bench_fn(|| {
btree.insert_many(items);
})
}

// Descending keys. The held path should unwind leftwards just as it unwinds rightwards for
// ascending keys, so this ought to coalesce about as well.
#[bench(raw)]
pub fn btreemap_v2_insert_loop_desc_u64_u64() -> BenchResult {
let mut btree = BTreeMap::new(DefaultMemoryImpl::default());
let count = 10_000u64;
let mut rng = Rng::from_seed(0);
let items: Vec<(u64, u64)> = (0..count).rev().map(|i| (i, rng.rand_u64())).collect();
bench_fn(|| {
for (key, value) in items {
btree.insert(key, value);
}
})
}

#[bench(raw)]
pub fn btreemap_v2_insert_many_desc_u64_u64() -> BenchResult {
let mut btree = BTreeMap::new(DefaultMemoryImpl::default());
let count = 10_000u64;
let mut rng = Rng::from_seed(0);
let items: Vec<(u64, u64)> = (0..count).rev().map(|i| (i, rng.rand_u64())).collect();
bench_fn(|| {
btree.insert_many(items);
})
}

// The baseline for both unordered variants below.
#[bench(raw)]
pub fn btreemap_v2_insert_loop_random_u64_u64() -> BenchResult {
let mut btree = BTreeMap::new(DefaultMemoryImpl::default());
let mut rng = Rng::from_seed(0);
let items = generate_random_kv::<u64, u64>(10_000, &mut rng);
bench_fn(|| {
for (key, value) in items {
btree.insert(key, value);
}
})
}

// `insert_many` wants ascending keys, so a caller holding unordered data sorts it first.
// The sort is inside the measured section.
#[bench(raw)]
pub fn btreemap_v2_insert_many_sorted_random_u64_u64() -> BenchResult {
let mut btree = BTreeMap::new(DefaultMemoryImpl::default());
let mut rng = Rng::from_seed(0);
let items = generate_random_kv::<u64, u64>(10_000, &mut rng);
bench_fn(|| {
let mut items = items;
items.sort_by(|(a, _), (b, _)| a.cmp(b));
btree.insert_many(items);
})
}

// Feeding unordered keys forfeits the coalescing: the held path unwinds on almost every
// key, so this should land close to the plain insert loop rather than beating it.
#[bench(raw)]
pub fn btreemap_v2_insert_many_unsorted_u64_u64() -> BenchResult {
let mut btree = BTreeMap::new(DefaultMemoryImpl::default());
let mut rng = Rng::from_seed(0);
let items = generate_random_kv::<u64, u64>(10_000, &mut rng);
bench_fn(|| {
btree.insert_many(items);
})
}

// Inserting a sorted run into a map that already holds 100k entries: the batch lands in
// leaves that mostly have room, which is where coalescing pays most.
#[bench(raw)]
pub fn btreemap_v2_insert_loop_into_existing_u64_u64() -> BenchResult {
let mut btree = BTreeMap::new(DefaultMemoryImpl::default());
for i in 0..100_000u64 {
btree.insert(i * 10, i);
}
let items: Vec<(u64, u64)> = (0..10_000u64).map(|i| (i * 10 + 5, i)).collect();
bench_fn(|| {
for (key, value) in items {
btree.insert(key, value);
}
})
}

#[bench(raw)]
pub fn btreemap_v2_insert_many_into_existing_u64_u64() -> BenchResult {
let mut btree = BTreeMap::new(DefaultMemoryImpl::default());
for i in 0..100_000u64 {
btree.insert(i * 10, i);
}
let items: Vec<(u64, u64)> = (0..10_000u64).map(|i| (i * 10 + 5, i)).collect();
bench_fn(|| {
btree.insert_many(items);
})
}

// Pure overwrite of existing keys, with values large enough to live on V2 overflow pages.
// `insert` has to read the displaced value back in order to return it; `insert_many`
// discards it and so skips that read entirely.
#[bench(raw)]
pub fn btreemap_v2_insert_loop_overwrite_1kib_values() -> BenchResult {
let count = 2_000usize;
let mut btree = BTreeMap::new(DefaultMemoryImpl::default());
let mut rng = Rng::from_seed(0);
for (i, value) in generate_random_blocks(count, 1024, &mut rng)
.into_iter()
.enumerate()
{
btree.insert(i as u64, value);
}
let new_values = generate_random_blocks(count, 1024, &mut rng);
bench_fn(|| {
for (i, value) in new_values.into_iter().enumerate() {
btree.insert(i as u64, value);
}
})
}

#[bench(raw)]
pub fn btreemap_v2_insert_many_overwrite_1kib_values() -> BenchResult {
let count = 2_000usize;
let mut btree = BTreeMap::new(DefaultMemoryImpl::default());
let mut rng = Rng::from_seed(0);
for (i, value) in generate_random_blocks(count, 1024, &mut rng)
.into_iter()
.enumerate()
{
btree.insert(i as u64, value);
}
let new_values = generate_random_blocks(count, 1024, &mut rng);
bench_fn(|| {
btree.insert_many(
new_values
.into_iter()
.enumerate()
.map(|(i, v)| (i as u64, v)),
);
})
}

// Benchmarks for `BTreeMap::contains_key`.
// Reduced grid: contains_key traversal is identical to get, only skips value deserialization.
bench_tests! {
Expand Down
57 changes: 57 additions & 0 deletions src/btreemap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
//! ----------------------------------------
//! ```
mod allocator;
mod bulk_insert;
mod iter;
mod node;
mod node_cache;
Expand All @@ -59,6 +60,7 @@ use crate::{
Memory, Storable,
};
use allocator::Allocator;
use bulk_insert::BulkInsert;
pub use iter::Iter;
use node::{DerivedPageSize, Entry, Node, NodeType, PageSize, Version};
use node_cache::NodeCache;
Expand Down Expand Up @@ -706,6 +708,61 @@ where
.map(V::from_bytes)
}

/// Inserts many key-value pairs, writing each modified node to stable memory at most
/// once instead of once per key.
///
/// [`insert`](Self::insert) writes the target node — and, when a node splits, its
/// sibling and parent too — on every call, so inserting a run of nearby keys rewrites
/// the same node again and again. `insert_many` instead keeps the current root-to-leaf
/// path in memory and saves a modified node to stable memory only once the input has
/// moved past it. It also writes the header once for the whole call rather than once
/// per key.
///
/// The iterator is consumed lazily and never collected, so the memory cost is one tree
/// path however many pairs are supplied.
///
/// Duplicate keys are applied in the order given, so the last occurrence wins, exactly
/// as with repeated `insert` calls — the resulting map is identical, only the number of
/// writes differs.
///
/// PRECONDITION: for every pair
/// key.to_bytes().len() <= max_size(Key)
/// value.to_bytes().len() <= max_size(Value)
///
/// # Supply keys in sorted order for best performance
///
/// The coalescing pays off exactly when consecutive keys land in the same node, so
/// **feed the pairs in sorted key order**. Both ascending and descending work just as
/// well — what matters is that consecutive keys are close together, not which way the
/// run travels.
///
/// Out-of-order keys are still handled correctly — the path unwinds to an ancestor that
/// covers the key, as far as the root if necessary — but each one costs a fresh
/// descent, and shuffled input performs no better than calling
/// [`insert`](Self::insert) in a loop. Sort first if the data is not already ordered.
///
/// # Examples
///
/// ```rust
/// use ic_stable_structures::{BTreeMap, DefaultMemoryImpl};
///
/// let mut map: BTreeMap<u64, u64, _> = BTreeMap::new(DefaultMemoryImpl::default());
///
/// // Streamed straight from the iterator; nothing is materialized.
/// map.insert_many((0..1_000).map(|i| (i, i * 2)));
///
/// assert_eq!(map.len(), 1_000);
/// assert_eq!(map.get(&500), Some(1_000));
/// ```
pub fn insert_many(&mut self, entries: impl IntoIterator<Item = (K, V)>) {
let mut bulk = BulkInsert::new(self);
for (key, value) in entries {
bulk.insert(key, value);
}
// Dropping `bulk` here writes out the path it is still holding, along with the
// header. That is what completes the batch.
}

/// Inserts an entry into a node that is *not full*.
fn insert_nonfull(
&mut self,
Expand Down
Loading
Loading