Structures

The probe nobody waits for

Robin Hood hashing makes an inserting key steal a slot from a key that has probed less far. The mean number of probes afterwards is 4.817, and before it was 4.817 — identical, and it cannot be otherwise, because the total displacement is fixed by the hash. What changes is the worst case, from 114 slots from home to 19, and a table reported by its average lookup cost shows no difference at all.

A hash table with open addressing puts a key at the slot its hash names, and if that slot is taken it walks forward until it finds an empty one. How far it walked is the key’s displacement, and a lookup for that key costs one more probe than its displacement.

The trouble with plain linear probing is that displacement is unfairly distributed. An early arrival keeps a slot it barely wanted; a later arrival walks past it and past twenty others. At a load factor of 0.95 in a table of 512 slots, the worst-displaced key sits 114 slots from home, and the key immediately after it in memory may sit at zero.

Robin Hood hashing fixes that with one rule: when the key being inserted has walked further than the key occupying the slot, they swap, and the richer key carries on looking.

Probe displacement at load 0.85, 435 keys in 512 slotsHow far each key sits from the slot it hashed to, under plain linear probing and under Robin Hood. The means are identical — 2.384 both times, and they cannot differ, because the total displacement is decided by the hash and Robin Hood only decides who bears it. The worst case falls from 48 probes to 12, and the variance from 36.94 to 7.16. A table reported by its average lookup cost would show no difference at all between these two.04812162024283236404448slots from homepale: linear probing · dark: Robin Hoodmean 2.384identical for bothworst 48 → 12var 37 → 7435 keys, 512 slots, seed 20260811displacements, counted exactly
Fig. 1 Displacement distributions for the same 435 keys in the same 512 slots, inserted the same way, under plain linear probing and under Robin Hood. The means are identical to fifteen decimal places. The worst case falls from 48 slots to 12 and the variance from 36.94 to 7.16.

The mean cannot move, and this is why

The most useful thing about Robin Hood hashing is a fact about it that requires no measurement at all.

Fix a set of keys and a hash function. The set of slots the keys hash to is fixed. The set of slots they occupy is fixed too — a table with nn keys in mm slots has the same nn occupied slots whichever insertion policy is used, provided the policy is a permutation of who sits where. So (occupied slothome slot)\sum(\text{occupied slot} - \text{home slot}) is the same number under any such policy.

Robin Hood is exactly such a policy: it never changes which slots are occupied, only which key is in which. So the total displacement is invariant and the mean is invariant.

That is a theorem and this site’s habit is to check theorems rather than repeat them. assertRobinHoodMovesVarianceNotMean requires the two means to agree to within 10910^{-9} and fails the build if they do not. It has a second clause requiring the variance to at least halve, because a policy that left the variance alone would satisfy the first clause by doing nothing.

load mean probes linear worst Robin Hood worst linear variance Robin Hood variance
0.50 1.699 19 7 3.42 1.13
0.75 2.495 32 10 14.71 3.28
0.85 3.384 49 13 36.94 7.16
0.95 5.817 115 20 174.98 16.94

At every load the mean is shared between the two columns because it must be. At load 0.95 the worst case improves by a factor of 5.75 and the variance by a factor of 10.3.

Why an average was the wrong summary all along

The probe formula nobody checks is about Knuth’s expression for the expected number of probes in a linear-probing table:

12(1+1(1α)2)\frac{1}{2}\left(1 + \frac{1}{(1-\alpha)^2}\right)

at load factor α\alpha. That essay measured it, found it accurate for random keys, and found it wildly wrong for keys whose stride shares a factor with the capacity. What it did not say — because the site had no way to say it — is that the formula is an expectation, and the expectation is not what a hash table’s user experiences.

A program doing a million lookups in a table at load 0.95 does not experience 5.817 probes. It experiences 5.817 probes on average, with individual lookups ranging from 1 to 115, and the ones at 115 are the ones that show up in a latency percentile. If the table is behind a request, the 99th percentile of the response time is a function of the tail of that distribution and not of its mean.

