pax_global_header00006660000000000000000000000064150121530600014503gustar00rootroot0000000000000052 comment=38fa293bccb612016cb0e4316cdcabed8c2af43f go-sieve-0.3.0/000077500000000000000000000000001501215306000132215ustar00rootroot00000000000000go-sieve-0.3.0/.gitignore000066400000000000000000000012341501215306000152110ustar00rootroot00000000000000# Compiled Object files, Static and Dynamic libs (Shared Objects) *.o *.a *.so # Folders _obj _test # Architecture specific extensions/prefixes *.[568vq] [568vq].out *.cgo1.go *.cgo2.c _cgo_defun.c _cgo_gotypes.go _cgo_export.* _testmain.go *.exe *.test *.prof bin/* .*.sw? .idea logs/* # gg ignores vendor/src/* vendor/pkg/* servers.iml *.DS_Store # vagrant ignores tools/vagrant/.vagrant tools/vagrant/adsrv-conf/.frontend tools/vagrant/adsrv-conf/.bidder tools/vagrant/adsrv-conf/.transcoder tools/vagrant/redis-cluster-conf/7777/nodes.conf tools/vagrant/redis-cluster-conf/7778/nodes.conf tools/vagrant/redis-cluster-conf/7779/nodes.conf *.aof *.rdb *.deb go-sieve-0.3.0/LICENSE000066400000000000000000000024241501215306000142300ustar00rootroot00000000000000BSD 2-Clause License Copyright (c) 2024, Sudhi Herle Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. go-sieve-0.3.0/README.md000066400000000000000000000022551501215306000145040ustar00rootroot00000000000000# go-sieve - SIEVE is simpler than LRU ## What is it? `go-sieve` is a golang implementation of the [SIEVE](https://yazhuozhang.com/assets/pdf/nsdi24-sieve.pdf) cache eviction algorithm. This implementation closely follows the paper's pseudo-code - but uses golang generics to provide an ergonomic interface. ## Key Design Features ### Custom Memory Allocator for Reduced GC Pressure This implementation uses a custom memory allocator designed to minimize GC pressure: - **Pre-allocated Node Pool**: Rather than allocating nodes individually, the cache pre-allocates all nodes at initialization time in a single contiguous array based on cache capacity. - **Efficient Freelist**: Recycled nodes are managed through a zero-overhead freelist that repurposes the existing node pointers, avoiding the need for auxiliary data structures. - **Single-Allocation Strategy**: By allocating all memory upfront in a single operation, the implementation reduces heap fragmentation and minimizes the number of objects the garbage collector must track. ## Usage The API is designed to be simple and intuitive. See the test files for examples of how to use the cache in your applications. go-sieve-0.3.0/assert_test.go000066400000000000000000000014071501215306000161120ustar00rootroot00000000000000// assert_test.go - utility function for tests // // (c) 2024 Sudhi Herle // // Licensing Terms: GPLv2 // // If you need a commercial license for this work, please contact // the author. // // This software does not come with any express or implied // warranty; it is provided "as is". No claim is made to its // suitability for any purpose. package sieve_test import ( "fmt" "runtime" "testing" ) func newAsserter(t *testing.T) func(cond bool, msg string, args ...interface{}) { return func(cond bool, msg string, args ...interface{}) { if cond { return } _, file, line, ok := runtime.Caller(1) if !ok { file = "???" line = 0 } s := fmt.Sprintf(msg, args...) t.Fatalf("%s: %d: Assertion failed: %s\n", file, line, s) } } go-sieve-0.3.0/go.mod000066400000000000000000000000571501215306000143310ustar00rootroot00000000000000module github.com/opencoff/go-sieve go 1.24.3 go-sieve-0.3.0/go.sum000066400000000000000000000000001501215306000143420ustar00rootroot00000000000000go-sieve-0.3.0/sieve.go000066400000000000000000000202151501215306000146630ustar00rootroot00000000000000// sieve.go - SIEVE - a simple and efficient cache // // (c) 2024 Sudhi Herle // // Copyright 2024- Sudhi Herle // License: BSD-2-Clause // // If you need a commercial license for this work, please contact // the author. // // This software does not come with any express or implied // warranty; it is provided "as is". No claim is made to its // suitability for any purpose. // This is golang implementation of the SIEVE cache eviction algorithm // The original paper is: // https://yazhuozhang.com/assets/pdf/nsdi24-sieve.pdf // // This implementation closely follows the paper - but uses golang generics // for an ergonomic interface. // Package sieve implements the SIEVE cache eviction algorithm. // SIEVE stands in contrast to other eviction algorithms like LRU, 2Q, ARC // with its simplicity. The original paper is in: // https://yazhuozhang.com/assets/pdf/nsdi24-sieve.pdf // // SIEVE is built on a FIFO queue - with an extra pointer (called "hand") in // the paper. This "hand" plays a crucial role in determining who to evict // next. package sieve import ( "fmt" "strings" "sync" "sync/atomic" ) // node contains the tuple as a node in a linked list. type node[K comparable, V any] struct { sync.Mutex key K val V visited atomic.Bool next *node[K, V] prev *node[K, V] } // allocator manages a fixed pool of pre-allocated nodes and a freelist type allocator[K comparable, V any] struct { nodes []node[K, V] // Pre-allocated array of all nodes freelist *node[K, V] // Head of freelist of available nodes backing []node[K, V] // backing array - to help with reset/purge } // newAllocator creates a new allocator with capacity nodes func newAllocator[K comparable, V any](capacity int) *allocator[K, V] { a := make([]node[K, V], capacity) return &allocator[K, V]{ nodes: a, freelist: nil, backing: a, } } // alloc retrieves a node from the allocator // It first tries the freelist, then falls back to the pre-allocated array func (a *allocator[K, V]) alloc() *node[K, V] { // If freelist is not empty, use a node from there if a.freelist != nil { n := a.freelist a.freelist = n.next return n } // If we've used all pre-allocated nodes, return nil if len(a.nodes) == 0 { return nil } // Take a node from the pre-allocated array and shrink it n := &a.nodes[0] a.nodes = a.nodes[1:] return n } // free returns a node to the freelist func (a *allocator[K, V]) free(n *node[K, V]) { // Add the node to the head of the freelist n.next = a.freelist a.freelist = n } // reset resets the allocator as if newAllocator() is called func (a *allocator[K, V]) reset() { a.freelist = nil a.nodes = a.backing } // capacity returns the capacity of the cache func (a *allocator[K, V]) capacity() int { return cap(a.backing) } // Sieve represents a cache mapping the key of type 'K' with // a value of type 'V'. The type 'K' must implement the // comparable trait. An instance of Sieve has a fixed max capacity; // new additions to the cache beyond the capacity will cause cache // eviction of other entries - as determined by the SIEVE algorithm. type Sieve[K comparable, V any] struct { mu sync.Mutex cache *syncMap[K, *node[K, V]] head *node[K, V] tail *node[K, V] hand *node[K, V] size int allocator *allocator[K, V] } // New creates a new cache of size 'capacity' mapping key 'K' to value 'V' func New[K comparable, V any](capacity int) *Sieve[K, V] { s := &Sieve[K, V]{ cache: newSyncMap[K, *node[K, V]](), allocator: newAllocator[K, V](capacity), } return s } // Get fetches the value for a given key in the cache. // It returns true if the key is in the cache, false otherwise. // The zero value for 'V' is returned when key is not in the cache. func (s *Sieve[K, V]) Get(key K) (V, bool) { if v, ok := s.cache.Get(key); ok { v.visited.Store(true) return v.val, true } var x V return x, false } // Add adds a new element to the cache or overwrite one if it exists // Return true if we replaced, false otherwise func (s *Sieve[K, V]) Add(key K, val V) bool { if v, ok := s.cache.Get(key); ok { v.Lock() v.visited.Store(true) v.val = val v.Unlock() return true } s.mu.Lock() s.add(key, val) s.mu.Unlock() return false } // Probe adds if not present in the cache. // Returns: // // when key is present in the cache // when key is not present in the cache func (s *Sieve[K, V]) Probe(key K, val V) (V, bool) { if v, ok := s.cache.Get(key); ok { v.visited.Store(true) return v.val, true } s.mu.Lock() s.add(key, val) s.mu.Unlock() return val, false } // Delete deletes the named key from the cache // It returns true if the item was in the cache and false otherwise func (s *Sieve[K, V]) Delete(key K) bool { if v, ok := s.cache.Del(key); ok { s.mu.Lock() s.remove(v) s.mu.Unlock() return true } return false } // Purge resets the cache func (s *Sieve[K, V]) Purge() { s.mu.Lock() s.cache = newSyncMap[K, *node[K, V]]() s.head = nil s.tail = nil s.hand = nil // Reset the allocator s.allocator.reset() s.size = 0 s.mu.Unlock() } // Len returns the current cache utilization func (s *Sieve[K, V]) Len() int { return s.size } // Cap returns the max cache capacity func (s *Sieve[K, V]) Cap() int { return s.allocator.capacity() } // String returns a string description of the sieve cache func (s *Sieve[K, V]) String() string { s.mu.Lock() m := s.desc() s.mu.Unlock() return m } // Dump dumps all the cache contents as a newline delimited // string. func (s *Sieve[K, V]) Dump() string { var b strings.Builder s.mu.Lock() b.WriteString(s.desc()) b.WriteRune('\n') for n := s.head; n != nil; n = n.next { h := " " if n == s.hand { h = ">>" } b.WriteString(fmt.Sprintf("%svisited=%v, key=%v, val=%v\n", h, n.visited.Load(), n.key, n.val)) } s.mu.Unlock() return b.String() } // -- internal methods -- // add a new tuple to the cache and evict as necessary // caller must hold lock. func (s *Sieve[K, V]) add(key K, val V) { // cache miss; we evict and fnd a new node if s.size == s.allocator.capacity() { s.evict() } n := s.newNode(key, val) // Eviction is guaranteed to remove one node; so this should never happen. if n == nil { msg := fmt.Sprintf("%T: add <%v>: objpool empty after eviction", s, key) panic(msg) } s.cache.Put(key, n) // insert at the head of the list n.next = s.head n.prev = nil if s.head != nil { s.head.prev = n } s.head = n if s.tail == nil { s.tail = n } s.size += 1 } // evict an item from the cache. // NB: Caller must hold the lock func (s *Sieve[K, V]) evict() { hand := s.hand if hand == nil { hand = s.tail } for hand != nil { if !hand.visited.Load() { s.cache.Del(hand.key) s.remove(hand) s.hand = hand.prev return } hand.visited.Store(false) hand = hand.prev // wrap around and start again if hand == nil { hand = s.tail } } s.hand = hand } func (s *Sieve[K, V]) remove(n *node[K, V]) { s.size -= 1 // remove node from list if n.prev != nil { n.prev.next = n.next } else { s.head = n.next } if n.next != nil { n.next.prev = n.prev } else { s.tail = n.prev } // Return the node to the allocator's freelist s.allocator.free(n) } func (s *Sieve[K, V]) newNode(key K, val V) *node[K, V] { // Get a node from the allocator n := s.allocator.alloc() if n == nil { return nil } n.key, n.val = key, val n.next, n.prev = nil, nil n.visited.Store(false) return n } // desc describes the properties of the sieve func (s *Sieve[K, V]) desc() string { m := fmt.Sprintf("cache<%T>: size %d, cap %d, head=%p, tail=%p, hand=%p", s, s.size, s.allocator.capacity(), s.head, s.tail, s.hand) return m } // generic sync.Map type syncMap[K comparable, V any] struct { m sync.Map } func newSyncMap[K comparable, V any]() *syncMap[K, V] { m := syncMap[K, V]{} return &m } func (m *syncMap[K, V]) Get(key K) (V, bool) { v, ok := m.m.Load(key) if ok { return v.(V), true } var z V return z, false } func (m *syncMap[K, V]) Put(key K, val V) { m.m.Store(key, val) } func (m *syncMap[K, V]) Del(key K) (V, bool) { x, ok := m.m.LoadAndDelete(key) if ok { return x.(V), true } var z V return z, false } go-sieve-0.3.0/sieve_bench_custom_test.go000066400000000000000000000117501501215306000204570ustar00rootroot00000000000000// sieve_bench_custom_test.go - benchmarks for Sieve cache with custom memory allocator // // (c) 2024 Sudhi Herle // // Copyright 2024- Sudhi Herle // License: BSD-2-Clause package sieve_test import ( "fmt" "math/rand" "runtime" "runtime/debug" "testing" "time" "github.com/opencoff/go-sieve" ) // BenchmarkSieveAdd benchmarks the Add operation func BenchmarkSieveAdd(b *testing.B) { // Test with various cache sizes for _, cacheSize := range []int{1024, 8192, 32768} { b.Run(fmt.Sprintf("CacheSize_%d", cacheSize), func(b *testing.B) { cache := sieve.New[int, int](cacheSize) // Generate keys with some predictable access pattern keys := make([]int, b.N) for i := 0; i < b.N; i++ { if i%3 == 0 { // Reuse some keys for cache hits keys[i] = i % (cacheSize / 2) } else { // Use new keys for cache misses keys[i] = i + cacheSize } } b.ResetTimer() // Perform add operations that will cause evictions for i := 0; i < b.N; i++ { key := keys[i] cache.Add(key, key) // Occasionally delete some items to test the node recycling if i%5 == 0 && i > 0 { cache.Delete(keys[i-1]) } } }) } } // BenchmarkSieveGetHitMiss benchmarks Get operations with a mix of hits and misses func BenchmarkSieveGetHitMiss(b *testing.B) { cacheSize := 8192 cache := sieve.New[int, int](cacheSize) // Fill the cache with initial data for i := 0; i < cacheSize; i++ { cache.Add(i, i) } // Generate a mix of hit and miss patterns keys := make([]int, b.N) for i := 0; i < b.N; i++ { if i%2 == 0 { // Cache hit keys[i] = rand.Intn(cacheSize) } else { // Cache miss keys[i] = cacheSize + rand.Intn(cacheSize) } } b.ResetTimer() // Perform get operations var hit, miss int for i := 0; i < b.N; i++ { key := keys[i] if _, ok := cache.Get(key); ok { hit++ } else { miss++ // Add key that was a miss cache.Add(key, key) } } b.StopTimer() hitRatio := float64(hit) / float64(b.N) b.ReportMetric(hitRatio, "hit-ratio") } // BenchmarkSieveConcurrency benchmarks high concurrency operations func BenchmarkSieveConcurrency(b *testing.B) { cacheSize := 16384 cache := sieve.New[int, int](cacheSize) b.ResetTimer() // Run a highly concurrent benchmark with many goroutines b.RunParallel(func(pb *testing.PB) { // Each goroutine gets its own random seed r := rand.New(rand.NewSource(rand.Int63())) for pb.Next() { // Random operation: get, add, or delete op := r.Intn(10) key := r.Intn(cacheSize * 2) // Half will be misses if op < 6 { // 60% gets cache.Get(key) } else if op < 9 { // 30% adds cache.Add(key, key) } else { // 10% deletes cache.Delete(key) } } }) } // BenchmarkSieveGCPressure specifically measures the impact on garbage collection func BenchmarkSieveGCPressure(b *testing.B) { // Run with different cache sizes for _, cacheSize := range []int{1000, 10000, 50000} { b.Run(fmt.Sprintf("CacheSize_%d", cacheSize), func(b *testing.B) { // Fixed number of operations for consistent measurement operations := 1000000 // Force GC before test runtime.GC() // Capture GC stats before var statsBefore debug.GCStats debug.ReadGCStats(&statsBefore) var memStatsBefore runtime.MemStats runtime.ReadMemStats(&memStatsBefore) startTime := time.Now() // Create cache with custom allocator cache := sieve.New[int, int](cacheSize) // Run the workload runSieveWorkload(cache, operations) elapsedTime := time.Since(startTime) // Force GC to get accurate stats runtime.GC() // Capture GC stats after var statsAfter debug.GCStats debug.ReadGCStats(&statsAfter) var memStatsAfter runtime.MemStats runtime.ReadMemStats(&memStatsAfter) // Report metrics gcCount := statsAfter.NumGC - statsBefore.NumGC pauseTotal := statsAfter.PauseTotal - statsBefore.PauseTotal b.ReportMetric(float64(gcCount), "GC-cycles") b.ReportMetric(float64(pauseTotal.Nanoseconds())/float64(operations), "GC-pause-ns/op") b.ReportMetric(float64(memStatsAfter.HeapObjects)/float64(operations), "heap-objs/op") b.ReportMetric(float64(operations)/elapsedTime.Seconds(), "ops/sec") }) } } // runWorkload performs a consistent workload that stresses node allocation/deallocation func runSieveWorkload(cache *sieve.Sieve[int, int], operations int) { capacity := cache.Cap() // Create a workload that ensures significant cache churn for i := 0; i < operations; i++ { key := i % (capacity * 2) // Ensure we cycle through keys causing evictions // Mix of operations: 70% adds, 20% gets, 10% deletes op := i % 10 if op < 7 { // Add - heavy on adds to stress allocation cache.Add(key, i) } else if op < 9 { // Get cache.Get(key) } else { // Delete - to trigger freelist recycling cache.Delete(key) } // Every so often, add a burst of new entries to trigger evictions if i > 0 && i%10000 == 0 { for j := 0; j < capacity/10; j++ { cache.Add(i+j+capacity, i+j) } } } } go-sieve-0.3.0/sieve_bench_test.go000066400000000000000000000025441501215306000170660ustar00rootroot00000000000000// sieve_bench_test.go -- benchmark testing // // (c) 2024 Sudhi Herle // // Copyright 2024- Sudhi Herle // License: BSD-2-Clause // // If you need a commercial license for this work, please contact // the author. // // This software does not come with any express or implied // warranty; it is provided "as is". No claim is made to its // suitability for any purpose. package sieve_test import ( "math/rand" "sync/atomic" "testing" "github.com/opencoff/go-sieve" ) func BenchmarkSieve_Add(b *testing.B) { c := sieve.New[int, int](8192) ent := make([]int, b.N) for i := 0; i < b.N; i++ { var k int if i%2 == 0 { k = int(rand.Int63() % 16384) } else { k = int(rand.Int63() % 32768) } ent[i] = k } b.ResetTimer() for i := 0; i < b.N; i++ { k := ent[i] c.Add(k, k) } } func BenchmarkSieve_Get(b *testing.B) { c := sieve.New[int, int](8192) ent := make([]int, b.N) for i := 0; i < b.N; i++ { var k int if i%2 == 0 { k = int(rand.Int63() % 16384) } else { k = int(rand.Int63() % 32768) } c.Add(k, k) ent[i] = k } b.ResetTimer() var hit, miss int64 for i := 0; i < b.N; i++ { if _, ok := c.Get(ent[i]); ok { atomic.AddInt64(&hit, 1) } else { atomic.AddInt64(&miss, 1) } } b.Logf("%d: hit %d, miss %d, ratio %4.2f", b.N, hit, miss, float64(hit)/float64(hit+miss)) } go-sieve-0.3.0/sieve_test.go000066400000000000000000000122071501215306000157240ustar00rootroot00000000000000// sieve_test.go - test harness for sieve cache // // (c) 2024 Sudhi Herle // // Copyright 2024- Sudhi Herle // License: BSD-2-Clause // // If you need a commercial license for this work, please contact // the author. // // This software does not come with any express or implied // warranty; it is provided "as is". No claim is made to its // suitability for any purpose. package sieve_test import ( "encoding/binary" "fmt" "math/rand" "runtime" "strings" "sync" "sync/atomic" "testing" "time" "github.com/opencoff/go-sieve" ) func TestBasic(t *testing.T) { assert := newAsserter(t) s := sieve.New[int, string](4) ok := s.Add(1, "hello") assert(!ok, "empty cache: expected clean add of 1") ok = s.Add(2, "foo") assert(!ok, "empty cache: expected clean add of 2") ok = s.Add(3, "bar") assert(!ok, "empty cache: expected clean add of 3") ok = s.Add(4, "gah") assert(!ok, "empty cache: expected clean add of 4") ok = s.Add(1, "world") assert(ok, "key 1: expected to replace") ok = s.Add(5, "boo") assert(!ok, "adding 5: expected to be new add") _, ok = s.Get(2) assert(!ok, "evict: expected 2 to be evicted") } func TestEvictAll(t *testing.T) { assert := newAsserter(t) size := 128 s := sieve.New[int, string](size) for i := 0; i < size*2; i++ { val := fmt.Sprintf("val %d", i) _, ok := s.Probe(i, val) assert(!ok, "%d: exp new add", i) } // the first half should've been all evicted for i := 0; i < size; i++ { _, ok := s.Get(i) assert(!ok, "%d: exp to be evicted", i) } // leaving the second half intact for i := size; i < size*2; i++ { ok := s.Delete(i) assert(ok, "%d: exp del on existing cache elem") } } func TestAllOps(t *testing.T) { size := 8192 vals := randints(size * 3) s := sieve.New[uint64, uint64](size) for i := range vals { k := vals[i] s.Add(k, k) } vals = shuffle(vals) var hit, miss int for i := range vals { k := vals[i] _, ok := s.Get(k) if ok { hit++ } else { miss++ } } t.Logf("%d items: hit %d, miss %d, ratio %4.2f\n", len(vals), hit, miss, float64(hit)/float64(hit+miss)) } type timing struct { typ string d time.Duration hit, miss uint64 } type barrier atomic.Uint64 func (b *barrier) Wait() { v := (*atomic.Uint64)(b) for { if v.Load() == 1 { return } runtime.Gosched() } } func (b *barrier) Signal() { v := (*atomic.Uint64)(b) v.Store(1) } func TestSpeed(t *testing.T) { size := 32768 vals := randints(size * 3) //valr := shuffle(vals) // we will start 4 types of workers: add, get, del, probe // each worker will be working on a shuffled version of // the uint64 array. for ncpu := 2; ncpu <= 32; ncpu *= 2 { var wg sync.WaitGroup wg.Add(ncpu) s := sieve.New[uint64, uint64](size) var bar barrier // number of workers of each type m := ncpu / 2 ch := make(chan timing, m) for i := 0; i < m; i++ { go func(ch chan timing, wg *sync.WaitGroup) { var hit, miss uint64 bar.Wait() st := time.Now() // shuffled array for _, x := range vals { v := x % 16384 if _, ok := s.Get(v); ok { hit++ } else { miss++ } } d := time.Now().Sub(st) ch <- timing{ typ: "get", d: d, hit: hit, miss: miss, } wg.Done() }(ch, &wg) go func(ch chan timing, wg *sync.WaitGroup) { var hit, miss uint64 bar.Wait() st := time.Now() for _, x := range vals { v := x % 16384 if _, ok := s.Probe(v, v); ok { hit++ } else { miss++ } } d := time.Now().Sub(st) ch <- timing{ typ: "probe", d: d, hit: hit, miss: miss, } wg.Done() }(ch, &wg) } bar.Signal() // wait for goroutines to end and close the chan go func() { wg.Wait() close(ch) }() // now harvest timing times := map[string]timing{} for tm := range ch { if v, ok := times[tm.typ]; ok { z := (int64(v.d) + int64(tm.d)) / 2 v.d = time.Duration(z) v.hit = (v.hit + tm.hit) / 2 v.miss = (v.miss + tm.miss) / 2 times[tm.typ] = v } else { times[tm.typ] = tm } } var out strings.Builder fmt.Fprintf(&out, "Tot CPU %d, workers/type %d %d elems\n", ncpu, m, len(vals)) for _, v := range times { var ratio string ns := toNs(int64(v.d), len(vals), m) ratio = hitRatio(v.hit, v.miss) fmt.Fprintf(&out, "%6s %4.2f ns/op%s\n", v.typ, ns, ratio) } t.Logf("%s", out.String()) } } func dup[T ~[]E, E any](v T) []E { n := len(v) g := make([]E, n) copy(g, v) return g } func shuffle[T ~[]E, E any](v T) []E { i := len(v) for i--; i >= 0; i-- { j := rand.Intn(i + 1) v[i], v[j] = v[j], v[i] } return v } func toNs(tot int64, nvals, ncpu int) float64 { return (float64(tot) / float64(nvals)) / float64(ncpu) } func hitRatio(hit, miss uint64) string { r := float64(hit) / float64(hit+miss) return fmt.Sprintf(" hit-ratio %4.2f (hit %d, miss %d)", r, hit, miss) } func randints(sz int) []uint64 { var b [8]byte v := make([]uint64, sz) for i := 0; i < sz; i++ { n, err := rand.Read(b[:]) if n != 8 || err != nil { panic("can't generate rand") } v[i] = binary.BigEndian.Uint64(b[:]) } return v }