Skip to content

Commit 8e4ea2d

Browse files
Better comments (#66)
* Better comments --------- Co-authored-by: Eddie <eddie see email elsewhere> Co-authored-by: Scottbruceheart <105394870+Scottbruceheart@users.noreply.github.com>
1 parent 24b5b99 commit 8e4ea2d

File tree

2 files changed

+376
-283
lines changed

2 files changed

+376
-283
lines changed

inc/zoo/map/RobinHood.h

Lines changed: 82 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,43 @@
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+
2057
namespace zoo {
2158
namespace rh {
2259

@@ -31,38 +68,16 @@ struct RelocationStackExhausted: RobinHoodException {
3168
using RobinHoodException::RobinHoodException;
3269
};
3370

71+
/// \brief The canonical backend (implementation)
3472
template<int PSL_Bits, int HashBits, typename U = std::uint64_t>
3573
struct 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.
215263
template<typename K, typename MV>
216264
struct 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.
246303
template<
247304
typename K,
248305
typename MV,

0 commit comments

Comments
 (0)