Robin Hood improves exactly the quantity that matters and leaves exactly the quantity that gets reported unchanged. That is not a coincidence; it is the design. And it means a benchmark reporting mean lookup time would measure the two as identical, which is very close to what happened historically — the technique was published by Celis in 1986 and was largely ignored for twenty-five years, until latency percentiles became the thing systems people measured.

Probe displacement at load 0.95, 486 keys in 512 slotsHow far each key sits from the slot it hashed to, under plain linear probing and under Robin Hood. The means are identical — 4.817 both times, and they cannot differ, because the total displacement is decided by the hash and Robin Hood only decides who bears it. The worst case falls from 114 probes to 19, and the variance from 174.98 to 16.94. A table reported by its average lookup cost would show no difference at all between these two.0102030405060708090100110slots from homepale: linear probing · dark: Robin Hoodmean 4.817identical for bothworst 114 → 19var 175 → 17486 keys, 512 slots, seed 20260811displacements, counted exactly
Fig. 2 The same comparison at load 0.95, where the effect is largest. Plain linear probing’s distribution has a long thin tail running out to 114 slots from home; Robin Hood’s stops at 19. Both have a mean displacement of 4.817, so a lookup costs 5.817 probes on average either way. The picture is the argument, and no number that summarises either distribution to a single value can show it.

The cost, and where it goes

The rule is not free. Each swap during insertion is a write, and at load 0.95 in a 512-slot table there are 1,224 swaps for 486 keys — about two and a half per insertion.

That is the trade and it is the usual shape: more work when writing, less variance when reading. For a table built once and queried many times it is clearly worth it. For a table with a high update rate and no latency requirement, it is not obviously anything.

There is a second and less obvious cost that this site cannot measure and should name. Robin Hood’s insertion carries a key in hand and swaps it repeatedly, which means the write pattern is a chain of dependent stores rather than one store — and dependent stores do not pipeline. The write count is a poor proxy for the write cost here, in the same way the comparison count is a poor proxy for a merge’s cost in the branch the machine guesses.

There is also a benefit the counters do not see. Because every key’s displacement is bounded by a small number, a lookup can stop early: if the current slot’s displacement is less than how far the search has walked, the key being sought cannot be present, because it would have stolen this slot. Plain linear probing has no such test and must walk to the next empty slot. On a lookup for an absent key that is a large saving and it is invisible to a probe count over present keys.

Where the variance comes from

Understanding why the distributions differ so much requires one idea, and it is the same one that makes linear probing fast in the first place.

Linear probing forms clusters: runs of consecutive occupied slots. A key hashing anywhere into a cluster of length LL must walk to the end of it, so the expected displacement is proportional to the cluster length, and clusters grow superlinearly with load because a new key landing anywhere in a cluster extends it and merges it with the next.

That is the mechanism behind the 1/(1α)21/(1-\alpha)^2 in the formula: it is a statement about cluster lengths, and cluster lengths are heavy-tailed.

Robin Hood does not break up the clusters — the occupied slots are the same slots, so the clusters are identical. It redistributes the displacement within each cluster, so that instead of one key at the far end paying for the whole cluster’s length, every key in it pays about the average. The cluster length distribution is unchanged; the per-key cost distribution is flattened against it.

That framing explains the table above precisely. At low load the clusters are short and there is little to redistribute; at high load they are long and the redistribution is worth a factor of ten in variance.

Probes per insertion against load factor, table of 8,192The line is Knuth's closed form for linear probing, integrated over the fill; the points are a table actually filled and counted. They agree to within 2.3% everywhere. At a load factor of 0.5 an insertion averages 1.49 probes; at 0.9 it averages 5.39. The formula that matters for insertion is the unsuccessful-search one, ½(1 + 1/(1−α)²), and using the successful-search form instead is an error that hides at low load and is glaring at high load.1234560.000.250.500.75formulameasuredload factor αprobes per insertion8,192 slots, mean of 8 fills, seeds stated in lib/structures.jstwo routes agree to 2.3%
Fig. 3 The classical picture from the structures field: measured probe counts against Knuth’s formula as the load factor rises, in a table of 8,192 slots. Every number on this curve is a mean, and every one of them is the same for Robin Hood hashing as for plain linear probing — which is exactly the point of this essay and is why the curve alone was never enough.

