deed_diagnostics/hashing.rs
1//! A hash for keys that are already numbers.
2//!
3//! Everything this compiler keys a map by is a small integer or two: a
4//! [`Span`](crate::Span) is a pair of byte offsets, a definition is an index
5//! into a table. The standard library hashes with SipHash, which is chosen to
6//! survive an attacker choosing the keys. Nothing here has that problem: the
7//! keys come out of a file the compiler was handed, and a file that could pick
8//! its own spans could do far worse than make a hash map slow.
9//!
10//! Measured before it was written, because that is the rule. Reading a name
11//! was the most expensive small thing in the language and it is two lookups,
12//! one from the span where the name is written to what it refers to and one
13//! from that to the value. `crates/deed-driver/examples/interpreting.rs` says
14//! what a name costs and what it costs now.
15//!
16//! The mixing is one multiply and one shift, the same shape as the hash used
17//! by every compiler that has had this problem, and it is written out here
18//! rather than depended on because it is nine lines.
19
20use std::hash::{BuildHasherDefault, Hasher};
21
22/// A [`BuildHasher`](std::hash::BuildHasher) for maps keyed by numbers.
23pub type ByNumber = BuildHasherDefault<NumberHasher>;
24
25/// Mixes whatever it is given into one word.
26///
27/// The constant is the 64 bit odd number used for this everywhere: multiplying
28/// by an odd number is reversible, so no two inputs collide before the shift,
29/// and the shift moves the high bits, which the multiply mixed best, down to
30/// where a hash map reads them.
31#[derive(Default)]
32pub struct NumberHasher(u64);
33
34const MIX: u64 = 0x517c_c1b7_2722_0a95;
35
36impl NumberHasher {
37 fn add(&mut self, word: u64) {
38 self.0 = (self.0 ^ word).wrapping_mul(MIX);
39 }
40}
41
42impl Hasher for NumberHasher {
43 fn finish(&self) -> u64 {
44 // The multiply mixes upward, so the answer lives in the high bits.
45 self.0 ^ (self.0 >> 32)
46 }
47
48 /// Bytes, for a key that is not a number after all.
49 ///
50 /// Kept correct rather than fast: `Hasher` is one trait and a map keyed by
51 /// something else would otherwise get a hash that ignores most of it. Slow
52 /// and right beats fast and wrong, and nothing this is used for takes this
53 /// path.
54 fn write(&mut self, bytes: &[u8]) {
55 for chunk in bytes.chunks(8) {
56 let mut word = [0u8; 8];
57 word[..chunk.len()].copy_from_slice(chunk);
58 self.add(u64::from_le_bytes(word));
59 }
60 }
61
62 fn write_u8(&mut self, value: u8) {
63 self.add(u64::from(value));
64 }
65
66 fn write_u16(&mut self, value: u16) {
67 self.add(u64::from(value));
68 }
69
70 fn write_u32(&mut self, value: u32) {
71 self.add(u64::from(value));
72 }
73
74 fn write_u64(&mut self, value: u64) {
75 self.add(value);
76 }
77
78 fn write_usize(&mut self, value: usize) {
79 self.add(value as u64);
80 }
81}
82
83#[cfg(test)]
84mod tests {
85 use super::*;
86 use std::collections::HashMap;
87 use std::hash::Hash;
88
89 fn hash_of<T: Hash>(value: &T) -> u64 {
90 let mut hasher = NumberHasher::default();
91 value.hash(&mut hasher);
92 hasher.finish()
93 }
94
95 /// The thing a hash has to do. Two keys that differ in either half have to
96 /// be able to tell each other apart, and a hash that ignored the second
97 /// word would pass every other test in this file.
98 #[test]
99 fn two_keys_that_differ_hash_differently() {
100 assert_ne!(hash_of(&(1u32, 2u32)), hash_of(&(2u32, 1u32)));
101 assert_ne!(hash_of(&(0u32, 1u32)), hash_of(&(0u32, 2u32)));
102 assert_ne!(hash_of(&(1u32, 0u32)), hash_of(&(2u32, 0u32)));
103 }
104
105 /// And the thing a hash map needs on top of that: the same key twice is
106 /// the same entry, whichever map it is in.
107 #[test]
108 fn a_map_keyed_by_it_still_behaves_like_a_map() {
109 let mut map: HashMap<(u32, u32), &str, ByNumber> = HashMap::default();
110 for start in 0..64u32 {
111 map.insert((start, start + 3), "here");
112 }
113
114 assert_eq!(map.len(), 64);
115 assert_eq!(map.get(&(7, 10)), Some(&"here"));
116 assert_eq!(map.get(&(7, 11)), None);
117 }
118
119 /// Bytes go through the slow path, which has to be a hash rather than a
120 /// constant, or a map keyed by a string would put everything in one bucket.
121 #[test]
122 fn a_key_that_is_not_a_number_is_still_hashed() {
123 assert_ne!(hash_of(&"one"), hash_of(&"two"));
124 assert_ne!(hash_of(&"a longer key than eight bytes"), hash_of(&"one"));
125 }
126}