Unverified Commit c22562ac authored by Michael Müller's avatar Michael Müller Committed by GitHub
Browse files

Implement Entry API for storage2::LazyHashMap (#480)

* [core] Rename Entry to Internal Entry

* [core] Add Entry API for storage2::LazyHashMap

* [core] Add storage2::LazyHashMap::len()

* [core] Migrate tests to use storage2::LazyHashMap::len()

* [core] Implement FromIterator and Extend for storage2::LazyHashMap

* [core] Implement macro to generate LazyHashMap + HashMap Entry API tests

* [core] Remove redundant storage2::HashMap Entry API tests

* [core] Make storage2::HashMap Entry API use storage2::LazyHashMap's Entry API

* [core] Move parameterized Entry API tests into separate file

* [core] Rename InternalEntry to StorageEntry

* [core] Make lazy_hmap module public

* [core] Generate Entry API benches for LazyHashMap and HashMap from macro

* [core] Minor streamlining

* [core] Display hashmap variant in benchmark description

* [core] Fix comment

* [core] Fix typos

* [core] Make more use of BTreeMap Entry API

* [core] Replace unwrap with expect

* [core] Improve comment

* [core] Handle loading from storage

* [core] Restrict unsafe

* [core] Less ops for case "entry not in cache, but in storage"

* [core] Rename len()

* [core] Fix typo

* [core] Fix visibility

* [core] Address comments

* [core] Add test to verify that cache is marked as 'Mutated'

* [core] Shorten code with utility function

* [core] Improve naming
parent ba5aed19
Pipeline #107636 passed with stages
in 7 minutes and 30 seconds
......@@ -22,218 +22,291 @@ use criterion::{
use ink_core::{
env,
storage2::{
collections::{
hashmap::Entry,
HashMap as StorageHashMap,
},
traits::{
KeyPtr,
SpreadLayout,
},
storage2::traits::{
KeyPtr,
SpreadLayout,
},
};
use ink_primitives::Key;
criterion_group!(
populated_cache,
bench_insert_populated_cache,
bench_remove_populated_cache,
);
criterion_group!(
empty_cache,
bench_insert_empty_cache,
bench_remove_empty_cache,
);
criterion_main!(populated_cache, empty_cache,);
/// The number of `ENTIRES` denotes how many test values are put into
/// the `StorageHashMap` used in these benchmarks.
const ENTRIES: i32 = 500;
/// Returns some test values for use in benchmarks.
fn test_values() -> Vec<(i32, i32)> {
(0..ENTRIES)
.into_iter()
.map(|index| (index, index))
.collect::<Vec<_>>()
}
#[cfg(test)]
macro_rules! gen_tests_for_backend {
( $backend:ty ) => {
criterion_group!(
populated_cache,
bench_insert_populated_cache,
bench_remove_populated_cache,
);
criterion_group!(
empty_cache,
bench_insert_empty_cache,
bench_remove_empty_cache,
);
criterion_main!(populated_cache, empty_cache,);
/// Creates a `StorageHashMap` from the given slice.
fn hashmap_from_slice(slice: &[(i32, i32)]) -> StorageHashMap<i32, i32> {
slice.iter().copied().collect::<StorageHashMap<i32, i32>>()
}
/// The number of `ENTIRES` denotes how many test values are put into
/// the hashmap used in these benchmarks.
const ENTRIES: i32 = 500;
/// Creates a `StorageHashMap` from `test_values()`.
fn setup_hashmap() -> StorageHashMap<i32, i32> {
let test_values = test_values();
hashmap_from_slice(&test_values[..])
}
/// Returns some test values for use in benchmarks.
fn test_values() -> Vec<(i32, i32)> {
(0..ENTRIES)
.into_iter()
.map(|index| (index, index))
.collect::<Vec<_>>()
}
/// Returns always the same `KeyPtr`.
fn key_ptr() -> KeyPtr {
let root_key = Key::from([0x42; 32]);
KeyPtr::from(root_key)
}
/// Creates a hashmap from the given slice.
fn hashmap_from_slice(slice: &[(i32, i32)]) -> $backend {
slice.iter().copied().collect::<$backend>()
}
/// Creates a `StorageHashMap` and pushes it to the contract storage.
fn push_storage_hashmap() {
let hmap = setup_hashmap();
SpreadLayout::push_spread(&hmap, &mut key_ptr());
}
/// Creates a hashmap from `test_values()`.
fn setup_hashmap() -> $backend {
let test_values = test_values();
hashmap_from_slice(&test_values[..])
}
/// Pulls a lazily loading `StorageHashMap` instance from the contract storage.
fn pull_storage_hashmap() -> StorageHashMap<i32, i32> {
<StorageHashMap<i32, i32> as SpreadLayout>::pull_spread(&mut key_ptr())
}
/// Returns always the same `KeyPtr`.
fn key_ptr() -> KeyPtr {
let root_key = Key::from([0x42; 32]);
KeyPtr::from(root_key)
}
/// Iteratively checks if an entry is in the `StorageHashMap`. If not, it
/// is inserted. In either case it is incremented afterwards.
fn insert_and_inc(hmap: &mut StorageHashMap<i32, i32>) {
for key in 0..ENTRIES * 2 {
if !black_box(hmap.contains_key(&key)) {
black_box(hmap.insert(key, key));
/// Creates a hashmap and pushes it to the contract storage.
fn push_storage_hashmap() {
let hmap = setup_hashmap();
SpreadLayout::push_spread(&hmap, &mut key_ptr());
}
*black_box(hmap.get_mut(&key)).unwrap() += 1;
}
/// Pulls a lazily loading hashmap instance from the contract storage.
fn pull_storage_hashmap() -> $backend {
<$backend as SpreadLayout>::pull_spread(&mut key_ptr())
}
/// Iteratively checks if an entry is in the hashmap. If not, it
/// is inserted. In either case it is incremented afterwards.
fn insert_and_inc(hmap: &mut $backend) {
for key in 0..ENTRIES * 2 {
if !black_box(contains_key(hmap, &key)) {
black_box(insert(hmap, key, key));
}
*black_box(hmap.get_mut(&key)).unwrap() += 1;
}
}
/// Iteratively checks if an entry is in the hashmap. If not, it
/// is inserted. In either case it is incremented afterwards.
///
/// Uses the Entry API.
fn insert_and_inc_entry_api(hmap: &mut $backend) {
for key in 0..ENTRIES * 2 {
let v = black_box(hmap.entry(key).or_insert(key));
*v += 1;
}
}
/// Iteratively checks if an entry is in the hashmap. If yes, it
/// is taken out.
fn remove(hmap: &mut $backend) {
for key in 0..ENTRIES * 2 {
if black_box(contains_key(hmap, &key)) {
let _ = black_box(take(hmap, &key));
}
}
}
/// Iteratively checks if an entry is in the hashmap. If yes, it
/// is taken out.
///
/// Uses the Entry API.
fn remove_entry_api(hmap: &mut $backend) {
for key in 0..ENTRIES * 2 {
if let Entry::Occupied(o) = black_box(hmap.entry(key)) {
o.remove();
}
}
}
fn bench_insert_populated_cache(c: &mut Criterion) {
let mut group = c.benchmark_group(
format!("{} Compare: `insert_and_inc` and `insert_and_inc_entry_api` (populated cache)", stringify!($backend))
);
group.bench_function("insert_and_inc", |b| {
b.iter_batched_ref(
|| setup_hashmap(),
|hmap| insert_and_inc(hmap),
BatchSize::SmallInput,
)
});
group.bench_function("insert_and_inc_entry_api", |b| {
b.iter_batched_ref(
|| setup_hashmap(),
|hmap| insert_and_inc_entry_api(hmap),
BatchSize::SmallInput,
)
});
group.finish();
}
fn bench_remove_populated_cache(c: &mut Criterion) {
let _ = env::test::run_test::<env::DefaultEnvTypes, _>(|_| {
let mut group = c.benchmark_group(
format!("{} Compare: `remove` and `remove_entry_api` (populated cache)", stringify!($backend))
);
group.bench_function("remove", |b| {
b.iter_batched_ref(
|| setup_hashmap(),
|hmap| remove(hmap),
BatchSize::SmallInput,
)
});
group.bench_function("remove_entry_api", |b| {
b.iter_batched_ref(
|| setup_hashmap(),
|hmap| remove_entry_api(hmap),
BatchSize::SmallInput,
)
});
group.finish();
Ok(())
})
.unwrap();
}
fn bench_insert_empty_cache(c: &mut Criterion) {
let _ = env::test::run_test::<env::DefaultEnvTypes, _>(|_| {
let mut group = c.benchmark_group(
format!("{} Compare: `insert_and_inc` and `insert_and_inc_entry_api` (empty cache)", stringify!($backend))
);
group.bench_function("insert_and_inc", |b| {
b.iter_batched_ref(
|| {
push_storage_hashmap();
pull_storage_hashmap()
},
|hmap| insert_and_inc(hmap),
BatchSize::SmallInput,
)
});
group.bench_function("insert_and_inc_entry_api", |b| {
b.iter_batched_ref(
|| {
push_storage_hashmap();
pull_storage_hashmap()
},
|hmap| insert_and_inc_entry_api(hmap),
BatchSize::SmallInput,
)
});
group.finish();
Ok(())
})
.unwrap();
}
fn bench_remove_empty_cache(c: &mut Criterion) {
let _ = env::test::run_test::<env::DefaultEnvTypes, _>(|_| {
let mut group =
c.benchmark_group(format!("{} Compare: `remove` and `remove_entry_api` (empty cache)", stringify!($backend)));
group.bench_function("remove", |b| {
b.iter_batched_ref(
|| {
push_storage_hashmap();
pull_storage_hashmap()
},
|hmap| remove(hmap),
BatchSize::SmallInput,
)
});
group.bench_function("remove_entry_api", |b| {
b.iter_batched_ref(
|| {
push_storage_hashmap();
pull_storage_hashmap()
},
|hmap| remove_entry_api(hmap),
BatchSize::SmallInput,
)
});
group.finish();
Ok(())
})
.unwrap();
}
};
}
/// Iteratively checks if an entry is in the `StorageHashMap`. If not, it
/// is inserted. In either case it is incremented afterwards.
///
/// Uses the Entry API.
fn insert_and_inc_entry_api(hmap: &mut StorageHashMap<i32, i32>) {
for key in 0..ENTRIES * 2 {
let v = black_box(hmap.entry(key).or_insert(key));
*v += 1;
mod lazyhmap_backend {
use super::*;
use ink_core::{
hash::hasher::Blake2x256Hasher,
storage2::lazy::lazy_hmap::{
Entry,
LazyHashMap,
},
};
gen_tests_for_backend!(LazyHashMap<i32, i32, Blake2x256Hasher>);
pub fn insert(
hmap: &mut LazyHashMap<i32, i32, Blake2x256Hasher>,
key: i32,
value: i32,
) -> Option<i32> {
hmap.put_get(&key, Some(value))
}
}
/// Iteratively checks if an entry is in the `StorageHashMap`. If yes, it
/// is taken out.
fn remove(hmap: &mut StorageHashMap<i32, i32>) {
for key in 0..ENTRIES * 2 {
if black_box(hmap.contains_key(&key)) {
let _ = black_box(hmap.take(&key));
}
pub fn take(
hmap: &mut LazyHashMap<i32, i32, Blake2x256Hasher>,
key: &i32,
) -> Option<i32> {
hmap.put_get(key, None)
}
}
/// Iteratively checks if an entry is in the `StorageHashMap`. If yes, it
/// is taken out.
///
/// Uses the Entry API.
fn remove_entry_api(hmap: &mut StorageHashMap<i32, i32>) {
for key in 0..ENTRIES * 2 {
if let Entry::Occupied(o) = black_box(hmap.entry(key)) {
o.remove();
}
pub fn contains_key(
hmap: &LazyHashMap<i32, i32, Blake2x256Hasher>,
key: &i32,
) -> bool {
hmap.get(key).is_some()
}
}
fn bench_insert_populated_cache(c: &mut Criterion) {
let mut group = c.benchmark_group(
"Compare: `insert_and_inc` and `insert_and_inc_entry_api` (populated cache)",
);
group.bench_function("insert_and_inc", |b| {
b.iter_batched_ref(
|| setup_hashmap(),
|hmap| insert_and_inc(hmap),
BatchSize::SmallInput,
)
});
group.bench_function("insert_and_inc_entry_api", |b| {
b.iter_batched_ref(
|| setup_hashmap(),
|hmap| insert_and_inc_entry_api(hmap),
BatchSize::SmallInput,
)
});
group.finish();
pub fn run() {
self::main()
}
}
fn bench_remove_populated_cache(c: &mut Criterion) {
let _ = env::test::run_test::<env::DefaultEnvTypes, _>(|_| {
let mut group = c.benchmark_group(
"Compare: `remove` and `remove_entry_api` (populated cache)",
);
group.bench_function("remove", |b| {
b.iter_batched_ref(
|| setup_hashmap(),
|hmap| remove(hmap),
BatchSize::SmallInput,
)
});
group.bench_function("remove_entry_api", |b| {
b.iter_batched_ref(
|| setup_hashmap(),
|hmap| remove_entry_api(hmap),
BatchSize::SmallInput,
)
});
group.finish();
Ok(())
})
.unwrap();
}
mod hashmap_backend {
use super::*;
use ink_core::storage2::collections::{
hashmap::Entry,
HashMap as StorageHashMap,
};
fn bench_insert_empty_cache(c: &mut Criterion) {
let _ = env::test::run_test::<env::DefaultEnvTypes, _>(|_| {
let mut group = c.benchmark_group(
"Compare: `insert_and_inc` and `insert_and_inc_entry_api` (empty cache)",
);
group.bench_function("insert_and_inc", |b| {
b.iter_batched_ref(
|| {
push_storage_hashmap();
pull_storage_hashmap()
},
|hmap| insert_and_inc(hmap),
BatchSize::SmallInput,
)
});
group.bench_function("insert_and_inc_entry_api", |b| {
b.iter_batched_ref(
|| {
push_storage_hashmap();
pull_storage_hashmap()
},
|hmap| insert_and_inc_entry_api(hmap),
BatchSize::SmallInput,
)
});
group.finish();
Ok(())
})
.unwrap();
gen_tests_for_backend!(StorageHashMap<i32, i32>);
pub fn insert(
hmap: &mut StorageHashMap<i32, i32>,
key: i32,
value: i32,
) -> Option<i32> {
hmap.insert(key, value)
}
pub fn take(hmap: &mut StorageHashMap<i32, i32>, key: &i32) -> Option<i32> {
hmap.take(key)
}
pub fn contains_key(hmap: &StorageHashMap<i32, i32>, key: &i32) -> bool {
hmap.contains_key(key)
}
pub fn run() {
self::main()
}
}
fn bench_remove_empty_cache(c: &mut Criterion) {
let _ = env::test::run_test::<env::DefaultEnvTypes, _>(|_| {
let mut group =
c.benchmark_group("Compare: `remove` and `remove_entry_api` (empty cache)");
group.bench_function("remove", |b| {
b.iter_batched_ref(
|| {
push_storage_hashmap();
pull_storage_hashmap()
},
|hmap| remove(hmap),
BatchSize::SmallInput,
)
});
group.bench_function("remove_entry_api", |b| {
b.iter_batched_ref(
|| {
push_storage_hashmap();
pull_storage_hashmap()
},
|hmap| remove_entry_api(hmap),
BatchSize::SmallInput,
)
});
group.finish();
Ok(())
})
.unwrap();
fn main() {
hashmap_backend::run();
lazyhmap_backend::run();
}
......@@ -38,7 +38,12 @@ use crate::{
},
storage2::{
collections::Stash,
lazy::LazyHashMap,
lazy::lazy_hmap::{
Entry as LazyEntry,
LazyHashMap,
OccupiedEntry as LazyOccupiedEntry,
VacantEntry as LazyVacantEntry,
},
traits::PackedLayout,
},
};
......@@ -101,51 +106,43 @@ struct ValueEntry<V> {
key_index: KeyIndex,
}
/// A vacant entry with previous and next vacant indices.
pub struct OccupiedEntry<'a, K, V, H = Blake2x256Hasher>
/// An occupied entry that holds the value.
pub struct OccupiedEntry<'a, K, V>
where
K: Ord + Clone + PackedLayout,
V: PackedLayout,
H: Hasher,
Key: From<<H as Hasher>::Output>,
{
/// A reference to the used `HashMap` instance.
base: &'a mut HashMap<K, V, H>,
/// The index of the key associated with this value.
key_index: KeyIndex,
/// The key stored in this entry.
key: K,
/// A reference to the `Stash` instance, containing the keys.
keys: &'a mut Stash<K>,
/// The `LazyHashMap::OccupiedEntry`.
values_entry: LazyOccupiedEntry<'a, K, ValueEntry<V>>,
}
/// A vacant entry with previous and next vacant indices.
pub struct VacantEntry<'a, K, V, H = Blake2x256Hasher>
pub struct VacantEntry<'a, K, V>
where
K: Ord + Clone + PackedLayout,
V: PackedLayout,
H: Hasher,
Key: From<<H as Hasher>::Output>,
{
/// A reference to the used `HashMap` instance.
base: &'a mut HashMap<K, V, H>,
/// The key stored in this entry.
key: K,
/// A reference to the `Stash` instance, containing the keys.
keys: &'a mut Stash<K>,
/// The `LazyHashMap::VacantEntry`.
values_entry: LazyVacantEntry<'a, K, ValueEntry<V>>,
}
/// An entry within the stash.
///
/// The vacant entries within a storage stash form a doubly linked list of
/// vacant entries that is used to quickly re-use their vacant storage.
pub enum Entry<'a, K: 'a, V: 'a, H = Blake2x256Hasher>
pub enum Entry<'a, K: 'a, V: 'a>
where
K: Ord + Clone + PackedLayout,
V: PackedLayout,
H: Hasher,
Key: From<<H as Hasher>::Output>,
{
/// A vacant entry that holds the index to the next and previous vacant entry.
Vacant(VacantEntry<'a, K, V, H>),
Vacant(VacantEntry<'a, K, V>),
/// An occupied entry that holds the value.
Occupied(OccupiedEntry<'a, K, V, H>),
Occupied(OccupiedEntry<'a, K, V>),
}
impl<K, V, H> HashMap<K, V, H>
......@@ -163,11 +160,17 @@ where
}
}
/// Returns the number of key- value pairs stored in the hash map.
/// Returns the number of key-value pairs stored in the hash map.
pub fn len(&self) -> u32 {
self.keys.len()
}
/// Returns the number of key-value pairs stored in the cache.
#[cfg(test)]
pub(crate) fn len_cached_entries(&self) -> u32 {
self.keys.len()
}
/// Returns `true` if the hash map is empty.
pub fn is_empty(&self) -> bool {
self.keys.is_empty()
......@@ -386,33 +389,35 @@ where
}
/// Gets the given key's corresponding entry in the map for in-place manipulation.
pub fn entry(&mut self, key: K) -> Entry<K, V, H> {
let v = self.values.get(&key);
match v {
Some(entry) => {
pub fn entry(&mut self, key: K) -> Entry<K, V> {
let entry = self.values.entry(key);
match entry {
LazyEntry::Occupied(o) => {
Entry::Occupied(OccupiedEntry {
key,
key_index: entry.key_index,
base: self,
keys: &mut self.keys,
values_entry: o,
})
}
LazyEntry::Vacant(v) => {
Entry::Vacant(VacantEntry {
keys: &mut self.keys,