1717 #include < assert>
1818#endif
1919
20+ /* ! \file RobinHood.h
21+ \brief User entry point to the implementation of hash tables using the "Robin
22+ Hood" invariant.
23+
24+ The "Robin Hood" monicker means that each key has a preferred or "home" slot
25+ in the hash table. If, upon insertion, the key can not be inserted into its
26+ home slot, then the insertion would look to insert it as close as possible to
27+ the home slot.
28+
29+ In this code base, the acronym PSL is used frequently, it means "Probe Sequence
30+ Length", this is the distance from the preferred or "home" slot and the current
31+ search position.
32+ For a practical reason, a key inserted into its home has a PSL of 1, in this
33+ way, the metadata indicates with a PSL of 0 that no key is in the slot,
34+ or that the slot is free.
35+
36+ The invariant is that a key won't be inserted further away from its home than
37+ the key in the current slot. That is, a key is "richer" than another if it is
38+ closer to its "home", the insertion mechanism would "evict" a key that would be
39+ richer than the key being inserted. In this regard, the "Robin Hood" metaphor
40+ is realized: the insertion "steals" from the rich to give it to the poor.
41+
42+ \note All of this codebase makes the unchecked assumption that the byte ordering
43+ is LITTLE ENDIAN
44+
45+ \todo complement with the other theoretical and practical comments relevant,
46+ including:
47+ 1. How the table is not stable with regards to insetions and deletions,
48+ 2. How an insertion can cascade into very long chains of evictions/reinsertions
49+ 3. The theoretical guarantee that the longest PSL is in the order of Log(N)
50+ 4. How it seems that in practice the theoretical guarantee is not achieved.
51+ ...
52+
53+ \todo determine a moment to endure the version control pain of making the
54+ indentation consistent.
55+ */
56+
2057namespace zoo {
2158namespace rh {
2259
@@ -31,38 +68,16 @@ struct RelocationStackExhausted: RobinHoodException {
3168 using RobinHoodException::RobinHoodException;
3269};
3370
71+ // / \brief The canonical backend (implementation)
3472template <int PSL_Bits, int HashBits, typename U = std::uint64_t >
3573struct RH_Backend {
3674 using Metadata = impl::Metadata<PSL_Bits, HashBits, U>;
3775
3876 constexpr static inline auto Width = Metadata::NBits;
3977 Metadata *md_;
4078
41- /* ! \brief SWAR check for a potential match
42- The invariant in Robin Hood is that the element being looked for, the "needle", is "richer"
43- than the elements already present, the "haystack".
44- "Richer" means that the PSL is smaller.
45- A PSL of 0 can only happen in the haystack, to indicate the slot is empty, this is "richest".
46- The first time the needle has a PSL greater than the haystacks' means the matching will fail,
47- because the hypothetical prior insertion would have "stolen" that slot.
48- If there is an equal, it would start a sequence of potential matches. To determine an actual match:
49- 1. A cheap SWAR check of hoisted hashes
50- 2. If there are still potential matches (now also the hoisted hashes), fall back to non-SWAR,
51- or iterative and expensive "deep equality" test for each potential match, outside of this function
52-
53- The above makes it very important to detect the first case in which the PSL is greater equal to the needle.
54- We call this the "deadline".
55- Because we assume the LITTLE ENDIAN byte ordering, the first element would be the least significant
56- non-false Boolean SWAR.
57-
58- Note about performance:
59- Every "early exit" faces a big justification hurdle, the proportion of cases
60- they intercept to be large enough that the branch prediction penalty of the entropy introduced is
61- overcompensated.
62- */
63-
64- // / Boolean SWAR true in the first element/lane of the needle strictly poorer than its corresponding
65- // / haystack
79+ // / Boolean SWAR true in the first element/lane of the needle strictly
80+ // / poorer than its corresponding haystack
6681 constexpr static auto
6782 firstInvariantBreakage (Metadata needle, Metadata haystack) {
6883 auto nPSL = needle.PSLs ();
@@ -97,6 +112,36 @@ struct RH_Backend {
97112 return std::tuple{Metadata{nPSL.PSLs () | needlePSLsToSaturate}, bool (saturation)}; // saturated at any point, last swar to check.
98113 }
99114
115+
116+ /* ! \brief SWAR check for a potential match
117+
118+ The invariant in Robin Hood is that the element being looked for, the
119+ "needle", is at least as "rich" as the elements already present (the
120+ "haystack").
121+ "Richer" means that the PSL is smaller.
122+ A PSL of 0 can only happen in the haystack, to indicate the slot is empty,
123+ this is "richest".
124+ The first time the needle has a PSL greater than the haystacks' means the
125+ matching will fail, because the hypothetical prior insertion would have
126+ "stolen" that slot.
127+ If the PSLs are equal, it starts a sequence of potential matches. To
128+ determine if there is an actual match, perform:
129+ 1. A cheap SWAR check of hoisted hashes
130+ 2. If there are still potential matches (now also the hoisted hashes), fall
131+ back to non-SWAR, or iterative and expensive "deep equality" test for each
132+ potential match, outside of this function.
133+
134+ The above makes it very important to detect the first case in which the PSL
135+ is greater equal to the needle. We call this the "deadline".
136+ We assume the LITTLE ENDIAN byte ordering: the first element will
137+ be the least significant non-false Boolean SWAR.
138+
139+ Note about performance:
140+ Every "early exit" faces a big justification hurdle, the proportion of cases
141+ they intercept must be large enough that the branch prediction penalty of the
142+ entropy introduced (by the early exit) is overcompensated.
143+ */
144+
100145 constexpr static impl::MatchResult<PSL_Bits, HashBits, U>
101146 potentialMatches (
102147 Metadata needle, Metadata haystack
@@ -212,6 +257,9 @@ RH_Backend<PSL_Bits, HashBits, U>::findMisaligned_assumesSkarupkeTail(
212257 }
213258 }
214259
260+ // / \brief The slots in the table may have a key-value pair or not, this
261+ // / optionality is not suitably captured by any standard library component,
262+ // / hence we need to implement our own.
215263template <typename K, typename MV>
216264struct KeyValuePairWrapper {
217265 using type = std::pair<K, MV>;
@@ -243,6 +291,15 @@ struct KeyValuePairWrapper {
243291 const auto &value () const noexcept { return const_cast <KeyValuePairWrapper *>(this )->value (); }
244292};
245293
294+ // / \brief Frontend with the "Skarupke Tail"
295+ // /
296+ // / Normally we need to explicitly check for whether key searches have reached
297+ // / the end of the table. Malte Skarupke devised a tail of table entries to
298+ // / make this explicit check unnecessary: Regardless of the end of the table,
299+ // / a search must terminate in failure if the maximum PSL is reached, then,
300+ // / by just adding an extra maximum PSL entries to the table, while keeping the
301+ // / slot indexing function the same, searches at the end of the table will never
302+ // / attempt to go past the real end, but return not-found within the tail.
246303template <
247304 typename K,
248305 typename MV,
0 commit comments