diff --git a/benchmarks/compare/canbench_results.yml b/benchmarks/compare/canbench_results.yml index 0fc9dec8..add6fc9e 100644 --- a/benchmarks/compare/canbench_results.yml +++ b/benchmarks/compare/canbench_results.yml @@ -2,21 +2,21 @@ benches: read_chunks_btreemap_1: total: calls: 1 - instructions: 1220197642 + instructions: 1219162597 heap_increase: 3233 stable_memory_increase: 1665 scopes: {} read_chunks_btreemap_1k: total: calls: 1 - instructions: 5418812624 + instructions: 5414908676 heap_increase: 1604 stable_memory_increase: 1665 scopes: {} read_chunks_btreemap_1m: total: calls: 1 - instructions: 133684080756 + instructions: 133588820086 heap_increase: 1892 stable_memory_increase: 3201 scopes: {} @@ -41,24 +41,45 @@ benches: heap_increase: 1892 stable_memory_increase: 1665 scopes: {} + read_chunks_vec_1: + total: + calls: 1 + instructions: 1363286727 + heap_increase: 3202 + stable_memory_increase: 1665 + scopes: {} + read_chunks_vec_1k: + total: + calls: 1 + instructions: 1378054827 + heap_increase: 1602 + stable_memory_increase: 1665 + scopes: {} + read_chunks_vec_1m: + total: + calls: 1 + instructions: 4584728190 + heap_increase: 1892 + stable_memory_increase: 1665 + scopes: {} write_chunks_btreemap_1: total: calls: 1 - instructions: 1070838059 + instructions: 1069803049 heap_increase: 3233 stable_memory_increase: 1665 scopes: {} write_chunks_btreemap_1k: total: calls: 1 - instructions: 4918823602 + instructions: 4914919689 heap_increase: 1604 stable_memory_increase: 1665 scopes: {} write_chunks_btreemap_1m: total: calls: 1 - instructions: 89917688426 + instructions: 89822427791 heap_increase: 1892 stable_memory_increase: 3201 scopes: {} @@ -83,4 +104,25 @@ benches: heap_increase: 1892 stable_memory_increase: 1665 scopes: {} + write_chunks_vec_1: + total: + calls: 1 + instructions: 1257791168 + heap_increase: 3202 + stable_memory_increase: 1665 + scopes: {} + write_chunks_vec_1k: + total: + calls: 1 + instructions: 1271592530 + heap_increase: 1602 + stable_memory_increase: 1665 + scopes: {} + write_chunks_vec_1m: + total: + calls: 1 + instructions: 3575186447 + heap_increase: 1892 + stable_memory_increase: 1665 + scopes: {} version: 0.1.15 diff --git a/benchmarks/compare/src/main.rs b/benchmarks/compare/src/main.rs index 4e62b4bd..310a3dc4 100644 --- a/benchmarks/compare/src/main.rs +++ b/benchmarks/compare/src/main.rs @@ -2,7 +2,8 @@ use canbench_rs::{bench, bench_fn}; use ic_cdk::api::stable::WASM_PAGE_SIZE_IN_BYTES; use ic_stable_structures::{ memory_manager::{MemoryId, MemoryManager}, - BTreeMap, DefaultMemoryImpl, Memory, + storable::BoundedVecN, + BTreeMap, DefaultMemoryImpl, Memory, Vec as StableVec, }; const TOTAL_SIZE: usize = 100 * 1024 * 1024; // 100 MiB @@ -20,12 +21,16 @@ fn ensure_memory_size(memory: &impl Memory, size: usize) { } } +const fn chunk_size() -> usize { + TOTAL_SIZE / N +} + fn chunk_data(n: usize) -> Vec> { let chunk_size = TOTAL_SIZE / n; (0..n).map(|_| vec![37; chunk_size]).collect() } -// Stable memory benchmarks +// Stable Memory benchmarks fn write_chunks_stable(mem_id: u8, n: usize) { let memory = init_memory(mem_id); @@ -76,7 +81,34 @@ fn read_chunks_btreemap(mem_id: u8, n: usize) { }); } -// Macro to define a single benchmark function +// StableVec benchmarks + +fn write_chunks_vec(mem_id: u8, n: usize) { + let vec: StableVec, _> = + StableVec::new(init_memory(mem_id)).expect("Vec::new failed"); + let chunks: Vec<_> = chunk_data(n).iter().map(|v| BoundedVecN::from(v)).collect(); + + bench_fn(|| { + for chunk in &chunks { + vec.push(chunk).expect("Vec::push failed"); + } + }); +} + +fn read_chunks_vec(mem_id: u8, n: usize) { + write_chunks_vec::(mem_id, n); + let vec: StableVec, _> = + StableVec::init(init_memory(mem_id)).expect("Vec::init failed"); + + bench_fn(|| { + for i in 0..n as u64 { + let _ = vec.get(i); + } + }); +} + +// Benchmark macros + macro_rules! bench_case { ($name:ident, $func:ident, $mem_id:expr, $n:expr) => { #[bench] @@ -86,7 +118,19 @@ macro_rules! bench_case { }; } -// Stable Memory benchmarks +macro_rules! bench_case_sized { + ($name:ident, $func:ident, $mem_id:expr, $n:expr) => { + #[bench] + fn $name() { + const SIZE: usize = chunk_size::<$n>(); + $func::($mem_id, $n); + } + }; +} + +// Benchmark registrations + +// Stable Memory bench_case!(write_chunks_stable_1, write_chunks_stable, 10, 1); bench_case!(write_chunks_stable_1k, write_chunks_stable, 11, K); bench_case!(write_chunks_stable_1m, write_chunks_stable, 12, M); @@ -94,7 +138,7 @@ bench_case!(read_chunks_stable_1, read_chunks_stable, 20, 1); bench_case!(read_chunks_stable_1k, read_chunks_stable, 21, K); bench_case!(read_chunks_stable_1m, read_chunks_stable, 22, M); -// BTreeMap benchmarks +// BTreeMap bench_case!(write_chunks_btreemap_1, write_chunks_btreemap, 30, 1); bench_case!(write_chunks_btreemap_1k, write_chunks_btreemap, 31, K); bench_case!(write_chunks_btreemap_1m, write_chunks_btreemap, 32, M); @@ -102,4 +146,12 @@ bench_case!(read_chunks_btreemap_1, read_chunks_btreemap, 40, 1); bench_case!(read_chunks_btreemap_1k, read_chunks_btreemap, 41, K); bench_case!(read_chunks_btreemap_1m, read_chunks_btreemap, 42, M); +// StableVec +bench_case_sized!(write_chunks_vec_1, write_chunks_vec, 50, 1); +bench_case_sized!(write_chunks_vec_1k, write_chunks_vec, 51, K); +bench_case_sized!(write_chunks_vec_1m, write_chunks_vec, 52, M); +bench_case_sized!(read_chunks_vec_1, read_chunks_vec, 60, 1); +bench_case_sized!(read_chunks_vec_1k, read_chunks_vec, 61, K); +bench_case_sized!(read_chunks_vec_1m, read_chunks_vec, 62, M); + fn main() {} diff --git a/benchmarks/src/common.rs b/benchmarks/src/common.rs index 8a921c11..5e283acd 100644 --- a/benchmarks/src/common.rs +++ b/benchmarks/src/common.rs @@ -1,4 +1,4 @@ -use ic_stable_structures::storable::{Blob, Storable, UnboundedVecN}; +use ic_stable_structures::storable::{Blob, BoundedVecN, Storable, UnboundedVecN}; use tiny_rng::{Rand, Rng}; pub trait Random { @@ -29,6 +29,17 @@ impl Random for UnboundedVecN { } } +impl Random for BoundedVecN { + fn random(rng: &mut Rng) -> Self { + let size = rng.rand_u32() % Self::max_size(); + let mut buf = Vec::with_capacity(size as usize); + for _ in 0..size { + buf.push(rng.rand_u8()); + } + Self::from(&buf) + } +} + impl Random for u64 { fn random(rng: &mut Rng) -> Self { rng.rand_u64() diff --git a/src/storable.rs b/src/storable.rs index f40e1a3a..c6a2a8d7 100644 --- a/src/storable.rs +++ b/src/storable.rs @@ -232,6 +232,51 @@ impl Storable for UnboundedVecN { const BOUND: Bound = Bound::Unbounded; } +/// Bounded vector of bytes, always of length `N`. +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] +pub struct BoundedVecN(Vec); + +impl BoundedVecN { + pub fn max_size() -> u32 { + N as u32 + } + + pub fn from(slice: &[u8]) -> Self { + assert!( + slice.len() <= N, + "expected a slice with length <= {} bytes, but found {} bytes", + N, + slice.len() + ); + let mut vec = Vec::with_capacity(N); + vec.extend_from_slice(slice); + vec.resize(N, 0); + Self(vec) + } +} + +impl Default for BoundedVecN { + fn default() -> Self { + Self(vec![0; N]) + } +} + +impl Storable for BoundedVecN { + fn to_bytes(&self) -> Cow<[u8]> { + Cow::Owned(self.0.clone()) + } + + #[inline] + fn from_bytes(bytes: Cow<[u8]>) -> Self { + Self(bytes.into_owned()) + } + + const BOUND: Bound = Bound::Bounded { + max_size: N as u32, + is_fixed_size: false, + }; +} + // NOTE: Below are a few implementations of `Storable` for common types. // Some of these implementations use `unwrap`, as opposed to returning a `Result` // with a possible error. The reason behind this decision is that these