Deletion, which is where open addressing usually goes wrong

Every discussion of open addressing eventually reaches deletion, and it is where the design is at its least elegant.

A key cannot simply be removed. Emptying its slot breaks the probe sequence of every key that walked past it: a lookup for one of those keys would reach the newly empty slot, conclude the key is absent, and be wrong. The standard answer is a tombstone — a marker meaning “occupied for the purpose of continuing, empty for the purpose of inserting” — and tombstones accumulate, so a table that has seen many deletions has an effective load factor above its actual one and eventually has to be rebuilt.

Robin Hood removes the need for tombstones entirely, and the mechanism is the same invariant that flattens the distribution.

Because every key sits at or after its home slot and displacements decrease with distance from a cluster’s start, deleting a key can be repaired by shifting back: take the next key, and if its displacement is greater than zero, move it into the hole and repeat. Stop at the first key whose displacement is zero, because that key is at home and nothing walked past the hole to reach it.

The result is a table with no tombstones, whose load factor is exactly its occupancy, and which never needs rebuilding for that reason. A property of the distribution — that displacements decrease along a cluster — turns into a property of the data structure’s lifecycle, and neither is visible in a probe count.

This site does not measure deletion, and saying so is the point of the paragraph. The counters here are built around insertions and lookups; a structure’s behaviour under a workload of mixed operations over time is a different subject with a different instrument, and it is the one where hash tables in real systems actually spend their difficulty.

The lookup this essay did not measure

Every number above is about keys that are present. The early-stop test is named as a benefit the counters miss, and it is worth pricing, because on the other kind of lookup it is not a refinement — it is the largest effect on the page.

A plain linear-probing search for an absent key cannot stop until it reaches an empty slot, so it walks the whole remaining cluster. The classical expressions say what that costs:

present12(1+11α),absent12(1+1(1α)2)\text{present} \approx \tfrac12\left(1 + \tfrac{1}{1-\alpha}\right), \qquad \text{absent} \approx \tfrac12\left(1 + \tfrac{1}{(1-\alpha)^2}\right)

At a load factor of 0.75 the ratio between them is 3.4. At 0.95 it is nineteen. An unsuccessful lookup in a nearly-full linear-probing table is an order of magnitude more expensive than a successful one, and every plate in this essay measures the cheaper of the two.

Robin Hood removes almost all of it. Because displacements decrease along a cluster, a search that has walked further than the current slot’s occupant can stop immediately: the sought key would have stolen that slot. So an absent lookup stops at about the mean displacement rather than at the end of the cluster, and its cost is close to a present lookup’s — six probes rather than two hundred, at the load where the gap is widest.

That makes the technique’s headline claim understated in a specific way. On present keys the mean is provably unchanged and only the variance moves, which is the essay’s central and slightly austere result. On absent keys the mean itself moves, by a factor that grows without bound as the table fills, and the invariance argument does not apply — because the two policies are no longer doing the same number of probes.

Two things follow that matter more than the variance for many deployments.

Absent lookups are common. A membership test that usually says no, a cache that usually misses, a deduplication check on mostly-new keys — all of them are dominated by the case this essay does not measure, and for all of them Robin Hood is a mean-time improvement rather than a tail one.

And it changes which load factor is affordable. The reason tables resize at 0.75 rather than 0.95 is the 1/(1α)21/(1-\alpha)^2 in the absent-lookup expression, and Robin Hood replaces that term with something closer to 1/(1α)1/(1-\alpha). A structure whose unsuccessful search degrades linearly rather than quadratically can be run fuller, which is memory saved — and it is the same trade the last section describes, arriving from the operation this essay left out.

When the hash is the problem instead

