Skip to content

Commit 5c48fd7

Browse files
committed
Speed up EntityHasher slightly
1 parent 222a342 commit 5c48fd7

File tree

1 file changed

+29
-11
lines changed

1 file changed

+29
-11
lines changed

crates/bevy_utils/src/lib.rs

+29-11
Original file line numberDiff line numberDiff line change
@@ -266,29 +266,47 @@ impl BuildHasher for EntityHash {
266266
/// A very fast hash that is only designed to work on generational indices
267267
/// like `Entity`. It will panic if attempting to hash a type containing
268268
/// non-u64 fields.
269+
///
270+
/// This is heavily optimized for typical cases, where there are lots of runs
271+
/// of contiguous indices and almost no generation conflicts.
272+
///
273+
/// If you have an unusual case -- say all your indices are multiples of 256
274+
/// or most of the entities are dead generations -- then you might want also to
275+
/// try [`AHasher`] for a slower hash computation but fewer lookup conflicts.
269276
#[derive(Debug, Default)]
270277
pub struct EntityHasher {
271278
hash: u64,
272279
}
273280

274-
// This value comes from rustc-hash (also known as FxHasher) which in turn got
275-
// it from Firefox. It is something like `u64::MAX / N` for an N that gives a
276-
// value close to π and works well for distributing bits for hashing when using
277-
// with a wrapping multiplication.
278-
const FRAC_U64MAX_PI: u64 = 0x517cc1b727220a95;
279-
280281
impl Hasher for EntityHasher {
281282
fn write(&mut self, _bytes: &[u8]) {
282283
panic!("can only hash u64 using EntityHasher");
283284
}
284285

285286
#[inline]
286287
fn write_u64(&mut self, i: u64) {
287-
// Apparently hashbrown's hashmap uses the upper 7 bits for some SIMD
288-
// optimisation that uses those bits for binning. This hash function
289-
// was faster than i | (i << (64 - 7)) in the worst cases, and was
290-
// faster than PassHasher for all cases tested.
291-
self.hash = i | (i.wrapping_mul(FRAC_U64MAX_PI) << 32);
288+
// SwissTable (and thus `hashbrown`) cares about two things from the hash:
289+
// - H1: low bits (masked by `2ⁿ-1`) to pick the slot in which to store the item
290+
// - H2: high 7 bits are used to SIMD optimize hash collision probing
291+
// For more see <https://abseil.io/about/design/swisstables#metadata-layout>
292+
293+
// This hash function assumes that the entity ids are still well-distributed,
294+
// so for H1 leaves the entity id alone in the low bits so that id locality
295+
// will also give memory locality for things spawned together.
296+
// For H2, take advantage of the fact that while multiplication doesn't
297+
// spread entropy to the low bits, it's incredibly good at spreading it
298+
// upward, which is exactly where we need it the most.
299+
300+
// The high 32 bits of this are ⅟φ for Fibonacci hashing. That works
301+
// particularly well for hashing for the same reason as described in
302+
// <https://extremelearning.com.au/unreasonable-effectiveness-of-quasirandom-sequences/>
303+
// It loses no information because it has a modular inverse.
304+
// (Specifically, `0x144c_bc89_u32 * 0x9e37_79b9_u32 == 1`.)
305+
// The low 32 bits are just 1, to leave the entity id there unchanged.
306+
const UPPER_PHI: u64 = 0x9e37_79b9_0000_0001;
307+
// This bit-masking is free, as the optimizer can just not load the generation.
308+
let id = i & 0xFFFF_FFFF;
309+
self.hash = id.wrapping_mul(UPPER_PHI);
292310
}
293311

294312
#[inline]

0 commit comments

Comments
 (0)