Skip to content
54 changes: 48 additions & 6 deletions benchmarks/compare/canbench_results.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: {}
Expand All @@ -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: {}
Expand All @@ -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
62 changes: 57 additions & 5 deletions benchmarks/compare/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -20,12 +21,16 @@ fn ensure_memory_size(memory: &impl Memory, size: usize) {
}
}

const fn chunk_size<const N: usize>() -> usize {
TOTAL_SIZE / N
}

fn chunk_data(n: usize) -> Vec<Vec<u8>> {
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);
Expand Down Expand Up @@ -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<const CHUNK_SIZE: usize>(mem_id: u8, n: usize) {
let vec: StableVec<BoundedVecN<CHUNK_SIZE>, _> =
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<const CHUNK_SIZE: usize>(mem_id: u8, n: usize) {
write_chunks_vec::<CHUNK_SIZE>(mem_id, n);
let vec: StableVec<BoundedVecN<CHUNK_SIZE>, _> =
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]
Expand All @@ -86,20 +118,40 @@ 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::<SIZE>($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);
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);
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() {}
13 changes: 12 additions & 1 deletion benchmarks/src/common.rs
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -29,6 +29,17 @@ impl<const K: usize> Random for UnboundedVecN<K> {
}
}

impl<const K: usize> Random for BoundedVecN<K> {
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()
Expand Down
45 changes: 45 additions & 0 deletions src/storable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,51 @@ impl<const N: usize> Storable for UnboundedVecN<N> {
const BOUND: Bound = Bound::Unbounded;
}

/// Bounded vector of bytes, always of length `N`.
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub struct BoundedVecN<const N: usize>(Vec<u8>);

impl<const N: usize> BoundedVecN<N> {
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<const N: usize> Default for BoundedVecN<N> {
fn default() -> Self {
Self(vec![0; N])
}
}

impl<const N: usize> Storable for BoundedVecN<N> {
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
Expand Down