Everything above assumes the hash spreads the keys. A hash is a family, not a function is about what happens when it does not, and Robin Hood does not help there in the way it might appear to.

If every key hashes to the same slot, the cluster is the whole table. Robin Hood redistributes the displacement within it, so instead of one key at displacement n1n-1 and the rest spread below, every key sits at about n/2n/2. The variance collapses, as advertised, and the mean is n/2n/2 and the table is useless.

That is worth stating because “reduces the worst case” sounds like a defence against adversarial keys and it is not one. Robin Hood makes a table’s performance uniform; it does not make it good. Under an adversarial hash it makes every lookup equally terrible instead of most lookups fast and a few catastrophic — which for a latency percentile is arguably worse, since the bad case now happens on every request rather than on one in a thousand.

The defence against adversarial keys is a keyed hash family, and there is no structural substitute for it. Robin Hood addresses the variance that arises from randomness; nothing addresses variance that arises from an adversary except denying the adversary the information.

The load factor and the table size are the two dials, and the finding survives both. The means stay identical because the total displacement is decided by the hash; what moves is everything about the distribution.

Probe displacement at load 0.5, 256 keys in 512 slotsHow far each key sits from the slot it hashed to, under plain linear probing and under Robin Hood. The means are identical — 0.699 both times, and they cannot differ, because the total displacement is decided by the hash and Robin Hood only decides who bears it. The worst case falls from 18 probes to 6, and the variance from 3.42 to 1.13. A table reported by its average lookup cost would show no difference at all between these two.024681012141618slots from homepale: linear probing · dark: Robin Hoodmean 0.699identical for bothworst 18 → 6var 3 → 1256 keys, 512 slots, seed 20260811displacements, counted exactly
Fig. 4 Half a table rather than 85% of one. Both means are 0.699 and cannot differ; the worst case falls from 18 probes to 6 and the variance from 3.42 to 1.13. A table reported by its average lookup cost would show no difference between these two at all.
Probe displacement at load 0.85, 1,740 keys in 2,048 slotsHow far each key sits from the slot it hashed to, under plain linear probing and under Robin Hood. The means are identical — 3.150 both times, and they cannot differ, because the total displacement is decided by the hash and Robin Hood only decides who bears it. The worst case falls from 300 probes to 17, and the variance from 168.28 to 14.20. A table reported by its average lookup cost would show no difference at all between these two.0255075100125150175200225250275300slots from homepale: linear probing · dark: Robin Hoodmean 3.150identical for bothworst 300 → 17var 168 → 141,740 keys, 2,048 slots, seed 20260811displacements, counted exactly
Fig. 5 And four times the table at the original load. Both means are 3.150; the worst case falls from 300 probes to 17 and the variance from 168.28 to 14.20. A bigger table makes the tail worse for plain probing and does not make it worse for Robin Hood, which is the whole of what the reordering buys.

What the libraries do

Robin Hood hashing is the basis of several widely used tables — Rust’s hashbrown was originally Robin Hood before moving to SIMD probing, and a good many C++ open-addressing tables use it directly.

The dominant modern design, Google’s Swiss tables, does something related and different: it stores a byte of each key’s hash in a separate metadata array, and probes sixteen slots at a time with a vector comparison. That does not reduce displacement at all — it makes each probe sixteen times cheaper, which attacks the same problem from the other end.

Two techniques, the same symptom, opposite mechanisms. Robin Hood makes the number of probes uniform; Swiss tables make the probes cheap enough that the number stops mattering. Which is better depends on the same kind of hardware ratio that decided the branchless search — and, as there, this site can decompose the trade and cannot resolve it.

