Skip to content

Commit 51f14e5

Browse files
Thomas StrombergThomas Stromberg
authored andcommitted
improve benchmarking code
1 parent 3635b2b commit 51f14e5

File tree

1 file changed

+58
-101
lines changed

1 file changed

+58
-101
lines changed

benchmarks/benchmark_test.go

Lines changed: 58 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import (
77
"math"
88
"math/rand/v2"
99
"strconv"
10-
"strings"
1110
"sync"
1211
"sync/atomic"
1312
"testing"
@@ -29,15 +28,27 @@ func TestBenchmarkSuite(t *testing.T) {
2928
}
3029

3130
fmt.Println()
32-
fmt.Println("╔══════════════════════════════════════════════════════════════════════════════╗")
33-
fmt.Println("║ BDCACHE BENCHMARK COMPARISON ║")
34-
fmt.Println("╚══════════════════════════════════════════════════════════════════════════════╝")
31+
fmt.Println("bdcache benchmark bake-off")
32+
fmt.Println()
3533

3634
runHitRateBenchmark()
3735
runPerformanceBenchmark()
3836
runConcurrentBenchmark()
3937
}
4038

39+
// formatPercent formats a percentage with appropriate precision.
40+
func formatPercent(pct float64) string {
41+
if pct < 10 {
42+
return fmt.Sprintf("%.1f%%", pct)
43+
}
44+
return fmt.Sprintf("%.0f%%", pct)
45+
}
46+
47+
// formatCacheName returns a cache name padded for alignment.
48+
func formatCacheName(name string) string {
49+
return fmt.Sprintf("%-13s", name)
50+
}
51+
4152
// =============================================================================
4253
// Hit Rate Comparison
4354
// =============================================================================
@@ -61,8 +72,6 @@ func runHitRateBenchmark() {
6172
fmt.Println("|---------------|---------|-----------|---------|")
6273

6374
workload := generateWorkload(hitRateWorkload, hitRateKeySpace, hitRateAlpha, 42)
64-
// Use sizes that represent 1%, 2.5%, 5% of 1M keyspace
65-
// Note: freecache has 512KB minimum, so smaller sizes may give it unfair advantage
6675
cacheSizes := []int{10000, 25000, 50000}
6776

6877
caches := []struct {
@@ -89,29 +98,26 @@ func runHitRateBenchmark() {
8998
formatCacheName(c.name), rates[0], rates[1], rates[2])
9099
}
91100

92-
// Print relative performance summary
93101
fmt.Println()
94102
printHitRateSummary(results)
95103
}
96104

