Repository navigation
Performance: shard layout, call-free wrapped reads, fixed index load - #6
Merged
Merged
Conversation
floatdrop
force-pushed
the
perf-review
branch
from
November 12, 2026 14:56
edb24d7 to
29db709
Compare
The shards of a cache lie side by side, and their sizes were not multiples of 128 bytes, so each one sat at another place in a line, and in some of them the slab header that every read touches lay across two lines. Each cache now wraps its shard in a type that pads it with a line or more and rounds it up to whole lines, so that every shard starts at the same place in one. The fields of a cslab that only writers use move below its pad, which leaves what a read needs at 81 bytes: on one line even where the array starts a word into one, behind the allocator's header, as most do. With 8 CPUs and 1% writes, tinylfu goes from 5.32 to 4.62 ns per operation, level with s3fifo, and sieve from 4.82 to 4.55. Nothing else moves measurably. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The methods of a cache are generic, so they are compiled in the package that instantiates them. Where that package gets to the cache through a generic type of another package that instantiates nothing, as the benchmarks do, the compiler does not have the bodies of the atomic types' methods or of Readers.Pin, and a read made about ten calls, one of them while a node was pinned. The words that readers touch are now plain integers accessed with the functions of sync/atomic, which the compiler builds in: the index, seq and overdue of a slab, the meta of a node, which gets Meta and SetMeta, and the pin and the hits of a slot. seq and overdue become 32 bits wide, and the 64-bit words all lie in slices, which keeps them aligned on 32-bit platforms. Pinning moves from Readers to generic methods of the slab. The inline check gains a wrapper and a package that uses it, and makes sure that reads there call nothing but the lock they rarely take. Through the wrapper of the benchmarks, reads are 2-7% faster and, with 1% writes on 8 CPUs, tinylfu is 9% faster. Used directly, single-threaded reads with string keys are 1-3% slower, the same instructions laid out differently, and 25% writes on 8 CPUs are about 1% faster. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The indexes of the slabs and of the ghosts were tables of a power of two in size, the smallest above 1.5 times the capacity, so that their load went from a third to two thirds as the capacity grew to the next power of two. Lookups were up to 40% slower at two thirds than at a third, at capacities one entry apart. A table now has three slots for every entry, whatever the capacity, and a hash's probe starts at the hash scaled to the size of the table rather than at its lower bits. Probes and the moves that close a gap wrap around the end of a table of any size. Sizes are worked out in uint, so the largest still fits 32-bit platforms. Where the load was two thirds or a half, misses are 25-40% faster and writes that evict up to 29% faster; where it was a third, nothing changes. With 8 CPUs, reads in the benchmarks are 5-9% faster. An entry takes 3 to 18 bytes more, a ghost about twenty, and memory.md and the README say so. Tests that took keys for hashes would now have every entry in one slot, so they spread their keys, and those that collide on purpose put their hashes where they want them, across the end of the table among others. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The benchmarks ran five times on 1, 2, 4 and 8 CPUs on the branch, and the README follows. On 8 CPUs sieve, s3fifo and tinylfu now read 50-55% faster than otter, against 28-36% before. With 1% writes on 8 CPUs, tinylfu is level with s3fifo: the gap of 22% that the README could not explain was the layout of the shards, and is gone, so the README no longer mentions it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
floatdrop
force-pushed
the
perf-review
branch
from
November 12, 2026 15:13
29db709 to
f9d0d36
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Three changes from a performance review, each its own commit, then a fresh throughput run.
1. Lay shards out in whole cache lines (9bcafbc)
Shard sizes were not multiples of 128 B, so the slab header that every lock-free read touches straddled two lines in some shards. A per-package
paddedwrapper rounds shards to whole lines, and the cslab fields only writers use move below its pad (readers' part: 113 → 81 B).2. Keep reads free of calls when a cache is wrapped in a generic type (c6e0e81)
Reached through a generic type of another package that instantiates nothing (as
bench/cachesdoes), the read path made ~10 real calls (typed-atomic methods,Readers.Pin), one of them while a node was pinned. The words readers touch are now plain integers accessed withsync/atomicfunctions (intrinsics);Node.Meta()/SetMeta()are the accessors; pinning moved to genericSlabmethods.internal/inlinecheckgains a wrapper probe that fails on the old code.3. Give every hash index three slots per entry (5c9de57)
Power-of-two tables let the load swing between 1/3 and 2/3 with capacity; lookups were up to 40% slower at 2/3. Tables in
slab,cslabandghostnow havemax(3·cap, 1)slots of any size, with a multiply-shift home slot and wrap-around probing and deletion.memory.mdand the README say so.homeAtso they still cover wrap-around and the stay branch of deletion.4. Measure throughput again (edb24d7)
5 runs on 1/2/4/8 CPUs. On 8 CPUs the lock-free caches read 50–55% faster than otter (was 28–36%); the README's throughput table and claims follow.
Tried and dropped: lazy promotion in
lru(15–23% faster at 4–8 CPUs, 12% slower on 1, ends exact LRU), spinning writers before they park (no gain beyond noise), amaphash.Stringfast path (slower), and a ghost index that holds the hash (would cost 7–11 B per entry).Each commit was reviewed separately;
go test -raceand golangci-lint (incl. 386) pass on each.🤖 Generated with Claude Code