Skip to content

Commit

Permalink
Hash stability guarantees (bevyengine#11690)
Browse files Browse the repository at this point in the history
# Objective

We currently over/underpromise hash stability:
- `HashMap`/`HashSet` use `BuildHasherDefault<AHasher>` instead of
`RandomState`. As a result, the hash is stable within the same run.
- [aHash isn't stable between devices (and
versions)](https://github.com/tkaitchuck/ahash?tab=readme-ov-file#goals-and-non-goals),
yet it's used for `StableHashMap`/`StableHashSet`
- the specialized hashmaps are stable

Interestingly, `StableHashMap`/`StableHashSet` aren't used by Bevy
itself (anymore).

## Solution
Add/fix documentation

## Alternatives
For `StableHashMap`/`StableHashSet`:
- remove them
- revive bevyengine#7107

---

## Changelog
- added iteration stability guarantees for different hashmaps
  • Loading branch information
SpecificProtagonist authored and tjamaan committed Feb 6, 2024
1 parent 5a078c4 commit a0b994d
Show file tree
Hide file tree
Showing 2 changed files with 46 additions and 12 deletions.
8 changes: 3 additions & 5 deletions crates/bevy_reflect/src/utility.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! Helpers for working with Bevy reflection.

use crate::TypeInfo;
use bevy_utils::{FixedState, StableHashMap};
use bevy_utils::{FixedState, NoOpTypeIdHash, TypeIdMap};
use std::{
any::{Any, TypeId},
hash::BuildHasher,
Expand Down Expand Up @@ -195,7 +195,7 @@ impl<T: TypedProperty> NonGenericTypeCell<T> {
/// ```
/// [`impl_type_path`]: crate::impl_type_path
/// [`TypePath`]: crate::TypePath
pub struct GenericTypeCell<T: TypedProperty>(RwLock<StableHashMap<TypeId, &'static T::Stored>>);
pub struct GenericTypeCell<T: TypedProperty>(RwLock<TypeIdMap<&'static T::Stored>>);

/// See [`GenericTypeCell`].
pub type GenericTypeInfoCell = GenericTypeCell<TypeInfo>;
Expand All @@ -205,9 +205,7 @@ pub type GenericTypePathCell = GenericTypeCell<TypePathComponent>;
impl<T: TypedProperty> GenericTypeCell<T> {
/// Initialize a [`GenericTypeCell`] for generic types.
pub const fn new() -> Self {
// Use `bevy_utils::StableHashMap` over `bevy_utils::HashMap`
// because `BuildHasherDefault` is unfortunately not const.
Self(RwLock::new(StableHashMap::with_hasher(FixedState)))
Self(RwLock::new(TypeIdMap::with_hasher(NoOpTypeIdHash)))
}

/// Returns a reference to the [`TypedProperty`] stored in the cell.
Expand Down
50 changes: 43 additions & 7 deletions crates/bevy_utils/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,13 +87,17 @@ impl BuildHasher for FixedState {
/// speed keyed hashing algorithm intended for use in in-memory hashmaps.
///
/// aHash is designed for performance and is NOT cryptographically secure.
///
/// Within the same execution of the program iteration order of different
/// `HashMap`s only depends on the order of insertions and deletions,
/// but it will not be stable between multiple executions of the program.
pub type HashMap<K, V> = hashbrown::HashMap<K, V, BuildHasherDefault<AHasher>>;

/// A stable hash map implementing aHash, a high speed keyed hashing algorithm
/// intended for use in in-memory hashmaps.
///
/// Unlike [`HashMap`] this has an iteration order that only depends on the order
/// of insertions and deletions and not a random source.
/// Unlike [`HashMap`] the iteration order stability extends between executions
/// using the same Bevy version on the same device.
///
/// aHash is designed for performance and is NOT cryptographically secure.
pub type StableHashMap<K, V> = hashbrown::HashMap<K, V, FixedState>;
Expand All @@ -102,13 +106,17 @@ pub type StableHashMap<K, V> = hashbrown::HashMap<K, V, FixedState>;
/// speed keyed hashing algorithm intended for use in in-memory hashmaps.
///
/// aHash is designed for performance and is NOT cryptographically secure.
///
/// Within the same execution of the program iteration order of different
/// `HashSet`s only depends on the order of insertions and deletions,
/// but it will not be stable between multiple executions of the program.
pub type HashSet<K> = hashbrown::HashSet<K, BuildHasherDefault<AHasher>>;

/// A stable hash set implementing aHash, a high speed keyed hashing algorithm
/// intended for use in in-memory hashmaps.
///
/// Unlike [`HashSet`] this has an iteration order that only depends on the order
/// of insertions and deletions and not a random source.
/// Unlike [`HashMap`] the iteration order stability extends between executions
/// using the same Bevy version on the same device.
///
/// aHash is designed for performance and is NOT cryptographically secure.
pub type StableHashSet<K> = hashbrown::HashSet<K, FixedState>;
Expand Down Expand Up @@ -224,6 +232,7 @@ impl Hasher for PassHasher {
}

/// A [`HashMap`] pre-configured to use [`Hashed`] keys and [`PassHash`] passthrough hashing.
/// Iteration order only depends on the order of insertions and deletions.
pub type PreHashMap<K, V> = hashbrown::HashMap<Hashed<K>, V, PassHash>;

/// Extension methods intended to add functionality to [`PreHashMap`].
Expand Down Expand Up @@ -322,17 +331,30 @@ impl Hasher for EntityHasher {
}

/// A [`HashMap`] pre-configured to use [`EntityHash`] hashing.
/// Iteration order only depends on the order of insertions and deletions.
pub type EntityHashMap<K, V> = hashbrown::HashMap<K, V, EntityHash>;

/// A [`HashSet`] pre-configured to use [`EntityHash`] hashing.
/// Iteration order only depends on the order of insertions and deletions.
pub type EntityHashSet<T> = hashbrown::HashSet<T, EntityHash>;

/// A specialized hashmap type with Key of [`TypeId`]
pub type TypeIdMap<V> =
hashbrown::HashMap<TypeId, V, std::hash::BuildHasherDefault<NoOpTypeIdHasher>>;
/// Iteration order only depends on the order of insertions and deletions.
pub type TypeIdMap<V> = hashbrown::HashMap<TypeId, V, NoOpTypeIdHash>;

#[doc(hidden)]
/// [`BuildHasher`] for [`TypeId`]s.
#[derive(Default)]
pub struct NoOpTypeIdHash;

impl BuildHasher for NoOpTypeIdHash {
type Hasher = NoOpTypeIdHasher;

fn build_hasher(&self) -> Self::Hasher {
NoOpTypeIdHasher(0)
}
}

#[doc(hidden)]
pub struct NoOpTypeIdHasher(u64);

// TypeId already contains a high-quality hash, so skip re-hashing that hash.
Expand Down Expand Up @@ -469,4 +491,18 @@ mod tests {

std::hash::Hash::hash(&TypeId::of::<()>(), &mut Hasher);
}

#[test]
fn stable_hash_within_same_program_execution() {
let mut map_1 = HashMap::new();
let mut map_2 = HashMap::new();
for i in 1..10 {
map_1.insert(i, i);
map_2.insert(i, i);
}
assert_eq!(
map_1.iter().collect::<Vec<_>>(),
map_2.iter().collect::<Vec<_>>()
);
}
}

0 comments on commit a0b994d

Please sign in to comment.