diff --git a/Cargo.toml b/Cargo.toml index 08ef4ca..8af32a1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,13 +13,22 @@ categories = ["no-std", "embedded", "data-structures"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -hash32 = "0.2.1" +hash32 = "0.3.0" +cfg-if = "1.0.0" +serde = { version = "1.0.137", optional = true, default-features = false } [dependencies.arrayvec] -version = "0.7.0" +version = "0.7.2" default-features = false [dev-dependencies] -hash32-derive = "0.1.0" +hash32-derive = "0.1.1" rand_xorshift = "0.3.0" -rand_core = "0.6.2" +rand_core = "0.6.3" +serde = { version = "1.0.137", features = ["derive"] } +serde_json = "1.0.81" + +[features] +default = ["serde"] +hugesize = [] +serde = ["dep:serde"] diff --git a/src/lib.rs b/src/lib.rs index 1cbf05e..45b1369 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,11 +11,16 @@ //! [2](https://codecapsule.com/2013/11/17/robin-hood-hashing-backward-shift-deletion/)) //! the functionality is explained very nicely. #![cfg_attr(not(test), no_std)] -mod map; +pub mod map; use map::{Iter, IterMut, Map}; //use std::{fmt::Display}; -use core::{borrow::Borrow, fmt, iter::FromIterator, ops}; -use hash32::Hash; +use core::{borrow::Borrow, fmt, hash::Hash, iter::FromIterator, ops}; + +#[cfg(feature = "serde")] +use serde::{de, de::MapAccess, de::Visitor, Deserialize, Deserializer, Serialize, Serializer}; + +#[cfg(feature = "serde")] +use core::marker::PhantomData; /// A fixed capacity no_std hashmap. /// @@ -34,8 +39,6 @@ use hash32::Hash; /// /// ``` /// use fchashmap::FcHashMap; -/// use hash32_derive::Hash32; -/// use hash32::Hash; /// /// #[derive(Debug)] /// struct Reading { @@ -43,7 +46,7 @@ use hash32::Hash; /// humidy: f32, /// } /// -/// #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash32)] +/// #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] /// struct DeviceId([u8; 8]); /// /// impl DeviceId { @@ -73,11 +76,11 @@ use hash32::Hash; /// /// assert!(fc_hash_map.get(&dev3).is_none()); /// ``` -/// +/// /// ## Performance /// /// The following diagram shows the timing behavior on a Cortex M4f system (STM32F3) at 72 MHz. It -/// can be seen that the performance of the hashmap decreases significantly from a fill margin of +/// can be seen that the performance of the hashmap decreases significantly from a fill margin of /// about 80%. /// /// ![Image](https://raw.githubusercontent.com/Simsys/fchashmap/master/benches/cm4_performance/fchashmap.png) @@ -85,8 +88,7 @@ pub struct FcHashMap { map: Map, } -impl FcHashMap -{ +impl FcHashMap { // pub fn show(&self) { self.map.show() } /// Creates an empty HashMap. @@ -497,3 +499,65 @@ where self.iter() } } + +#[cfg(feature = "serde")] +impl Serialize for FcHashMap +where + K: Serialize + Eq + Hash, + V: Serialize, +{ + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.collect_map(self) + } +} + +#[cfg(feature = "serde")] +impl<'de, K, V, const CAP: usize> Deserialize<'de> for FcHashMap +where + K: Deserialize<'de> + Eq + Hash, + V: Deserialize<'de>, +{ + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct MapVisitor { + marker: PhantomData>, + } + + impl<'de, K, V, const CAP: usize> Visitor<'de> for MapVisitor + where + K: Deserialize<'de> + Eq + Hash, + V: Deserialize<'de>, + { + type Value = FcHashMap; + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("a map") + } + + fn visit_map(self, mut map: A) -> Result + where + A: MapAccess<'de>, + { + let mut values = FcHashMap::::new(); + + while let Some((key, value)) = map.next_entry()? { + values + .insert(key, value) + .map_err(|_| de::Error::custom("out of space"))?; + } + + Ok(values) + } + } + + let visitor = MapVisitor { + marker: PhantomData, + }; + deserializer.deserialize_map(visitor) + } +} diff --git a/src/map.rs b/src/map.rs index 93399a5..a1f514e 100644 --- a/src/map.rs +++ b/src/map.rs @@ -1,53 +1,117 @@ #![allow(dead_code)] use arrayvec::ArrayVec; -use core::{borrow::Borrow, mem, slice}; -use hash32::{BuildHasher, BuildHasherDefault, FnvHasher, Hash, Hasher}; +use core::{ + borrow::Borrow, + hash::{BuildHasher, Hash}, + mem, slice, +}; +use hash32::{BuildHasherDefault, FnvHasher, Hasher}; + +cfg_if::cfg_if! { + if #[cfg(feature = "hugesize")] { + use core::convert::TryFrom; + + #[derive(Clone, Copy, PartialEq)] + struct HashValue(u32); + + // There are up to 0x7fffffff (2^31 - 1) elements allowed. The first bit of u32 is used to mark + // a empty element + const HASH_VALUE_IS_EMPTY: HashValue = HashValue(0x80000000); + + impl HashValue { + // Drop the negative sign + fn new(hash: u32) -> Self { + HashValue((hash & 0x7fffffff) as u32) + } -#[derive(Clone, Copy, PartialEq)] -struct HashValue(u16); + // Calulate expected index from hash value + fn desired_h_idx(&self, mask: usize) -> usize { + usize::try_from(self.0).unwrap() & mask + } -// There are up to 0x7fff (32767) elements allowed. The first bit of u16 is used to mark -// a empty element -const HASH_VALUE_IS_EMPTY: HashValue = HashValue(0x8000); + // Calculate distance from expected index from current index + fn h_idx_distance(&self, mask: usize, current_h_idx: usize) -> usize { + current_h_idx.wrapping_sub(self.desired_h_idx(mask) as usize) & mask + } + } -impl HashValue { - // Create 15 bit hash value from u32 hash - fn new(hash: u32) -> Self { - HashValue((hash & 0x7fff) as u16) - } + // A Combination of hash value and index into the bucket list + #[derive(Clone, Copy)] + struct HashIndex { + hash: HashValue, + b_idx: u32, + } - // Calulate expected index from hash value - fn desired_h_idx(&self, mask: usize) -> usize { - usize::from(self.0) & mask - } + impl HashIndex { + // Create a nuew hash index from given parameters + fn new(hash: HashValue, b_idx: usize) -> Self { + Self { + hash, + b_idx: b_idx as u32, + } + } - // Calculate distance from expected index from current index - fn h_idx_distance(&self, mask: usize, current_h_idx: usize) -> usize { - current_h_idx.wrapping_sub(self.desired_h_idx(mask) as usize) & mask - } -} + // Clear actual hash index an mark it as empty + fn clear(&mut self) { + self.hash = HASH_VALUE_IS_EMPTY; + } -// A Combination of hash value and index into the bucket list -#[derive(Clone, Copy)] -struct HashIndex { - hash: HashValue, - b_idx: u16, -} + // Check if hash index is empty + fn is_empty(&self) -> bool { + self.hash == HASH_VALUE_IS_EMPTY + } + } + } else { + #[derive(Clone, Copy, PartialEq)] + struct HashValue(u16); + + // There are up to 0x7fff (32767) elements allowed. The first bit of u16 is used to mark + // a empty element + const HASH_VALUE_IS_EMPTY: HashValue = HashValue(0x8000); + + impl HashValue { + // Create 15 bit hash value from u32 hash + fn new(hash: u32) -> Self { + HashValue((hash & 0x7fff) as u16) + } -impl HashIndex { - // Create a nuew hash index from given parameters - fn new(hash: HashValue, b_idx: usize) -> Self { - Self { hash, b_idx: b_idx as u16 } - } + // Calulate expected index from hash value + fn desired_h_idx(&self, mask: usize) -> usize { + usize::from(self.0) & mask + } - // Clear actual hash index an mark it as empty - fn clear(&mut self) { - self.hash = HASH_VALUE_IS_EMPTY; - } + // Calculate distance from expected index from current index + fn h_idx_distance(&self, mask: usize, current_h_idx: usize) -> usize { + current_h_idx.wrapping_sub(self.desired_h_idx(mask) as usize) & mask + } + } + + // A Combination of hash value and index into the bucket list + #[derive(Clone, Copy)] + struct HashIndex { + hash: HashValue, + b_idx: u16, + } - // Check if hash index is empty - fn is_empty(&self) -> bool { - self.hash == HASH_VALUE_IS_EMPTY + impl HashIndex { + // Create a nuew hash index from given parameters + fn new(hash: HashValue, b_idx: usize) -> Self { + Self { + hash, + b_idx: b_idx as u16, + } + } + + // Clear actual hash index an mark it as empty + fn clear(&mut self) { + self.hash = HASH_VALUE_IS_EMPTY; + } + + // Check if hash index is empty + fn is_empty(&self) -> bool { + self.hash == HASH_VALUE_IS_EMPTY + } + } } } @@ -64,8 +128,7 @@ pub struct Map { build_hasher: BuildHasherDefault, } -impl Map -{ +impl Map { // Create a new map pub fn new() -> Self { debug_assert!((Self::capacity() as u32) < u32::MAX); @@ -98,7 +161,7 @@ impl Map { let mut h = self.build_hasher.build_hasher(); key.hash(&mut h); - HashValue::new(h.finish()) + HashValue::new(h.finish32()) } // Inserts a key-value pair into the map. @@ -142,7 +205,7 @@ impl Map if next_hash_index.is_empty() { // We found the right place: store and return *next_hash_index = hash_index; - unsafe { self.buckets.push_unchecked( Bucket { key, value, hash }) } + unsafe { self.buckets.push_unchecked(Bucket { key, value, hash }) } return Ok(None); } else { // Replace HashIndexs and continue shifting and searching for a vacancy @@ -211,7 +274,7 @@ impl Map // The HashIndex at location h_idx and the bucket at location b_idx are deleted. self.hash_table[found_h_idx].clear(); let deleted_bucket = self.buckets.swap_pop(found_b_idx).unwrap(); // ArrayVec; - //let deleted_bucket = unsafe { self.buckets.swap_remove_unchecked(found_b_idx) }; // heapless::Vec; + //let deleted_bucket = unsafe { self.buckets.swap_remove_unchecked(found_b_idx) }; // heapless::Vec; // Correct index that points to the entry that had to swap places. // This has only to be done, if wass not the last element in self.buckets diff --git a/tests/monte_carlo.rs b/tests/monte_carlo.rs index e65ab69..ab68510 100644 --- a/tests/monte_carlo.rs +++ b/tests/monte_carlo.rs @@ -1,18 +1,16 @@ use fchashmap::FcHashMap; -use rand_xorshift::XorShiftRng; use rand_core::{RngCore, SeedableRng}; +use rand_xorshift::XorShiftRng; use std::collections::HashMap; - struct MonteCarlo { - fc_hashmap: FcHashMap::, - std_hashmap: HashMap::, + fc_hashmap: FcHashMap, + std_hashmap: HashMap, } const MAP_SIZE: usize = 16384; const SEED: u64 = 1234567890987654321; - impl MonteCarlo { fn new() -> Self { Self { @@ -30,14 +28,28 @@ impl MonteCarlo { match r_fc { Ok(r_v) => { if r_v != r_std { - println!("Error 1, len {}, key {}, value {}, r_v{:?}, r_std {:?}", self.fc_hashmap.len(), key, value, r_v, r_std); + println!( + "Error 1, len {}, key {}, value {}, r_v{:?}, r_std {:?}", + self.fc_hashmap.len(), + key, + value, + r_v, + r_std + ); }; assert_eq!(r_v, r_std); - }, + } Err(e) => { - println!("Error 2, len {}, key {}, value {}, e{:?}, r_std {:?}", self.fc_hashmap.len(), key, value, e, r_std); + println!( + "Error 2, len {}, key {}, value {}, e{:?}, r_std {:?}", + self.fc_hashmap.len(), + key, + value, + e, + r_std + ); assert!(false); - }, + } } } @@ -48,7 +60,13 @@ impl MonteCarlo { let r_std = self.std_hashmap.remove(&key); if r_fc != r_std { - println!("Error 3, len {}, key {}, r_fc{:?}, r_std {:?}", self.fc_hashmap.len(), key, r_fc, r_std); + println!( + "Error 3, len {}, key {}, r_fc{:?}, r_std {:?}", + self.fc_hashmap.len(), + key, + r_fc, + r_std + ); }; assert_eq!(r_fc, r_std); } @@ -60,7 +78,13 @@ impl MonteCarlo { let r_std = self.std_hashmap.get(&key); if r_fc != r_std { - println!("Error 4, len {}, key {}, r_fc{:?}, r_std {:?}", self.fc_hashmap.len(), key, r_fc, r_std); + println!( + "Error 4, len {}, key {}, r_fc{:?}, r_std {:?}", + self.fc_hashmap.len(), + key, + r_fc, + r_std + ); }; assert_eq!(r_fc, r_std); } @@ -103,7 +127,7 @@ impl MonteCarlo { // First, we fill the map at 50% let mut rng = XorShiftRng::seed_from_u64(SEED); loop { - if self.fc_hashmap.len() >= MAP_SIZE/2 { + if self.fc_hashmap.len() >= MAP_SIZE / 2 { break; } self.insert(&mut rng); @@ -117,7 +141,13 @@ impl MonteCarlo { let r_std = self.std_hashmap.get(&key); if r_fc != r_std { - println!("Error 6, len {}, key {}, r_fc{:?}, r_std {:?}", self.fc_hashmap.len(), key, r_fc, r_std); + println!( + "Error 6, len {}, key {}, r_fc{:?}, r_std {:?}", + self.fc_hashmap.len(), + key, + r_fc, + r_std + ); }; assert_eq!(r_fc, r_std); } @@ -125,7 +155,7 @@ impl MonteCarlo { } #[test] -fn monte_carlo () { +fn monte_carlo() { let mut m = MonteCarlo::new(); m.test_1(); m.test_2(); diff --git a/tests/serde.rs b/tests/serde.rs new file mode 100644 index 0000000..6132571 --- /dev/null +++ b/tests/serde.rs @@ -0,0 +1,25 @@ +use fchashmap::FcHashMap; +use serde::{Deserialize, Serialize}; + +#[test] +fn test_serialize() { + let mut m = FcHashMap::<&'static str, &'static str, 1024>::new(); + m.insert("hello", "world").unwrap(); + m.insert("foo", "bar").unwrap(); + + let serialized = serde_json::to_string(&m).unwrap(); + let deserialized: serde_json::Value = serde_json::from_str(&serialized).unwrap(); + + assert_eq!(deserialized["hello"], "world"); + assert_eq!(deserialized["foo"], "bar"); +} + +#[test] +fn test_deserialize() { + let json = r#"{"foo":"bar","hello":"world"}"#; + let deserialized: FcHashMap = serde_json::from_str(&json).unwrap(); + + assert_eq!(deserialized.len(), 2); + assert_eq!(deserialized["hello"], "world"); + assert_eq!(deserialized["foo"], "bar"); +}