Skip to content

Let a slab that is out of nodes borrow a look before it takes one - #2

Merged
floatdrop merged 2 commits into
mainfrom
fix-scan-sharing
Nov 11, 2026
Merged

floatdrop merged 2 commits into
mainfrom
fix-scan-sharing

Conversation

@floatdrop

Copy link
Copy Markdown
Owner

Replacing writes in sieve and s3fifo never shared a scan of the readers' slots, although evicting writes did. This makes them share.

The defect. A slab reuses the nodes of entries that have left only after a look at every reader slot, and it can borrow the look that another slab of the same cache has taken. retire tried to borrow once more nodes were waiting than the slab has patience for. But Replace calls alloc before retire, and on a full slab alloc runs out of spare nodes when exactly patience nodes are waiting, one short of that. There it looked for itself without trying to borrow.

The change. reclaim takes a reason: forRoom (too many nodes waiting: borrow, then look if still too many), forOne (a node is needed: borrow, look only if that released none), forSure (Reclaim, Released: always look).

Effect. With 16 slabs of 256 entries, 200,000 replacing writes took 6,243 looks before and take 528 now; evicting writes take 525 either way. A replacing write from one goroutine on a full cache of 16,384 entries, by the number of CPUs the cache is sized for:

CPUs before after
14 38 ns 35 ns
64 52 ns 36 ns
256 104 ns 37 ns

The published benchmark results are left as they are: with 8 CPUs or fewer the difference is about 1%, within the ranges shown there.

Checks. New tests: TestSharedLooks (fails without the fix), TestAllocBorrowsAroundPin, TestAllocAndTooManyPins. The mutation suite kills all 99 mutants, including five new ones for this logic. An independent review found no safety or liveness defect; its two tests are the last two above. One behavior changes within what the docs promise: a node whose reader has left can wait one round longer if the borrowed look still names it.

🤖 Generated with Claude Code

floatdrop and others added 2 commits November 11, 2026 11:06
A look at the readers' slots goes through every slot there is, and a
slab can make do with the look that another slab of the cache has taken.
That is what keeps the cost of a write from growing with the number of
CPUs. But only writes that evict got to borrow. A write that replaces a
value takes its new node before it retires the old one, and on a full
slab it ran out of spare nodes just before the count of waiting nodes
passed the slab's patience, which is where borrowing was tried. So it
always looked for itself: once for every 32 writes in a shard of 256
entries, where evicting writes look once in 1,350.

reclaim now says what it is for. A slab that needs a node borrows first
and looks only if that got it none. On a full cache of 16,384 entries, a
replacing write from one goroutine took 38 ns with the slots of 14 CPUs,
52 ns with those of 64 and 104 ns with those of 256; it takes 35 to 37 ns
with any of them now.

A node whose reader has left can wait one round longer than before if
the borrowed look still names it. That stays within the patience that
the packages promise.

Co-Authored-By: Claude <noreply@anthropic.com>
The test gave a goroutine 20 ms to find every spare node pinned, then
let go of one and expected the slab to have counted a wait. On a slow
macOS runner under the race detector the goroutine had not got that far,
found a node at its first try, and the test failed.

It now waits until the writer has looked at the readers' slots twice,
which it can only have done by coming away empty-handed once.

Co-Authored-By: Claude <noreply@anthropic.com>
@floatdrop
floatdrop merged commit b6ddf6c into main Nov 11, 2026
7 checks passed
@floatdrop
floatdrop deleted the fix-scan-sharing branch November 11, 2026 06:22
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.

1 participant