Unverified Commit eeb6bc0f authored by Michael Müller's avatar Michael Müller Committed by GitHub

[core] Add Entry API for storage2::HashMap (#477)

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

* [core] Replace unwrap's with expect's

* [core] Shorten code

* [core] Improve tests

* [core] Improve tests

* [core] Add benches for storage2::HashMap Entry API

* [core] Update comments

* [core] Add + update benches

* [core] Improve benches code, add more benches

* [core] Improve expect messages

* [core] Simplify test structure

* [core] Implement review comments

* [core] Shorten bench code

* [core] Fix bench iterations

* [core] Minimze black_box'es

* [core] Remove code dups

* [core] Shorten code
parent d99ea915
Pipeline #100984 passed with stages
in 10 minutes and 27 seconds
......@@ -89,3 +89,7 @@ harness = false
[[bench]]
name = "bench_bitstash"
harness = false
[[bench]]
name = "bench_hashmap"
harness = false
// Copyright 2019-2020 Parity Technologies (UK) Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use criterion::{
black_box,
criterion_group,
criterion_main,
BatchSize,
Criterion,
};
use ink_core::{
env,
storage2::{
collections::{
hashmap::Entry,
HashMap as StorageHashMap,
},
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<_>>()
}
/// Creates a `StorageHashMap` from the given slice.
fn hashmap_from_slice(slice: &[(i32, i32)]) -> StorageHashMap<i32, i32> {
slice.iter().copied().collect::<StorageHashMap<i32, i32>>()
}
/// Creates a `StorageHashMap` from `test_values()`.
fn setup_hashmap() -> StorageHashMap<i32, i32> {
let test_values = test_values();
hashmap_from_slice(&test_values[..])
}
/// Returns always the same `KeyPtr`.
fn key_ptr() -> KeyPtr {
let root_key = Key::from([0x42; 32]);
KeyPtr::from(root_key)
}
/// 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());
}
/// 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())
}
/// 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));
}
*black_box(hmap.get_mut(&key)).unwrap() += 1;
}
}
/// 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;
}
}
/// 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));
}
}
}
/// 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();
}
}
}
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();
}
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();
}
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();
}
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();
}
......@@ -101,6 +101,53 @@ struct ValueEntry<V> {
key_index: KeyIndex,
}
/// A vacant entry with previous and next vacant indices.
pub struct OccupiedEntry<'a, K, V, H = Blake2x256Hasher>
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 vacant entry with previous and next vacant indices.
pub struct VacantEntry<'a, K, V, H = Blake2x256Hasher>
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,
}
/// 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>
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>),
/// An occupied entry that holds the value.
Occupied(OccupiedEntry<'a, K, V, H>),
}
impl<K, V, H> HashMap<K, V, H>
where
K: Ord + Clone + PackedLayout,
......@@ -337,4 +384,204 @@ where
};
self.keys.defrag(Some(max_iterations), callback)
}
/// 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) => {
Entry::Occupied(OccupiedEntry {
key,
key_index: entry.key_index,
base: self,
})
}
None => Entry::Vacant(VacantEntry { key, base: self }),
}
}
}
impl<'a, K, V, H> Entry<'a, K, V, H>
where
K: Ord + Clone + PackedLayout,
V: PackedLayout + core::fmt::Debug + core::cmp::Eq + Default,
H: Hasher,
Key: From<<H as Hasher>::Output>,
{
/// Returns a reference to this entry's key.
pub fn key(&self) -> &K {
match self {
Entry::Occupied(entry) => &entry.key,
Entry::Vacant(entry) => &entry.key,
}
}
/// Ensures a value is in the entry by inserting the default value if empty, and returns
/// a reference to the value in the entry.
pub fn or_default(self) -> &'a V {
match self {
Entry::Occupied(entry) => entry.into_mut(),
Entry::Vacant(entry) => entry.insert(V::default()),
}
}
/// Ensures a value is in the entry by inserting the default if empty, and returns
/// a mutable reference to the value in the entry.
pub fn or_insert(self, default: V) -> &'a mut V {
match self {
Entry::Occupied(entry) => entry.into_mut(),
Entry::Vacant(entry) => entry.insert(default),
}
}
/// Ensures a value is in the entry by inserting the result of the default function if empty,
/// and returns mutable references to the key and value in the entry.
pub fn or_insert_with<F>(self, default: F) -> &'a mut V
where
F: FnOnce() -> V,
{
match self {
Entry::Occupied(entry) => entry.into_mut(),
Entry::Vacant(entry) => Entry::insert(default(), entry),
}
}
/// Ensures a value is in the entry by inserting, if empty, the result of the default
/// function, which takes the key as its argument, and returns a mutable reference to
/// the value in the entry.
pub fn or_insert_with_key<F>(self, default: F) -> &'a mut V
where
F: FnOnce(&K) -> V,
{
match self {
Entry::Occupied(entry) => entry.into_mut(),
Entry::Vacant(entry) => Entry::insert(default(&entry.key), entry),
}
}
/// Provides in-place mutable access to an occupied entry before any
/// potential inserts into the map.
pub fn and_modify<F>(self, f: F) -> Self
where
F: FnOnce(&mut V),
{
match self {
Entry::Occupied(mut entry) => {
{
let v = entry.get_mut();
f(v);
}
Entry::Occupied(entry)
}
Entry::Vacant(entry) => Entry::Vacant(entry),
}
}
/// Inserts `value` into `entry`.
fn insert(value: V, entry: VacantEntry<'a, K, V, H>) -> &'a mut V {
let old_value = entry.base.insert(entry.key.clone(), value);
debug_assert!(old_value.is_none());
entry
.base
.get_mut(&entry.key)
.expect("encountered invalid vacant entry")
}
}
impl<'a, K, V, H> VacantEntry<'a, K, V, H>
where
K: Ord + Clone + PackedLayout,
V: PackedLayout,
H: Hasher,
Key: From<<H as Hasher>::Output>,
{
/// Gets a reference to the key that would be used when inserting a value through the VacantEntry.
pub fn key(&self) -> &K {
&self.key
}
/// Take ownership of the key.
pub fn into_key(self) -> K {
self.key
}
/// Sets the value of the entry with the VacantEntry's key, and returns a mutable reference to it.
pub fn insert(self, value: V) -> &'a mut V {
// At this point we know that `key` does not yet exist in the map.
let key_index = self.base.keys.put(self.key.clone());
self.base
.values
.put(self.key.clone(), Some(ValueEntry { value, key_index }));
self.base
.get_mut(&self.key)
.expect("put was just executed; qed")
}
}
impl<'a, K, V, H> OccupiedEntry<'a, K, V, H>
where
K: Ord + Clone + PackedLayout,
V: PackedLayout,
H: Hasher,
Key: From<<H as Hasher>::Output>,
{
/// Gets a reference to the key in the entry.
pub fn key(&self) -> &K {
&self.key
}
/// Take the ownership of the key and value from the map.
pub fn remove_entry(self) -> (K, V) {
let entry = self
.base
.values
.put_get(&self.key, None)
.expect("`key` must exist");
self.base
.keys
.take(self.key_index)
.expect("`key_index` must point to a valid key entry");
(self.key, entry.value)
}
/// Gets a reference to the value in the entry.
pub fn get(&self) -> &V {
&self
.base
.get(&self.key)
.expect("entry behind `OccupiedEntry` must always exist")
}
/// Gets a mutable reference to the value in the entry.
///
/// If you need a reference to the `OccupiedEntry` which may outlive the destruction of the
/// `Entry` value, see `into_mut`.
pub fn get_mut(&mut self) -> &mut V {
self.base
.get_mut(&self.key)
.expect("entry behind `OccupiedEntry` must always exist")
}
/// Sets the value of the entry, and returns the entry's old value.
pub fn insert(&mut self, new_value: V) -> V {
let occupied = self
.base
.values
.get_mut(&self.key)
.expect("entry behind `OccupiedEntry` must always exist");
core::mem::replace(&mut occupied.value, new_value)
}
/// Takes the value out of the entry, and returns it.
pub fn remove(self) -> V {
self.remove_entry().1
}
/// Converts the OccupiedEntry into a mutable reference to the value in the entry
/// with a lifetime bound to the map itself.
pub fn into_mut(self) -> &'a mut V {
self.base
.get_mut(&self.key)
.expect("entry behind `OccupiedEntry` must always exist")
}
}
......@@ -12,7 +12,15 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use super::HashMap as StorageHashMap;
use super::{
Entry,
Entry::{
Occupied,
Vacant,
},
HashMap as StorageHashMap,
};
use crate::{
env,
storage2::traits::{
......@@ -21,6 +29,32 @@ use crate::{
},
};
use ink_primitives::Key;
use num_traits::ToPrimitive;
/// Returns a prefilled `HashMap` with `[('A', 13), ['B', 23])`.
fn prefilled_hmap() -> StorageHashMap<u8, i32> {
let test_values = [(b'A', 13), (b'B', 23)];
test_values
.iter()
.copied()
.collect::<StorageHashMap<u8, i32>>()
}
/// Returns always the same `KeyPtr`.
fn key_ptr() -> KeyPtr {
let root_key = Key::from([0x42; 32]);
KeyPtr::from(root_key)
}
/// Pushes a `HashMap` instance into the contract storage.
fn push_hmap(hmap: &StorageHashMap<u8, i32>) {
SpreadLayout::push_spread(hmap, &mut key_ptr());
}
/// Pulls a `HashMap` instance from the contract storage.
fn pull_hmap() -> StorageHashMap<u8, i32> {
<StorageHashMap<u8, i32> as SpreadLayout>::pull_spread(&mut key_ptr())
}
#[test]
fn new_works() {
......@@ -290,13 +324,10 @@ fn spread_layout_push_pull_works() -> env::Result<()> {
.iter()
.copied()
.collect::<StorageHashMap<u8, i32>>();
let root_key = Key::from([0x42; 32]);
SpreadLayout::push_spread(&hmap1, &mut KeyPtr::from(root_key));
push_hmap(&hmap1);
// Load the pushed storage vector into another instance and check that
// both instances are equal:
let hmap2 = <StorageHashMap<u8, i32> as SpreadLayout>::pull_spread(
&mut KeyPtr::from(root_key),
);
let hmap2 = pull_hmap();
assert_eq!(hmap1, hmap2);
Ok(())
})
......@@ -326,3 +357,241 @@ fn spread_layout_clear_works() {
})
.unwrap()
}
#[test]
fn entry_api_insert_inexistent_works_with_empty() {
// given
let mut hmap = <StorageHashMap<u8, bool>>::new();
assert!(matches!(hmap.entry(b'A'), Vacant(_)));
assert!(hmap.get(&b'A').is_none());
// when
assert!(*hmap.entry(b'A').or_insert(true));
// then
assert_eq!(hmap.get(&b'A'), Some(&true));
assert_eq!(hmap.len(), 1);
}
#[test]
fn entry_api_insert_existent_works() {
// given
let mut hmap = prefilled_hmap();
match hmap.entry(b'A') {
Vacant(_) => panic!(),
Occupied(o) => assert_eq!(o.get(), &13),
}
// when
hmap.entry(b'A').or_insert(77);
// then
assert_eq!(hmap.get(&b'A'), Some(&13));
assert_eq!(hmap.len(), 2);
}
#[test]
fn entry_api_mutations_work_with_push_pull() -> env::Result<()> {
env::test::run_test::<env::DefaultEnvTypes, _>(|_| {
// given
let hmap1 = prefilled_hmap();
assert_eq!(hmap1.get(&b'A'), Some(&13));
push_hmap(&hmap1);
let mut hmap2 = pull_hmap();
assert_eq!(hmap2.get(&b'A'), Some(&13));
// when
let v = hmap2.entry(b'A').or_insert(42);
*v += 1;
assert_eq!(hmap2.get(&b'A'), Some(&14));
push_hmap(&hmap2);
// then
let hmap3 = pull_hmap();
assert_eq!(hmap3.get(&b'A'), Some(&14));
Ok(())