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
97105func 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
229233func 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
254257const 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-
379340func 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