Skip to content

Commit ae58588

Browse files
Thomas StrombergThomas Stromberg
authored andcommitted
Simplify API by splitting memory and persistent cache up
1 parent 786176b commit ae58588

File tree

7 files changed

+105
-1776
lines changed

7 files changed

+105
-1776
lines changed

README.md

Lines changed: 77 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -34,47 +34,44 @@ As a stupid-fast in-memory cache:
3434
import "github.com/codeGROOVE-dev/bdcache"
3535

3636
// strings as keys, ints as values
37-
cache, _ := bdcache.New[string, int](ctx)
38-
cache.Set(ctx, "answer", 42, 0)
39-
val, found, err := cache.Get(ctx, "answer")
37+
cache := bdcache.Memory[string, int]()
38+
cache.Set("answer", 42, 0)
39+
val, found := cache.Get("answer")
4040
```
4141

42-
With local file persistence to survive restarts:
42+
or with local file persistence to survive restarts:
4343

4444
```go
4545
import (
4646
"github.com/codeGROOVE-dev/bdcache"
4747
"github.com/codeGROOVE-dev/bdcache/persist/localfs"
4848
)
4949

50-
p, err := localfs.New[string, User]("myapp", "")
51-
cache, _ := bdcache.New[string, User](ctx, bdcache.WithPersistence(p))
50+
p, _ := localfs.New[string, User]("myapp", "")
51+
cache, _ := bdcache.Persistent[string, User](ctx, p)
5252

