Skip to content

Add stats: count the hits, misses and evictions of any cache - #5

Merged
floatdrop merged 1 commit into
mainfrom
stats
Nov 12, 2026
Merged

floatdrop merged 1 commit into
mainfrom
stats

Conversation

@floatdrop

Copy link
Copy Markdown
Owner

What

A new package, stats. stats.Wrap puts a counter in front of any evict.Cache and is one itself:

cache := stats.Wrap(tinylfu.New[string, int](10_000))
s := cache.Stats() // Hits, Misses, Evictions, Len, Cap
s.HitRatio()
cache.Stats().Sub(s) // the counts of an interval
  • Get counts a hit or a miss.
  • Set counts an eviction when the entry it reports is not the one the key had, or when a cache without capacity hands the new entry back. A replacement is not an eviction.
  • Peek, Delete, Clear and All count nothing; Clear does not reset the counts.

The README's "no statistics" is gone, and it has a short Statistics section.

Why a wrapper

Counting inside each policy would be cheaper: the caches already write to a line in the right place (the reader slots, the shard under its lock). A wrapper touches none of the policy code and is easy to drop if it turns out not to be worth it.

Cost (Apple M3 Max)

tinylfu Get, all hits 1 CPU 8 CPUs
direct 15.7 ns 2.17 ns/op
through evict.Cache 16.0 ns 2.22 ns/op
through stats.Wrap 17.4 ns 2.37 ns/op
  • 10–14% over a few runs (medians of 7 above).
  • Goroutines count in stripes of 128 bytes, 16 per CPU, picked by stack address like the reader slots of cslab. That is 2–4 KiB per CPU for every wrapped cache, whatever its capacity.
  • A key converted from a []byte on the spot allocates through the wrapper (1 alloc against 0), because the compiler cannot see through the interface.
  • Stats() reads every stripe and calls Len: 0.17 µs on 8 CPUs for an idle tinylfu.

Checked

  • The conformance suite and NoAllocs from internal/cachetest, on wrapped lru and s3fifo.
  • Exact counts under 16 goroutines: hits + misses = requests, and evictions = entries added − entries left.
  • Evictions on all five policies, including -0 replacing +0, NaN keys and zero capacity.
  • A test that goroutines running at the same time count in different stripes. It fails if every goroutine counts in one stripe; that change alone made Get 20 times slower on 8 CPUs, and every other test still passed.
  • internal/inlinecheck has three new probes: direct use, and use through a generic type of another package that instantiates nothing. Get and Set make no call other than the one through the interface. The probes fail if the counters are switched to atomic.Uint64.
  • An independent review agent checked the counting against a model on all five policies (capacities 0 to 600, more shards than entries, any keys mixing 0, -0.0, "0" and nil) and found nothing wrong.
  • golangci-lint clean; go test -race ./... passes; GOOS=linux GOARCH=386 go vet ./... clean (32-bit not run, only vetted).

Note

perf-review also changes internal/inlinecheck/inline_test.go; whichever lands second resolves a small conflict in its patterns.

🤖 Generated with Claude Code

stats.Wrap puts a counter in front of an evict.Cache. Get counts a hit
or a miss, and Set an eviction when the entry it reports is not the one
the key had, or when a cache without capacity hands the new entry back.
Stats returns the counts with Len and Cap; HitRatio and Sub turn them
into rates.

Goroutines count in stripes of a cache line each, sixteen per CPU,
picked by the address of their stack as the reader slots of cslab are,
so that the reads of the lock-free caches keep scaling. A Get of tinylfu
that hits took 10 to 14% longer through the wrapper on one CPU and on
eight, on an Apple M3 Max.

The counts are plain words used with the functions of sync/atomic: with
the atomic types, a program that reaches the wrapper through a generic
type that instantiates nothing makes a call for every count. Probes in
internal/inlinecheck check both ways of using it.

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