97105
func printHitRateSummary(results []hitRateResult) {
98-
// Calculate average hit rate for each cache across all sizes
106+
// Calculate average hit rate for each cache
99107
type avgResult struct {
100108
name string
101109
avg float64
102110
}
103111
avgs := make([]avgResult, len(results))
104-
105112
for i, r := range results {
106-
avg := 0.0
113+
sum := 0.0
107114
for _, rate := range r.rates {
108-
avg += rate
115+
sum += rate
109116
}
110-
avg /= float64(len(r.rates))
111-
avgs[i] = avgResult{name: r.name, avg: avg}
117+
avgs[i] = avgResult{name: r.name, avg: sum / float64(len(r.rates))}
112118
}
113119

114-
// Sort by average hit rate (highest first)
120+
// Sort by average (highest first)
115121
for i := range len(avgs) - 1 {
116122
for j := i + 1; j < len(avgs); j++ {
117123
if avgs[j].avg > avgs[i].avg {
@@ -120,27 +126,25 @@ func printHitRateSummary(results []hitRateResult) {
120126
}
121127
}
122128

123-
// Find bdcache position
124-
var bdcacheIdx int
129+
// Find bdcache
130+
bdcacheIdx := -1
125131
for i, r := range avgs {
126132
if r.name == "bdcache" {
127133
bdcacheIdx = i
128134
break
129135
}
130136
}
137+
if bdcacheIdx < 0 {
138+
return
139+
}
131140

132-
bdcacheAvg := avgs[bdcacheIdx].avg
133-
141+
// Print comparison
134142
if bdcacheIdx == 0 {
135-
// bdcache is best
136-
secondBest := avgs[1]
137-
pctBetter := (bdcacheAvg - secondBest.avg) / secondBest.avg * 100
138-
fmt.Printf("🏆 Hit rate: +%s better than 2nd best (%s)\n", formatPercent(pctBetter), secondBest.name)
143+
pct := (avgs[0].avg - avgs[1].avg) / avgs[1].avg * 100
144+
fmt.Printf("- 🏆 Hit rate: %s faster than next best (%s)\n", formatPercent(pct), avgs[1].name)
139145
} else {
140-
// bdcache is not best
141-
best := avgs[0]
142-
pctWorse := (best.avg - bdcacheAvg) / bdcacheAvg * 100
143-
fmt.Printf("📉 Hit rate: -%s worse than best (%s)\n", formatPercent(pctWorse), best.name)
146+
pct := (avgs[0].avg - avgs[bdcacheIdx].avg) / avgs[bdcacheIdx].avg * 100
147+
fmt.Printf("- 😱 Hit rate: %s slower than best (%s)\n", formatPercent(pct), avgs[0].name)
144148
}
145149
}
146150

@@ -227,10 +231,9 @@ func hitRateTinyLFU(workload []int, cacheSize int) float64 {
227231
}
228232

229233
func hitRateFreecache(workload []int, cacheSize int) float64 {
230-
// freecache uses bytes; estimate ~24 bytes per entry (key + value + overhead)
231234
cacheBytes := cacheSize * 24
232235
if cacheBytes < 512*1024 {
233-
cacheBytes = 512 * 1024 // freecache minimum
236+
cacheBytes = 512 * 1024
234237
}
235238
cache := freecache.NewCache(cacheBytes)
236239
var hits int
@@ -248,7 +251,7 @@ func hitRateFreecache(workload []int, cacheSize int) float64 {
248251
}
249252

250253
// =============================================================================
251-
// Performance Comparison (using Go benchmark infrastructure)
254+
// Performance Comparison
252255
// =============================================================================
253256

254257
const perfCacheSize = 10000
@@ -295,91 +298,48 @@ func runPerformanceBenchmark() {
295298
r.setNs, r.setB, r.setAlloc)
296299
}
297300

298-
// Print relative performance summary
299301
fmt.Println()
300302
printLatencySummary(results, "Get", func(r perfResult) float64 { return r.getNs })
301303
printLatencySummary(results, "Set", func(r perfResult) float64 { return r.setNs })
302304
}
303305

304-
func printLatencySummary(results []perfResult, metric string, getNs func(perfResult) float64) {
305-
// Find bdcache and sort by this metric
306+
func printLatencySummary(results []perfResult, metric string, extract func(perfResult) float64) {
307+
// Sort by metric (lowest first for latency)
306308
sorted := make([]perfResult, len(results))
307309
copy(sorted, results)
308310
for i := range len(sorted) - 1 {
309311
for j := i + 1; j < len(sorted); j++ {
310-
if getNs(sorted[j]) < getNs(sorted[i]) {
312+
if extract(sorted[j]) < extract(sorted[i]) {
311313
sorted[i], sorted[j] = sorted[j], sorted[i]
312314
}
313315
}
314316
}
315317

316-
// Find bdcache position and calculate relative performance
317-
var bdcacheIdx int
318+
// Find bdcache
319+
bdcacheIdx := -1
318320
for i, r := range sorted {
319321
if r.name == "bdcache" {
320322
bdcacheIdx = i
321323
break
322324
}
323325
}
326+
if bdcacheIdx < 0 {
327+
return
328+
}
324329

325-
bdcacheNs := getNs(sorted[bdcacheIdx])
326-
330+
// Print comparison (for latency, lower is better)
327331
if bdcacheIdx == 0 {
328-
// bdcache is fastest
329-
secondBest := sorted[1]
330-
pctFaster := (getNs(secondBest) - bdcacheNs) / bdcacheNs * 100
331-
fmt.Printf("🏆 %s latency: +%s faster than 2nd best (%s)\n", metric, formatPercent(pctFaster), secondBest.name)
332+
pct := (extract(sorted[1]) - extract(sorted[0])) / extract(sorted[0]) * 100
333+
fmt.Printf("- 🏆 %s: %s faster than next best (%s)\n", metric, formatPercent(pct), sorted[1].name)
332334
} else {
333-
// bdcache is not fastest
334-
best := sorted[0]
335-
pctSlower := (bdcacheNs - getNs(best)) / getNs(best) * 100
336-
fmt.Printf("📉 %s latency: -%s slower than best (%s)\n", metric, formatPercent(pctSlower), best.name)
335+
pct := (extract(sorted[bdcacheIdx]) - extract(sorted[0])) / extract(sorted[0]) * 100
336+
fmt.Printf("- 😱 %s: %s slower than best (%s)\n", metric, formatPercent(pct), sorted[0].name)
337337
}
338338
}
339339

340-
// formatPercent formats a percentage with appropriate precision.
341-
// Uses 1 decimal place for values < 10%, otherwise no decimals.
342-
func formatPercent(pct float64) string {
343-
if pct < 10 {
344-
return fmt.Sprintf("%.1f%%", pct)
345-
}
346-
return fmt.Sprintf("%.0f%%", pct)
347-
}
348-
349-
// cacheEmoji returns the emoji for a cache library.
350-
// Each emoji is chosen to represent something about the library.
351-
var cacheEmoji = map[string]string{
352-
"bdcache": "🟡", // yellow circle - our cache
353-
"otter": "🦦", // otter - the animal
354-
"ristretto": "☕", // coffee - Italian espresso
355-
"tinylfu": "🔬", // microscope - tiny/research
356-
"freecache": "🆓", // free sign
357-
"lru": "📚", // books - classic algorithm
358-
}
359-
360-
// formatCacheName returns a cache name with its emoji, padded for alignment.
361-
// All entries use the same format: "name emoji" with padding to 13 visual chars.
362-
// Since emojis take 2 visual chars but more bytes, we manually construct the string.
363-
func formatCacheName(name string) string {
364-
emoji := cacheEmoji[name]
365-
if emoji == "" {
366-
return fmt.Sprintf("%-13s", name)
367-
}
368-
// Construct: "name emoji" then pad with spaces to reach 13 visual chars
369-
// emoji = 2 visual chars, so we need (13 - len(name) - 1 - 2) spaces after emoji
370-
// where 1 is the space between name and emoji
371-
visualLen := len(name) + 1 + 2 // name + space + emoji(2 visual)
372-
padding := 13 - visualLen
373-
if padding < 0 {
374-
padding = 0
375-
}
376-
return name + " " + emoji + strings.Repeat(" ", padding)
377-
}
378-
379340
func measurePerf(name string, getFn, setFn func(b *testing.B)) perfResult {
380341
getResult := testing.Benchmark(getFn)
381342
setResult := testing.Benchmark(setFn)
382-
383343
return perfResult{
384344
name: name,
385345
getNs: float64(getResult.NsPerOp()),
@@ -585,46 +545,43 @@ func runConcurrentBenchmark() {
585545
formatCacheName(r.name), r.getQPS/1e6, r.setQPS/1e6)
586546
}
587547

588-
// Print relative performance summary
589548
fmt.Println()
590549
printThroughputSummary(results, "Get", func(r concurrentResult) float64 { return r.getQPS })
591550
printThroughputSummary(results, "Set", func(r concurrentResult) float64 { return r.setQPS })
592551
}
593552
}
594553

595-
func printThroughputSummary(results []concurrentResult, metric string, getQPS func(concurrentResult) float64) {
596-
// Sort by this metric (highest first for throughput)
554+
func printThroughputSummary(results []concurrentResult, metric string, extract func(concurrentResult) float64) {
555+
// Sort by metric (highest first for throughput)
597556
sorted := make([]concurrentResult, len(results))
598557
copy(sorted, results)
599558
for i := range len(sorted) - 1 {
600559
for j := i + 1; j < len(sorted); j++ {
601-
if getQPS(sorted[j]) > getQPS(sorted[i]) {
560+
if extract(sorted[j]) > extract(sorted[i]) {
602561
sorted[i], sorted[j] = sorted[j], sorted[i]
603562
}
604563
}
605564
}
606565

607-
// Find bdcache position
608-
var bdcacheIdx int
566+
// Find bdcache
567+
bdcacheIdx := -1
609568
for i, r := range sorted {
610569
if r.name == "bdcache" {
611570
bdcacheIdx = i
612571
break
613572
}
614573
}
574+
if bdcacheIdx < 0 {
575+
return
576+
}
615577

616-
bdcacheQPS := getQPS(sorted[bdcacheIdx])
617-
578+
// Print comparison (for throughput, higher is better)
618579
if bdcacheIdx == 0 {
619-
// bdcache is fastest
620-
secondBest := sorted[1]
621-
pctFaster := (bdcacheQPS - getQPS(secondBest)) / getQPS(secondBest) * 100
622-
fmt.Printf("🏆 %s throughput: +%s faster than 2nd best (%s)\n", metric, formatPercent(pctFaster), secondBest.name)
580+
pct := (extract(sorted[0]) - extract(sorted[1])) / extract(sorted[1]) * 100
581+
fmt.Printf("- 🏆 %s: %s faster than next best (%s)\n", metric, formatPercent(pct), sorted[1].name)
623582
} else {
624-
// bdcache is not fastest
625-
best := sorted[0]
626-
pctSlower := (getQPS(best) - bdcacheQPS) / bdcacheQPS * 100
627-
fmt.Printf("📉 %s throughput: -%s slower than best (%s)\n", metric, formatPercent(pctSlower), best.name)
583+
pct := (extract(sorted[0]) - extract(sorted[bdcacheIdx])) / extract(sorted[bdcacheIdx]) * 100
584+
fmt.Printf("- 😱 %s: %s slower than best (%s)\n", metric, formatPercent(pct), sorted[0].name)
628585
}
629586
}
630587

0 commit comments

Comments
 (0)