53-
cache.SetAsync(ctx, "answer", 42, 0) // Don't wait for the key to persist
53+
cache.SetAsync(ctx, "user:123", user, 0) // Don't wait for the key to persist
54+
cache.Store.Len(ctx) // Access persistence layer directly
5455
```
5556

5657
A persistent cache suitable for Cloud Run or local development; uses Cloud Datastore if available
5758

5859
```go
5960
p, _ := cloudrun.New[string, User](ctx, "myapp")
60-
cache, _ := bdcache.New[string, User](ctx, bdcache.WithPersistence(p))
61+
cache, _ := bdcache.Persistent[string, User](ctx, p)
6162
```
6263

63-
64-
65-
6664
## Performance against the Competition
6765

6866
bdcache prioritizes high hit-rates and low read latency, but it performs quite well all around.
6967

7068
Here's the results from an M4 MacBook Pro - run `make bench` to see the results for yourself:
71-
7269
### Hit Rate (Zipf α=0.99, 1M ops, 1M keyspace)
7370

7471
| Cache | Size=1% | Size=2.5% | Size=5% |
7572
|---------------|---------|-----------|---------|
76-
| bdcache 🟡 | 94.46% | 94.89% | 95.09% |
77-
| otter 🦦 | 94.27% | 94.68% | 95.09% |
73+
| bdcache 🟡 | 94.45% | 94.91% | 95.09% |
74+
| otter 🦦 | 94.28% | 94.69% | 95.09% |
7875
| ristretto ☕ | 91.63% | 92.44% | 93.02% |
7976
| tinylfu 🔬 | 94.31% | 94.87% | 95.09% |
8077
| freecache 🆓 | 94.03% | 94.15% | 94.75% |
@@ -86,113 +83,113 @@ Here's the results from an M4 MacBook Pro - run `make bench` to see the results
8683

8784
| Cache | Get ns/op | Get B/op | Get allocs | Set ns/op | Set B/op | Set allocs |
8885
|---------------|-----------|----------|------------|-----------|----------|------------|
89-
| bdcache 🟡 | 9.0 | 0 | 0 | 20.0 | 0 | 0 |
90-
| lru 📚 | 22.0 | 0 | 0 | 22.0 | 0 | 0 |
91-
| ristretto ☕ | 31.0 | 14 | 0 | 68.0 | 120 | 3 |
92-
| otter 🦦 | 34.0 | 0 | 0 | 138.0 | 51 | 1 |
93-
| freecache 🆓 | 71.0 | 15 | 1 | 56.0 | 4 | 0 |
94-
| tinylfu 🔬 | 84.0 | 3 | 0 | 105.0 | 175 | 3 |
86+
| bdcache 🟡 | 7.0 | 0 | 0 | 12.0 | 0 | 0 |
87+
| lru 📚 | 24.0 | 0 | 0 | 22.0 | 0 | 0 |
88+
| ristretto ☕ | 30.0 | 13 | 0 | 69.0 | 119 | 3 |
89+
| otter 🦦 | 32.0 | 0 | 0 | 145.0 | 51 | 1 |
90+
| freecache 🆓 | 72.0 | 15 | 1 | 57.0 | 4 | 0 |
91+
| tinylfu 🔬 | 89.0 | 3 | 0 | 106.0 | 175 | 3 |
9592

96-
🏆 Get latency: +144% faster than 2nd best (lru)
97-
🏆 Set latency: +10% faster than 2nd best (lru)
93+
🏆 Get latency: +243% faster than 2nd best (lru)
94+
🏆 Set latency: +83% faster than 2nd best (lru)
9895

9996
### Single-Threaded Throughput (mixed read/write)
10097

10198
| Cache | Get QPS | Set QPS |
10299
|---------------|------------|------------|
103-
| bdcache 🟡 | 79.25M | 43.15M |
104-
| lru 📚 | 36.39M | 36.88M |
105-
| ristretto ☕ | 28.22M | 13.46M |
106-
| otter 🦦 | 25.46M | 7.16M |
107-
| freecache 🆓 | 13.30M | 16.32M |
108-
| tinylfu 🔬 | 11.32M | 9.34M |
100+
| bdcache 🟡 | 77.36M | 61.54M |
101+
| lru 📚 | 34.69M | 35.25M |
102+
| ristretto ☕ | 29.44M | 13.61M |
103+
| otter 🦦 | 25.63M | 7.10M |
104+
| freecache 🆓 | 12.92M | 15.65M |
105+
| tinylfu 🔬 | 10.87M | 8.93M |
109106

110-
🏆 Get throughput: +118% faster than 2nd best (lru)
111-
🏆 Set throughput: +17% faster than 2nd best (lru)
107+
🏆 Get throughput: +123% faster than 2nd best (lru)
108+
🏆 Set throughput: +75% faster than 2nd best (lru)
112109

113110
### Concurrent Throughput (mixed read/write): 4 threads
114111

115112
| Cache | Get QPS | Set QPS |
116113
|---------------|------------|------------|
117-
| bdcache 🟡 | 29.62M | 29.92M |
118-
| ristretto ☕ | 25.98M | 13.12M |
119-
| freecache 🆓 | 25.36M | 21.84M |
120-
| otter 🦦 | 23.14M | 3.99M |
121-
| lru 📚 | 9.39M | 9.64M |
122-
| tinylfu 🔬 | 5.75M | 4.91M |
114+
| bdcache 🟡 | 45.67M | 38.65M |
115+
| otter 🦦 | 28.11M | 4.06M |
116+
| ristretto ☕ | 27.06M | 13.41M |
117+
| freecache 🆓 | 24.67M | 20.84M |
118+
| lru 📚 | 9.29M | 9.56M |
119+
| tinylfu 🔬 | 5.72M | 4.94M |
123120

124-
🏆 Get throughput: +14% faster than 2nd best (ristretto)
125-
🏆 Set throughput: +37% faster than 2nd best (freecache)
121+
🏆 Get throughput: +62% faster than 2nd best (otter)
122+
🏆 Set throughput: +85% faster than 2nd best (freecache)
126123

127124
### Concurrent Throughput (mixed read/write): 8 threads
128125

129126
| Cache | Get QPS | Set QPS |
130127
|---------------|------------|------------|
131-
| bdcache 🟡 | 22.19M | 18.68M |
132-
| otter 🦦 | 19.74M | 3.03M |
133-
| ristretto ☕ | 18.82M | 11.39M |
134-
| freecache 🆓 | 16.83M | 16.30M |
135-
| lru 📚 | 7.55M | 7.68M |
136-
| tinylfu 🔬 | 4.95M | 4.15M |
128+
| bdcache 🟡 | 22.31M | 22.84M |
129+
| otter 🦦 | 19.49M | 3.30M |
130+
| ristretto ☕ | 18.67M | 11.46M |
131+
| freecache 🆓 | 17.34M | 16.36M |
132+
| lru 📚 | 7.66M | 7.75M |
133+
| tinylfu 🔬 | 4.81M | 4.11M |
137134

138-
🏆 Get throughput: +12% faster than 2nd best (otter)
139-
🏆 Set throughput: +15% faster than 2nd best (freecache)
135+
🏆 Get throughput: +14% faster than 2nd best (otter)
136+
🏆 Set throughput: +40% faster than 2nd best (freecache)
140137

141138
### Concurrent Throughput (mixed read/write): 12 threads
142139

143140
| Cache | Get QPS | Set QPS |
144141
|---------------|------------|------------|
145-
| bdcache 🟡 | 24.49M | 24.03M |
146-
| ristretto ☕ | 22.85M | 11.48M |
147-
| otter 🦦 | 21.77M | 2.92M |
148-
| freecache 🆓 | 17.45M | 16.70M |
149-
| lru 📚 | 7.42M | 7.62M |
150-
| tinylfu 🔬 | 4.55M | 3.70M |
142+
| bdcache 🟡 | 26.25M | 24.04M |
143+
| ristretto ☕ | 21.71M | 11.49M |
144+
| otter 🦦 | 19.78M | 2.93M |
145+
| freecache 🆓 | 15.84M | 16.10M |
146+
| lru 📚 | 7.50M | 8.92M |
147+
| tinylfu 🔬 | 4.08M | 3.37M |
151148

152-
🏆 Get throughput: +7.2% faster than 2nd best (ristretto)
153-
🏆 Set throughput: +44% faster than 2nd best (freecache)
149+
🏆 Get throughput: +21% faster than 2nd best (ristretto)
150+
🏆 Set throughput: +49% faster than 2nd best (freecache)
154151

155152
### Concurrent Throughput (mixed read/write): 16 threads
156153

157154
| Cache | Get QPS | Set QPS |
158155
|---------------|------------|------------|
159-
| bdcache 🟡 | 15.96M | 15.55M |
160-
| otter 🦦 | 15.64M | 2.84M |
161-
| ristretto ☕ | 15.59M | 12.31M |
162-
| freecache 🆓 | 15.24M | 14.72M |
163-
| lru 📚 | 7.47M | 7.42M |
164-
| tinylfu 🔬 | 4.71M | 3.43M |
165-
166-
🏆 Get throughput: +2.0% faster than 2nd best (otter)
167-
🏆 Set throughput: +5.6% faster than 2nd best (freecache)
156+
| bdcache 🟡 | 16.92M | 16.00M |
157+
| ristretto ☕ | 15.73M | 11.97M |
158+
| otter 🦦 | 15.70M | 2.89M |
159+
| freecache 🆓 | 14.67M | 14.42M |
160+
| lru 📚 | 7.53M | 8.07M |
161+
| tinylfu 🔬 | 4.75M | 3.41M |
162+
163+
🏆 Get throughput: +7.6% faster than 2nd best (ristretto)
164+
🏆 Set throughput: +11% faster than 2nd best (freecache)
168165

169166
### Concurrent Throughput (mixed read/write): 24 threads
170167

171168
| Cache | Get QPS | Set QPS |
172169
|---------------|------------|------------|
173-
| bdcache 🟡 | 15.93M | 15.41M |
174-
| otter 🦦 | 15.81M | 2.88M |
175-
| ristretto ☕ | 15.57M | 13.20M |
176-
| freecache 🆓 | 14.58M | 14.10M |
177-
| lru 📚 | 7.59M | 7.80M |
178-
| tinylfu 🔬 | 4.96M | 3.73M |
170+
| bdcache 🟡 | 20.08M | 16.56M |
171+
| ristretto ☕ | 16.76M | 12.81M |
172+
| otter 🦦 | 15.71M | 2.93M |
173+
| freecache 🆓 | 14.43M | 14.59M |
174+
| lru 📚 | 7.71M | 7.75M |
175+
| tinylfu 🔬 | 4.80M | 3.09M |
179176

180-
🏆 Get throughput: +0.7% faster than 2nd best (otter)
181-
🏆 Set throughput: +9.2% faster than 2nd best (freecache)
177+
🏆 Get throughput: +20% faster than 2nd best (ristretto)
178+
🏆 Set throughput: +14% faster than 2nd best (freecache)
182179

183180
### Concurrent Throughput (mixed read/write): 32 threads
184181

185182
| Cache | Get QPS | Set QPS |
186183
|---------------|------------|------------|
187-
| bdcache 🟡 | 16.68M | 15.38M |
188-
| otter 🦦 | 15.87M | 2.87M |
189-
| ristretto ☕ | 15.55M | 13.50M |
190-
| freecache 🆓 | 14.64M | 13.84M |
191-
| lru 📚 | 7.87M | 8.01M |
192-
| tinylfu 🔬 | 5.12M | 3.01M |
193-
194-
🏆 Get throughput: +5.1% faster than 2nd best (otter)
195-
🏆 Set throughput: +11% faster than 2nd best (freecache)
184+
| bdcache 🟡 | 15.84M | 15.29M |
185+
| ristretto ☕ | 15.36M | 13.49M |
186+
| otter 🦦 | 15.04M | 2.91M |
187+
| freecache 🆓 | 14.87M | 13.95M |
188+
| lru 📚 | 7.79M | 8.23M |
189+
| tinylfu 🔬 | 5.34M | 3.09M |
190+
191+
🏆 Get throughput: +3.1% faster than 2nd best (ristretto)
192+
🏆 Set throughput: +9.6% faster than 2nd best (freecache)
196193

197194
NOTE: Performance characteristics often have trade-offs. There are almost certainly workloads where other cache implementations are faster, but nobody blends speed and persistence the way that bdcache does.
198195

benchmarks/benchmark_test.go

Lines changed: 12 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
package benchmarks
33

44
import (
5-
"context"
65
"encoding/binary"
76
"fmt"
87
"math"
@@ -156,14 +155,13 @@ func generateWorkload(n, keySpace int, alpha float64, seed uint64) []int {
156155
}
157156

158157
func hitRateBdcache(workload []int, cacheSize int) float64 {
159-
ctx := context.Background()
160-
cache, _ := bdcache.New[int, int](ctx, bdcache.WithMemorySize(cacheSize))
158+
cache := bdcache.Memory[int, int](bdcache.WithSize(cacheSize))
161159
var hits int
162160
for _, key := range workload {
163-
if _, found, _ := cache.Get(ctx, key); found {
161+
if _, found := cache.Get(key); found {
164162
hits++
165163
} else {
166-
_ = cache.Set(ctx, key, key, 0)
164+
cache.Set(key, key, 0)
167165
}
168166
}
169167
return float64(hits) / float64(len(workload)) * 100
@@ -394,23 +392,21 @@ func measurePerf(name string, getFn, setFn func(b *testing.B)) perfResult {
394392
}
395393

396394
func benchBdcacheGet(b *testing.B) {
397-
ctx := context.Background()
398-
cache, _ := bdcache.New[int, int](ctx, bdcache.WithMemorySize(perfCacheSize))
395+
cache := bdcache.Memory[int, int](bdcache.WithSize(perfCacheSize))
399396
for i := range perfCacheSize {
400-
_ = cache.Set(ctx, i, i, 0)
397+
cache.Set(i, i, 0)
401398
}
402399
b.ResetTimer()
403400
for i := range b.N {
404-
_, _, _ = cache.Get(ctx, i%perfCacheSize)
401+
cache.Get(i % perfCacheSize)
405402
}
406403
}
407404

408405
func benchBdcacheSet(b *testing.B) {
409-
ctx := context.Background()
410-
cache, _ := bdcache.New[int, int](ctx, bdcache.WithMemorySize(perfCacheSize))
406+
cache := bdcache.Memory[int, int](bdcache.WithSize(perfCacheSize))
411407
b.ResetTimer()
412408
for i := range b.N {
413-
_ = cache.Set(ctx, i%perfCacheSize, i, 0)
409+
cache.Set(i%perfCacheSize, i, 0)
414410
}
415411
}
416412

@@ -634,16 +630,15 @@ func printThroughputSummary(results []concurrentResult, metric string, getQPS fu
634630

635631
//nolint:gocognit // benchmark code with repetitive cache setup
636632
func measureConcurrentQPS(cacheName string, threads int, write bool) float64 {
637-
ctx := context.Background()
638633
var ops atomic.Int64
639634
var wg sync.WaitGroup
640635
done := make(chan struct{})
641636

642637
switch cacheName {
643638
case "bdcache":
644-
cache, _ := bdcache.New[int, int](ctx, bdcache.WithMemorySize(perfCacheSize))
639+
cache := bdcache.Memory[int, int](bdcache.WithSize(perfCacheSize))
645640
for i := range perfCacheSize {
646-
_ = cache.Set(ctx, i, i, 0)
641+
cache.Set(i, i, 0)
647642
}
648643
for range threads {
649644
wg.Add(1)
@@ -655,9 +650,9 @@ func measureConcurrentQPS(cacheName string, threads int, write bool) float64 {
655650
return
656651
default:
657652
if write {
658-
_ = cache.Set(ctx, i%perfCacheSize, i, 0)
653+
cache.Set(i%perfCacheSize, i, 0)
659654
} else {
660-
_, _, _ = cache.Get(ctx, i%perfCacheSize)
655+
cache.Get(i % perfCacheSize)
661656
}
662657
ops.Add(1)
663658
}

0 commit comments

Comments
 (0)