Bucket occupancy under three hashes, 192 keys in 256 bucketsEach strip is the first 64 buckets, drawn as a column per bucket in proportion to how many keys landed in it, with the treeify threshold of 8 marked. a well-spread hash: longest bucket 4, worst lookup 4 chained and 4 treed · the low bits only: longest bucket 31, worst lookup 31 chained and 5 treed · every key collides: longest bucket 192, worst lookup 192 chained and 8 treed. The threshold is chosen so that under a hash worth using it never fires — the JDK's own comment puts the chance of a bucket reaching eight at about 6 × 10⁻⁸ — which makes it a mechanism whose entire value is in the case its author did not control.column height = keys in that bucket · line = threshold of 8a well-spread hashlongest 44 → 4worst lookupthe low bits onlylongest 3131 → 5worst lookupevery key collideslongest 192192 → 8worst lookup192 keys, 256 buckets, seed 20260811threshold 8, 64 buckets shown
Fig. 6 The alternative to open addressing, for comparison: chained buckets, where a collision is a list rather than a walk. The failure modes are completely different — a chained table degrades to one long list under a bad hash where an open-addressed one degrades to one long cluster — and the answer is different too, which is the subject of the next essay.

The load factor is the parameter that matters

One number decides everything above, and it is not the hash and not the policy.

Probe displacement at load 0.6, 307 keys in 512 slotsHow far each key sits from the slot it hashed to, under plain linear probing and under Robin Hood. The means are identical — 0.752 both times, and they cannot differ, because the total displacement is decided by the hash and Robin Hood only decides who bears it. The worst case falls from 18 probes to 6, and the variance from 3.38 to 1.14. A table reported by its average lookup cost would show no difference at all between these two.024681012141618slots from homepale: linear probing · dark: Robin Hoodmean 0.752identical for bothworst 18 → 6var 3 → 1307 keys, 512 slots, seed 20260811displacements, counted exactly
Fig. 7 The same comparison at load 0.6. The worst case is 18 slots from home against Robin Hood’s 6, and the variance 3.38 against 1.14 — real, and a fraction of the effect at 0.95. Below about half full the two policies are almost the same structure.

At load 0.5, plain linear probing’s worst displacement is 18 and Robin Hood’s is 6 — a factor of three, on a table where the mean displacement is 0.699 and almost every lookup is a single probe. At load 0.6 the numbers are the same to within one slot. At load 0.95 it is 114 against 19 on a table where the mean is 4.817.

The gap between the policies grows with the load, and so does the reason to care about it. At low load neither policy is doing much, because there is nothing to redistribute; at high load the redistribution is the difference between a usable table and one with a tail nobody can plan around.

Which reframes the design question. A table can buy a bounded worst case in two ways: keep the load low and rehash often, or keep the load high and use Robin Hood. The first costs memory — a table at load 0.5 is twice the size of one at 0.95 for the same keys — and the second costs writes. The insertion policy and the resize threshold are the same decision approached from two sides, and a library that has chosen one has constrained the other.

That is the same shape as the thresholds in the threshold somebody chose: a constant in a source file — the load factor at which the table doubles, 0.75 in Java, 0.66 in Python, 7/8 in Swiss tables — that decides more about the structure’s behaviour than the algorithm does, and that cannot be chosen independently of the rest.

The theorem is the check

A closing note about method, because this essay’s central fact is unusual for this site.

Almost every claim here is granted by measurement: a class fits or it does not, a formula matches or it does not. This one is granted by an argument — the total displacement is fixed by which slots are occupied, so the mean cannot move — and the measurement’s role is to check the implementation against the theorem rather than to establish the theorem.

That is the right way round and it is worth being explicit about, because the two are easily confused. If the measured means had differed, the conclusion would not have been that the theorem is false. It would have been that robinHood has a bug — most likely losing a key or leaving a slot occupied that should not be — and the assertion’s error message says so.

A measurement that disagrees with a proof is a measurement of something else, and knowing which of the two is under test is the difference between a check and a coincidence.

What this makes readable

Essays that name this one as a prerequisite.

Named alongside this one

Essays reaching for the same objects. Nobody chose these; they are what the concept index makes visible.

What links here

Every essay whose body links to this one.

The objects this essay names

Each one links to every other essay that touches it.

DisplacementDistributionHash tableLinear probingLoad factorOpen-addressingRobin Hood hashingSwapsThresholdVarianceWorst case