Skip to content

ck_rhs: Fix robin-hood probe depth calculations and a ck_rhs_fas edge case. - #282

Merged
cognet merged 2 commits into
concurrencykit:masterfrom
michael-grunder:fix-rhs-probe-depth
Jul 21, 2026
Merged

ck_rhs: Fix robin-hood probe depth calculations and a ck_rhs_fas edge case.#282
cognet merged 2 commits into
concurrencykit:masterfrom
michael-grunder:fix-rhs-probe-depth

Conversation

@michael-grunder

Copy link
Copy Markdown
Contributor

The two commits have detailed messages but the top-level changes are as follows:

  • The first commit contains a few robin-hood probe depth edge case fixes that we surfaced in normal operation.
  • The second commit fixes a failure whereck_rhs_fas returns success but actually doesn't replace the key. This can happen if the robin-hood insert logic grows the table.

I don't think I'm doing anything obviously wrong, but this is a very advanced hash set. In addition to the two regressions I did quite a lot of fuzzing of the map and could not find any introduced flaws.

The existing serial benchmark is within around 1% of performance on my x86-64 xeon so close to noise although it may be slightly slower due to the correctness fixes.

I also created a more granular benchmark script:
https://gist.github.com/michael-grunder/e3819c48617f130be8e2c75beee75ed8

The numbers from master and the feature branch are comparable.

Allocation failure surfaced an edge case where robin-hood probing limit
could become inaccurate.

The symptom we were seeing was the following:

```c
writeLock();
k = ck_rhs_get(hs, h, k);
d = ck_rhs_remove(hs, h, k);
assert(d == k); // fails because `d == NULL`
```

Standalone reproducer:
https://gist.github.com/michael-grunder/516e6ceb0b93b5f02f7aaac259f9e8d9

The symptom was more likely in low memory conditions causing allocation
failures but could occur without them.

The following three conditions could lead to the error:

1. A full Robin Hood relocation history switched to a probe mode that
   restarted the search instead of continuing the current relocation.
2. A successful Robin Hood relocation could leave its original slot marked
   `in_rh` erroneously.
3. Backward-shift deletion reconstructed a candidate's home bucket with a
   probe count that was one too small. This could lower a probe bound while a
   live entry remained beyond it.

A new `probe_bound.c` regression test is introduced that attempts to surface
all of the above edge cases. Because some of the tests need to peek into
private `ck_rhs_map` struct members it includes the actual
`src/ck_rhs.c` file not just the header.

This commit also includes a small refactor of logic after we are certain
the map just grew on us. Previously in these cases the code was setting
`in_rh = false` which was not required since the larger reallocated map
always has these entries initialized to false.
The ck_rhs_fas function can erroneously return success without actually
replacing the entry when Robin Hood relocation grows the table.

After growth, subsequent probing continued using the old, newly retired
map, causing the replacement to be lost.

The fix is simple. Just update the map pointer when we jump to
`restart`.

Reproducer:
https://gist.github.com/michael-grunder/b0d396001843b46aaa89b58fefd99819
@cognet

cognet commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Hey @michael-grunder, thanks for this!
So it took me a while, I haven't looked at that code for some time, but I believe I get it. For the first patch, basically what you're saying when we're reaching the CK_RHS_MAX_RH limit, we want to use the exact same probe algorithm we use with RH, except that we don't actually want to do any substitution, as if we're using the "regular" algorithm, it may return an entry that is not correct for RH, instead of returning "nope nothing" and we then know we have to grow the map, am I correct in my understanding ?

@michael-grunder

Copy link
Copy Markdown
Contributor Author

basically what you're saying when we're reaching the CK_RHS_MAX_RH limit, we want to use the exact same probe algorithm we use with RH, except that we don't actually want to do any substitution, as if we're using the "regular" algorithm, it may return an entry that is not correct for RH, instead of returning "nope nothing"

Yeah you've got it. What we were seeing in production, especially under allocation failures keeping the table denser than ideal was ck_rhs_remove returning NULL for an entry that is in the map.

/* Invariant: k is in the map but `rhs` will not probe far enough to find it */
d = ck_rhs_remove(hs, h, k);
assert(d == k); // fails because `d == NULL`

Whether this is the optimal solution I'm not sure. Mostly the fix was one edge case that could leave an in_rh flag stale/incorrect, and then an off by one wth the calcualted probe count.

@cognet

cognet commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

That makes to me, even if it is not optimal (and honestly I don't see how to do this better either, except for rewriting the whole thing), at least it is correct, so I'll merge it, we will still be able to make it better later.
Thanks again!

@cognet
cognet merged commit b5475f5 into concurrencykit:master Jul 21, 2026
18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants