Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ fuzz: ## Fuzz every target for FUZZTIME (default 30s) each.
$(GO) test -run='^$$' -fuzz=FuzzAgainstModel -fuzztime=$(FUZZTIME) ./twoq
$(GO) test -run='^$$' -fuzz=FuzzAgainstModel -fuzztime=$(FUZZTIME) ./sieve
$(GO) test -run='^$$' -fuzz=FuzzAgainstModel -fuzztime=$(FUZZTIME) ./s3fifo
$(GO) test -run='^$$' -fuzz=FuzzAgainstModel -fuzztime=$(FUZZTIME) ./tinylfu
cd bench && $(GO) test -run='^$$' -fuzz=FuzzRead -fuzztime=$(FUZZTIME) ./trace

.PHONY: bench
Expand Down
228 changes: 133 additions & 95 deletions README.md

Large diffs are not rendered by default.

16 changes: 9 additions & 7 deletions bench/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ libraries they are compared with.

| Name | What it is |
|:---|:---|
| `evict/lru`, `evict/twoq`, `evict/sieve`, `evict/s3fifo` | The caches of this module. |
| `evict/lru`, `evict/twoq`, `evict/sieve`, `evict/s3fifo`, `evict/tinylfu` | The caches of this module. |
| `otter` | [otter](https://github.com/maypok86/otter) v2, the state of the art among Go caches. It evicts with adaptive W-TinyLFU, reads without locks, and writes through buffers that a background task drains. |
| `golang-lru`, `golang-lru/2q` | [hashicorp/golang-lru](https://github.com/hashicorp/golang-lru) v2: an LRU and a 2Q cache behind a single lock, the way most caches used to be built. They are here for perspective, and `golang-lru` also as a check: the tests require `evict/lru` to agree with it on every single request of a trace. Its 2Q is not the same algorithm as `evict/twoq`: it promotes an entry that is read again while it is still recent, where the 2Q of the paper waits for it to come back after it has been evicted. |
| `optimal` | Not a cache but the bound on hit ratios: what a cache would score that knew the requests to come ([Belady's algorithm](https://en.wikipedia.org/wiki/Cache_replacement_policies#Belady's_algorithm), with the option of not storing a key at all). |
Expand All @@ -43,14 +43,16 @@ background, as it is in its own simulator.
The traces are downloaded on first use, into the user's cache directory unless `-dir` says
otherwise, and each file is checked against the SHA-256 that
[dataset/dataset.go](dataset/dataset.go) has on record for it. A run on another machine
therefore replays the very same requests, and the numbers for the caches of this module
and for golang-lru come out the same to the last digit.
therefore replays the very same requests, and the numbers for golang-lru and for the caches
of this module but `evict/tinylfu` come out the same to the last digit.

Otter's do not. It admits some entries at random, and its hit ratio moves from one replay
to the next because of it: by several percentage points at some capacities, and hardly at
all at others. A cache like that is replayed eleven times (`-runs`), and the report has the
median and the range. A cache is only called the winner of a row if it beat the others on
every replay.
all at others. Nor do those of `evict/tinylfu`, whose sketch has keys share counters by a
hash that is seeded at random, so that which keys it confuses differs from one replay to the
next. A cache like that is replayed eleven times (`-runs`), and the report has the median and
the range. A cache is only called the winner of a row if it beat the others on every
replay.

| Dataset | Workload | Requests | Keys | Download |
|:---|:---|---:|---:|---:|
Expand Down Expand Up @@ -149,7 +151,7 @@ measures comes after that. With a million entries it is not: the share of reads
`ReadThrough` reports for those caches is that of a cache which is still learning, and a
few tenths of a percentage point higher where a cache got through more requests in its
second, as it does with more CPUs. Otter's share moves with the number of CPUs in the small
cache as well, from 87.0% to 87.6%. That is not the warm-up, and why it does was not looked
cache as well, between 86.9% with two CPUs and 87.6% with eight. That is not the warm-up, and why it does was not looked
into.

| Benchmark | What every goroutine does |
Expand Down
21 changes: 21 additions & 0 deletions bench/caches/caches.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/floatdrop/evict/lru"
"github.com/floatdrop/evict/s3fifo"
"github.com/floatdrop/evict/sieve"
"github.com/floatdrop/evict/tinylfu"
"github.com/floatdrop/evict/twoq"
)

Expand Down Expand Up @@ -104,6 +105,21 @@ func All[K comparable, V any]() []Kind[K, V] {
return s3fifoCache[K, V]{s3fifo.New[K, V](capacity, s3fifo.WithShards(shards))}
},
},
{
Name: "evict/tinylfu",
New: func(capacity int) Cache[K, V] {
return tinylfuCache[K, V]{tinylfu.New[K, V](capacity)}
},
Exact: func(capacity int) Cache[K, V] {
return tinylfuCache[K, V]{tinylfu.New[K, V](capacity, tinylfu.WithShards(1))}
},
Shards: func(capacity, shards int) Cache[K, V] {
return tinylfuCache[K, V]{tinylfu.New[K, V](capacity, tinylfu.WithShards(shards))}
},
// Which keys share counters in its sketch is up to a hash that
// is seeded at random.
Varies: true,
},
{
Name: "otter",
// Setting the initial capacity is not a default, but it is what
Expand Down Expand Up @@ -170,6 +186,11 @@ type s3fifoCache[K comparable, V any] struct{ c *s3fifo.Cache[K, V] }
func (c s3fifoCache[K, V]) Get(key K) (V, bool) { return c.c.Get(key) }
func (c s3fifoCache[K, V]) Set(key K, value V) { c.c.Set(key, value) }

type tinylfuCache[K comparable, V any] struct{ c *tinylfu.Cache[K, V] }

func (c tinylfuCache[K, V]) Get(key K) (V, bool) { return c.c.Get(key) }
func (c tinylfuCache[K, V]) Set(key K, value V) { c.c.Set(key, value) }

type otterCache[K comparable, V any] struct{ c *otter.Cache[K, V] }

func (c otterCache[K, V]) Get(key K) (V, bool) { return c.c.GetIfPresent(key) }
Expand Down
4 changes: 2 additions & 2 deletions bench/cmd/hitratio/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ type input struct {

// defaultCaches are the columns of the report: the caches of this module,
// the ones they are compared with, and the best that any cache could do.
var defaultCaches = []string{"evict/lru", "evict/twoq", "evict/sieve", "evict/s3fifo", "otter", "golang-lru/2q", report.Bound}
var defaultCaches = []string{"evict/lru", "evict/twoq", "evict/sieve", "evict/s3fifo", "evict/tinylfu", "otter", "golang-lru/2q", report.Bound}

func allPlayers() []player {
var players []player
Expand Down Expand Up @@ -246,7 +246,7 @@ func shardingPlayers() []player {
continue
}
players = append(players,
player{kind.Name, false, func(t *input, capacity int) int {
player{kind.Name, kind.Varies, func(t *input, capacity int) int {
return replay.Hits(kind.Exact(capacity), t.keys)
}},
player{kind.Name + " in shards", true, func(t *input, capacity int) int {
Expand Down
9 changes: 5 additions & 4 deletions bench/cmd/hitratio/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ func TestRun(t *testing.T) {
md := read(t, dir, "hitratio.md")
for _, want := range []string{
"# Hit ratios\n",
"the median of 3 replays",
"the medians of 3 replays",
"in this run by up to ",
"Produced by `go run ./cmd/hitratio -caches evict/lru,evict/sieve@4,otter,optimal -runs 3 ",
"## At a glance\n",
Expand Down Expand Up @@ -118,9 +118,10 @@ func TestSharding(t *testing.T) {
"as on a machine with 8 CPUs",
"up to 64.",
"### Small\n",
"| capacity | evict/lru | evict/twoq | evict/sieve | evict/s3fifo |\n",
// A cache of 100 entries is in one piece either way.
"| 100 | 0.00 | 0.00 | 0.00 | 0.00 |\n",
"| capacity | evict/lru | evict/twoq | evict/sieve | evict/s3fifo | evict/tinylfu |\n",
// A cache of 100 entries is in one piece either way. The last one
// is not the same on every replay even so.
"| 100 | 0.00 | 0.00 | 0.00 | 0.00 | ",
} {
if !strings.Contains(md, want) {
t.Errorf("sharding.md lacks %q:\n%s", want, md)
Expand Down
22 changes: 14 additions & 8 deletions bench/cmd/hitratio/preamble.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,16 +45,17 @@ it is missing. The column `+"`optimal`"+` is not a cache but the bound that no c
beat: what one would score that knew the requests to come.

The caches of this module run in one piece here, with a single shard, and otter evicts on
the spot rather than in the background, as it does in its own simulator. The caches of
this module and those of golang-lru then evict the same way every time, and their numbers
are the same on every run. Otter's are not: it admits some entries at random, which moves
its hit ratio from one replay to the next, in this run %s.
Its number is the median of %d replays, which is also what the charts show, and the column
next to it has the lowest and the highest of them. That is not all the range there is:
of further replays, about %d in a hundred would come out below it or above it.
the spot rather than in the background, as it does in its own simulator. Most of the
caches then evict the same way every time, and their numbers are the same on every run.
Two do not. Otter admits some entries at random, and `+"`evict/tinylfu`"+` counts requests in a
sketch in which keys share counters, by a hash that is seeded at random. That moves their
hit ratios from one replay to the next, in this run %s.
Their numbers are the medians of %d replays, which is also what the charts show, and the
column next to each has the lowest and the highest of them. That is not all the range
there is: of further replays, about %d in a hundred would come out below it or above it.

A number is in bold if the cache did better in that row than every other one, on each of
their replays. Where the range of otter reaches the best of the others, the row has no
their replays. Where the range of one cache reaches the best of the others, the row has no
winner.

A program does not get the caches of this module in one piece unless it asks for that: by
Expand Down Expand Up @@ -101,6 +102,11 @@ so the hit ratio of a cache in shards differs from run to run. The number is the
difference for the median of %d replays, and the brackets hold the differences for the
worst and for the best of them.

`+"`evict/tinylfu`"+` differs from run to run in one piece as well: which keys share counters in
its sketch is up to the same hash. What its replays in shards are compared with is the
median of as many replays in one piece, so its brackets are never empty, not even where a
cache is too small to be split.

%s`, sharedCPUs, 8*sharedCPUs, runs, produced(command))
}

Expand Down
1 change: 1 addition & 0 deletions bench/report/chart.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ var (
"otter": 4,
"golang-lru": 5,
"golang-lru/2q": 6,
"evict/tinylfu": 7,
}
light = [...]string{"#2a78d6", "#eb6834", "#1baf7a", "#eda100", "#e87ba4", "#008300", "#6250d6", "#e34948"}
dark = [...]string{"#3987e5", "#d95926", "#199e70", "#c98500", "#d55181", "#008300", "#9085e9", "#e66767"}
Expand Down
34 changes: 23 additions & 11 deletions bench/results/hitratio-cloudphysics.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading