Skip to content

Add tinylfu: TinyLFU admission over S3-FIFO's queues - #4

Merged
floatdrop merged 4 commits into
mainfrom
tinylfu
Nov 11, 2026
Merged

floatdrop merged 4 commits into
mainfrom
tinylfu

Conversation

@floatdrop

Copy link
Copy Markdown
Owner

What

A fifth cache, tinylfu. A new entry waits in a small queue, as in S3-FIFO. When its turn comes, it gets into the main queue only if a frequency sketch has its key down as asked for more often of late than the entry that the main queue would evict next. The main queue gives an entry that was read another round, up to three in a row. The size of the small queue is found by Caffeine's hill climber.

Reads take no lock, as in s3fifo and sieve:

  • a read counts on its entry, up to three, and tallies a hit in the slot that it pins with, in a cache line that it writes to anyway;
  • only writers touch the sketch, and they pass on what readers counted when they come by an entry;
  • the climber goes by the hits from the slots and the misses that the shards hand on, so it measures the share of requests answered exactly, for the whole cache.

Commits

  1. internal/cslab: Lookup can tally hits in the reader's slot; Readers.Hits adds them up. s3fifo and sieve pass false.
  2. internal/sketch: the count-min sketch of TinyLFU in Caffeine's layout.
  3. tinylfu: the cache, the climber, a reference model, scenario tests, a fuzz target, and a load test of the climber under the race detector.
  4. bench: tinylfu joins the benchmarks, and all reports are made again; README and the docs that quote them follow the new numbers.

Results (bench/results, Apple M3 Max)

tinylfu otter s3fifo
mean hit ratio over the 11 traces 47.9% 47.5% 44.8%
furthest behind the best cache in any of the 88 rows 5.3 points 5.9 63
rows won outright 25 1 32
  • tinylfu is ahead of otter on every replay in 42 rows and behind in 14.
  • Its hit ratio varies between replays, by up to 5.6 points, because its sketch hashes with a random seed, so it is replayed 11 times, like otter.
  • It reads within 6% of s3fifo at 1, 2 and 8 CPUs, and reads through faster with 8, because it misses less.
  • Writes that evict get 5 to 10% less done than in s3fifo.
  • Memory is about that of s3fifo, and it gives the garbage collector just as little to do.

What speaks against it, all in the README:

  • It was designed on these 11 traces. On 14 others, in a simulation of the policy, it was level with S3-FIFO and W-TinyLFU.
  • On Mixed at the larger capacities it is up to 4 points behind lru.
  • Where every key comes back after one fixed while and never again, the climber has no slope, and it answers far less than lru (6.6% against 66.7% in a synthetic test).
  • With 8 CPUs and 1% writes it was 22% slower than s3fifo. Taking out everything it does differently on that path did not close the gap, and the cause has not been found.

The README now says "If in doubt, use tinylfu" (it said s3fifo). s3fifo stays the advice for the same results on every run and for workloads without loops.

Checked

  • make lint, make test (race detector), go vet and test compilation for linux/386; every commit builds and passes on its own.
  • The cache is compared operation by operation with a model of the policy (which also follows every move of the climber), and fuzzed against it.
  • Mutation testing: 120 mutants of the policy, the climber, the window and the slab's tally. 117 are killed; two survivors are equivalent; the last one, Clear without the window's lock, is caught by the race detector.
  • Three independent reviews:
    • The first found the climber's hit-rate signal biased on skewed workloads. It used read counts capped at 15 and drifted to a large small queue: 93.9% instead of 95.5% on Zipf 1.4. That led to counting hits exactly; the drift is gone, and the climber now ends within 0.2 points of the best fixed size on those workloads.
    • The second found a biased first measurement after Clear with batched shards; fixed, and tested.
    • The third fact-checked the README and docs against the result files; its ten corrections are in.
  • The library's hit ratios agree with an independent simulator of the policy in all 88 rows.

🤖 Generated with Claude Code

floatdrop and others added 4 commits November 11, 2026 21:27
Slab.Lookup takes a tally argument. When it is set, a lookup that finds
its key adds one to a counter in the slot of the Readers that it pinned
the node with, which is in the cache line that a reader writes to anyway.
Readers.Hits adds the counters up. Unlike the Meta of a node, the tally
knows no limit, so a cache can tell how many requests its readers
answered without their writing to anything that all of them share.

s3fifo and sieve do not tally.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
internal/sketch is the count-min sketch of TinyLFU in Caffeine's layout:
four 4-bit counters per key, in four words of one 64-byte block, sixteen
counters to a word and a word for every entry of the cache. Counters stop
at fifteen and are halved, with the tally of what they have counted,
whenever that tally reaches the sample.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A new entry waits in a small queue, as in S3-FIFO. When its turn comes it
gets into the main queue only if the sketch has its key down as asked for
more often of late than the entry that the main queue would evict next;
otherwise it goes. The main queue gives an entry that was read another
round, up to three in a row. Reads take no lock: they count on the entry
up to three, and the next write that comes by the entry tells the sketch,
which only writers touch.

The size of the small queue is found by Caffeine's hill climber, one for
the whole cache. It goes by the share of requests answered, counted
exactly: readers tally their hits in their slots, and shards hand on
their misses, in batches when there are several of them. It starts at a
hundredth of the room and stays between that and four fifths.

The cache is compared step by step with a model of the policy, fuzzed
against it, and checked under the race detector, also while the window
moves and Clear runs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tinylfu joins the caches of the benchmarks, as one whose hit ratio varies
from one replay to the next like otter's: its sketch has keys share
counters by a hash that is seeded at random. The reports are made again
for all caches, and the README and the docs that quote them follow the
new numbers.

On the eleven traces tinylfu answers 47.9% of the requests on average,
otter 47.5% and s3fifo 44.8%, and tinylfu is never more than 5.3 points
behind the best cache. The README now suggests it if in doubt, with what
speaks against it: it was designed on these traces, it varies between
runs, it is up to four points behind lru on Mixed at its larger
capacities, and with eight CPUs and 1% writes it was 22% slower than
s3fifo, which has not been explained.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@floatdrop
floatdrop merged commit a9a687d into main Nov 11, 2026
7 checks passed
@floatdrop
floatdrop deleted the tinylfu branch November 12, 2026 16:53
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