Explorar el Código

Adding some alternative string bank implementations for testing.

Matt Bolt hace 1 mes
padre
commit
a0fb2d42fc

+ 154 - 0
core/pkg/util/stringutil/lrubank.go

@@ -0,0 +1,154 @@
+package stringutil
+
+import (
+	"container/heap"
+	"sync"
+	"time"
+)
+
+type lruEntry struct {
+	value string
+	used  time.Time
+}
+
+type heapEntry struct {
+	*lruEntry
+	key string
+}
+
+type maxHeap []*heapEntry
+
+func (h maxHeap) Len() int           { return len(h) }
+func (h maxHeap) Less(i, j int) bool { return h[i].used.After(h[j].used) } // newer = "larger"
+func (h maxHeap) Swap(i, j int)      { h[i], h[j] = h[j], h[i] }
+
+func (h *maxHeap) Push(x any) {
+	*h = append(*h, x.(*heapEntry))
+}
+
+func (h *maxHeap) Pop() any {
+	old := *h
+	n := len(old)
+	x := old[n-1]
+	*h = old[:n-1]
+	return x
+}
+
+func nOldest(arr []*heapEntry, n int) []*heapEntry {
+	if n <= 0 {
+		return []*heapEntry{}
+	}
+
+	if n >= len(arr) {
+		return arr
+	}
+
+	h := maxHeap(arr[:n])
+	heap.Init(&h)
+
+	for _, entry := range arr[n:] {
+		// swap in oldest, re-heapify
+		if entry.used.Before(h[0].used) {
+			h[0] = entry
+			heap.Fix(&h, 0)
+		}
+	}
+
+	return []*heapEntry(h)
+}
+
+type lruStringBank struct {
+	lock sync.Mutex
+	stop chan struct{}
+	m    map[string]*lruEntry
+}
+
+func NewLruStringBank(capacity int, evictionInterval time.Duration) StringBank {
+	stop := make(chan struct{})
+	bank := &lruStringBank{
+		m: make(map[string]*lruEntry),
+	}
+
+	go func() {
+		for {
+			select {
+			case <-stop:
+				return
+			case <-time.After(evictionInterval):
+			}
+
+			// need to take the lock during eviction
+			bank.lock.Lock()
+			if len(bank.m) <= capacity {
+				bank.lock.Unlock()
+				continue
+			}
+
+			// we collect a list of all lru entries so we can max heap the first n elements
+			arr := make([]*heapEntry, 0, len(bank.m))
+			for k, v := range bank.m {
+				arr = append(arr, &heapEntry{key: k, lruEntry: v})
+			}
+
+			oldest := nOldest(arr, len(bank.m)-capacity)
+			for _, old := range oldest {
+				delete(bank.m, old.key)
+			}
+			bank.lock.Unlock()
+		}
+	}()
+
+	return bank
+}
+
+func (sb *lruStringBank) Stop() {
+	sb.lock.Lock()
+	defer sb.lock.Unlock()
+
+	if sb.stop != nil {
+		close(sb.stop)
+		sb.stop = nil
+	}
+}
+
+func (sb *lruStringBank) LoadOrStore(key, value string) (string, bool) {
+	sb.lock.Lock()
+
+	if v, ok := sb.m[key]; ok {
+		v.used = time.Now().UTC()
+		sb.lock.Unlock()
+		return v.value, ok
+	}
+
+	sb.m[key] = &lruEntry{
+		value: value,
+		used:  time.Now().UTC(),
+	}
+	sb.lock.Unlock()
+	return value, false
+}
+
+func (sb *lruStringBank) LoadOrStoreFunc(key string, f func() string) (string, bool) {
+	sb.lock.Lock()
+
+	if v, ok := sb.m[key]; ok {
+		v.used = time.Now().UTC()
+		sb.lock.Unlock()
+		return v.value, ok
+	}
+
+	// create the key and value using the func (the key could be deallocated later)
+	value := f()
+	sb.m[key] = &lruEntry{
+		value: value,
+		used:  time.Now().UTC(),
+	}
+	sb.lock.Unlock()
+	return value, false
+}
+
+func (sb *lruStringBank) Clear() {
+	sb.lock.Lock()
+	sb.m = make(map[string]*lruEntry)
+	sb.lock.Unlock()
+}

+ 405 - 0
core/pkg/util/stringutil/lrubank_test.go

@@ -0,0 +1,405 @@
+package stringutil
+
+import (
+	"fmt"
+	"sync"
+	"testing"
+	"time"
+)
+
+func TestBasicLruEvict(t *testing.T) {
+	lruBank := NewLruStringBank(3, 2*time.Second).(*lruStringBank)
+	defer lruBank.Stop()
+
+	lruBank.LoadOrStore("foo", "foo")
+	time.Sleep(500 * time.Millisecond)
+	lruBank.LoadOrStore("bar", "bar")
+	time.Sleep(500 * time.Millisecond)
+	lruBank.LoadOrStore("whaz", "whaz")
+	time.Sleep(500 * time.Millisecond)
+	// access foo, updating recency
+	lruBank.LoadOrStore("foo", "foo")
+	// should push bar out after eviction runs
+	lruBank.LoadOrStore("test", "test")
+	time.Sleep(time.Second)
+
+	lruBank.lock.Lock()
+	for _, v := range lruBank.m {
+		t.Logf("Value: %s\n", v.value)
+		if v.value == "bar" {
+			t.Errorf("The 'bar' entry should've been replaced by 'test'")
+		}
+	}
+}
+
+// ---------------------------------------------------------------------------
+// LoadOrStore
+// ---------------------------------------------------------------------------
+
+// A stored value must be retrievable and LoadOrStore must signal the hit/miss
+// correctly via the boolean return.
+func TestLoadOrStore_MissAndHit(t *testing.T) {
+	bank := NewLruStringBank(10, time.Minute).(*lruStringBank)
+	defer bank.Stop()
+
+	v, loaded := bank.LoadOrStore("k", "hello")
+	if loaded {
+		t.Errorf("first LoadOrStore: expected loaded=false, got true")
+	}
+	if v != "hello" {
+		t.Errorf("first LoadOrStore: expected value %q, got %q", "hello", v)
+	}
+
+	v, loaded = bank.LoadOrStore("k", "world")
+	if !loaded {
+		t.Errorf("second LoadOrStore: expected loaded=true, got false")
+	}
+	// The original value must be returned on a hit, not the new candidate.
+	if v != "hello" {
+		t.Errorf("second LoadOrStore: expected cached value %q, got %q", "hello", v)
+	}
+}
+
+// Hitting an existing entry must update its recency so it is not evicted ahead
+// of entries that were never touched again.
+func TestLoadOrStore_HitUpdateRecency(t *testing.T) {
+	bank := NewLruStringBank(2, 500*time.Millisecond).(*lruStringBank)
+	defer bank.Stop()
+
+	bank.LoadOrStore("old", "old")
+	time.Sleep(100 * time.Millisecond)
+	bank.LoadOrStore("keep", "keep")
+	time.Sleep(100 * time.Millisecond)
+
+	// Re-touch "old" so it becomes the most-recently-used.
+	bank.LoadOrStore("old", "old")
+	time.Sleep(100 * time.Millisecond)
+
+	// Adding a third entry exceeds capacity; "keep" should be the oldest now.
+	bank.LoadOrStore("new", "new")
+
+	// Wait for the eviction goroutine.
+	time.Sleep(600 * time.Millisecond)
+
+	bank.lock.Lock()
+	defer bank.lock.Unlock()
+
+	if _, ok := bank.m["keep"]; ok {
+		t.Error("expected 'keep' to be evicted but it is still present")
+	}
+	if _, ok := bank.m["old"]; !ok {
+		t.Error("expected 'old' to survive eviction after its recency was refreshed")
+	}
+}
+
+// ---------------------------------------------------------------------------
+// LoadOrStoreFunc
+// ---------------------------------------------------------------------------
+
+// The factory function must only be called on a cache miss, not on a hit.
+func TestLoadOrStoreFunc_FactoryCalledOnMissOnly(t *testing.T) {
+	bank := NewLruStringBank(10, time.Minute).(*lruStringBank)
+	defer bank.Stop()
+	calls := 0
+
+	factory := func() string {
+		calls++
+		return "generated"
+	}
+
+	bank.LoadOrStoreFunc("k", factory)
+	bank.LoadOrStoreFunc("k", factory)
+
+	if calls != 1 {
+		t.Errorf("factory should be called exactly once, got %d calls", calls)
+	}
+}
+
+// Regression test: LoadOrStoreFunc stores entries under the *value* key, not
+// the *key* argument.  Subsequent lookups by the original key will therefore
+// miss every time.  This test documents the behaviour so any fix is caught
+// immediately.
+func TestLoadOrStoreFunc_KeyBug(t *testing.T) {
+	bank := NewLruStringBank(10, time.Minute).(*lruStringBank)
+	defer bank.Stop()
+
+	// First call: miss → stores entry under value "v", not key "k".
+	v, loaded := bank.LoadOrStoreFunc("k", func() string { return "v" })
+	if loaded {
+		t.Errorf("first call: expected loaded=false")
+	}
+	if v != "v" {
+		t.Errorf("first call: expected value %q, got %q", "v", v)
+	}
+
+	// Second call with the same key: because the entry was stored under "v",
+	// looking up "k" misses again.  This is the bug: loaded should be true
+	// but the current implementation returns false.
+	_, loaded = bank.LoadOrStoreFunc("k", func() string { return "v" })
+	if !loaded {
+		t.Errorf("LoadOrStoreFunc second call returned loaded=false")
+	}
+}
+
+// ---------------------------------------------------------------------------
+// Capacity / eviction
+// ---------------------------------------------------------------------------
+
+// If the bank never exceeds capacity, nothing should be evicted.
+func TestEviction_BelowCapacityNoEviction(t *testing.T) {
+	const capacity = 5
+	bank := NewLruStringBank(capacity, 200*time.Millisecond).(*lruStringBank)
+	defer bank.Stop()
+
+	for i := 0; i < capacity; i++ {
+		bank.LoadOrStore(fmt.Sprintf("k%d", i), fmt.Sprintf("v%d", i))
+	}
+
+	// Wait several eviction cycles.
+	time.Sleep(600 * time.Millisecond)
+
+	bank.lock.Lock()
+	defer bank.lock.Unlock()
+
+	if got := len(bank.m); got != capacity {
+		t.Errorf("expected %d entries, got %d", capacity, got)
+	}
+}
+
+// After eviction the map must be trimmed down to exactly capacity.
+func TestEviction_ExceedCapacityTrimsToCapacity(t *testing.T) {
+	const capacity = 3
+	bank := NewLruStringBank(capacity, 350*time.Millisecond).(*lruStringBank)
+	defer bank.Stop()
+
+	for i := 0; i < capacity+3; i++ {
+		bank.LoadOrStore(fmt.Sprintf("k%d", i), fmt.Sprintf("v%d", i))
+		time.Sleep(20 * time.Millisecond) // ensure distinct timestamps
+	}
+
+	// Wait for eviction.
+	time.Sleep(500 * time.Millisecond)
+
+	bank.lock.Lock()
+	defer bank.lock.Unlock()
+
+	if got := len(bank.m); got > capacity {
+		t.Errorf("expected at most %d entries after eviction, got %d", capacity, got)
+	}
+}
+
+// The most-recently-used entries must survive eviction.
+func TestEviction_MRUSurvives(t *testing.T) {
+	const capacity = 2
+	bank := NewLruStringBank(capacity, 300*time.Millisecond).(*lruStringBank)
+	defer bank.Stop()
+
+	bank.LoadOrStore("evict1", "evict1")
+	time.Sleep(50 * time.Millisecond)
+	bank.LoadOrStore("evict2", "evict2")
+	time.Sleep(50 * time.Millisecond)
+
+	// These two are the most recent; they must survive.
+	bank.LoadOrStore("keep1", "keep1")
+	time.Sleep(50 * time.Millisecond)
+	bank.LoadOrStore("keep2", "keep2")
+
+	time.Sleep(500 * time.Millisecond)
+
+	bank.lock.Lock()
+	defer bank.lock.Unlock()
+
+	for _, must := range []string{"keep1", "keep2"} {
+		if _, ok := bank.m[must]; !ok {
+			t.Errorf("expected %q to survive eviction", must)
+		}
+	}
+}
+
+// ---------------------------------------------------------------------------
+// Clear
+// ---------------------------------------------------------------------------
+
+func TestClear_EmptiesMap(t *testing.T) {
+	bank := NewLruStringBank(10, time.Minute).(*lruStringBank)
+	defer bank.Stop()
+
+	for i := 0; i < 5; i++ {
+		bank.LoadOrStore(fmt.Sprintf("k%d", i), fmt.Sprintf("v%d", i))
+	}
+
+	bank.Clear()
+
+	bank.lock.Lock()
+	defer bank.lock.Unlock()
+
+	if len(bank.m) != 0 {
+		t.Errorf("expected empty map after Clear, got %d entries", len(bank.m))
+	}
+}
+
+// After a Clear, previously stored keys must not be found.
+func TestClear_PreviousKeysGone(t *testing.T) {
+	bank := NewLruStringBank(10, time.Minute).(*lruStringBank)
+	defer bank.Stop()
+
+	bank.LoadOrStore("hello", "world")
+	bank.Clear()
+
+	_, loaded := bank.LoadOrStore("hello", "new")
+	if loaded {
+		t.Error("expected key to be absent after Clear, but it was found")
+	}
+}
+
+// ---------------------------------------------------------------------------
+// nOldest helper
+// ---------------------------------------------------------------------------
+
+func TestNOldest_ReturnsCorrectCount(t *testing.T) {
+	now := time.Now()
+	entries := []*heapEntry{
+		{key: "a", lruEntry: &lruEntry{value: "a", used: now.Add(-4 * time.Second)}},
+		{key: "b", lruEntry: &lruEntry{value: "b", used: now.Add(-3 * time.Second)}},
+		{key: "c", lruEntry: &lruEntry{value: "c", used: now.Add(-2 * time.Second)}},
+		{key: "d", lruEntry: &lruEntry{value: "d", used: now.Add(-1 * time.Second)}},
+		{key: "e", lruEntry: &lruEntry{value: "e", used: now}},
+	}
+
+	oldest := nOldest(entries, 2)
+	if len(oldest) != 2 {
+		t.Fatalf("expected 2 oldest entries, got %d", len(oldest))
+	}
+
+	values := map[string]bool{}
+	for _, e := range oldest {
+		values[e.value] = true
+	}
+	for _, must := range []string{"a", "b"} {
+		if !values[must] {
+			t.Errorf("expected %q in oldest set, got %v", must, values)
+		}
+	}
+}
+
+func TestNOldest_NGreaterThanLen(t *testing.T) {
+	now := time.Now()
+	entries := []*heapEntry{
+		{key: "x", lruEntry: &lruEntry{value: "x", used: now}},
+		{key: "y", lruEntry: &lruEntry{value: "y", used: now.Add(-time.Second)}},
+	}
+
+	result := nOldest(entries, 10)
+	if len(result) != 2 {
+		t.Errorf("expected all %d entries when n >= len, got %d", 2, len(result))
+	}
+}
+
+func TestNOldest_NEqualsLen(t *testing.T) {
+	now := time.Now()
+	entries := []*heapEntry{
+		{key: "x", lruEntry: &lruEntry{value: "x", used: now}},
+		{key: "y", lruEntry: &lruEntry{value: "y", used: now.Add(-time.Second)}},
+	}
+
+	result := nOldest(entries, 2)
+	if len(result) != 2 {
+		t.Errorf("expected 2 entries when n == len, got %d", len(result))
+	}
+}
+
+func TestNOldest_NIsZero(t *testing.T) {
+	now := time.Now()
+	entries := []*heapEntry{
+		{key: "x", lruEntry: &lruEntry{value: "x", used: now}},
+	}
+
+	result := nOldest(entries, 0)
+	if len(result) != 0 {
+		t.Errorf("expected 0 entries when n=0, got %d", len(result))
+	}
+}
+
+// ---------------------------------------------------------------------------
+// Concurrency
+// ---------------------------------------------------------------------------
+
+// Concurrent LoadOrStore calls must not race or panic.
+func TestConcurrentLoadOrStore(t *testing.T) {
+	bank := NewLruStringBank(50, 100*time.Millisecond).(*lruStringBank)
+	defer bank.Stop()
+
+	const goroutines = 20
+	const opsEach = 100
+
+	var wg sync.WaitGroup
+	wg.Add(goroutines)
+
+	for g := 0; g < goroutines; g++ {
+		g := g
+		go func() {
+			defer wg.Done()
+			for i := 0; i < opsEach; i++ {
+				key := fmt.Sprintf("k%d", (g*opsEach+i)%30)
+				bank.LoadOrStore(key, key)
+			}
+		}()
+	}
+
+	wg.Wait()
+}
+
+// Concurrent calls interleaved with eviction cycles must not deadlock or race.
+func TestConcurrentLoadOrStoreWithEviction(t *testing.T) {
+	bank := NewLruStringBank(5, 50*time.Millisecond).(*lruStringBank)
+	defer bank.Stop()
+
+	const goroutines = 10
+	const duration = 300 * time.Millisecond
+
+	var wg sync.WaitGroup
+	stop := time.After(duration)
+
+	for i := 0; i < goroutines; i++ {
+		g := i
+		wg.Add(1)
+		go func() {
+			defer wg.Done()
+			for {
+				select {
+				case <-stop:
+					return
+				default:
+					key := fmt.Sprintf("g%d", g)
+					bank.LoadOrStore(key, key)
+				}
+			}
+		}()
+	}
+
+	wg.Wait()
+}
+
+// Concurrent Clear calls alongside reads/writes must not panic.
+func TestConcurrentClear(t *testing.T) {
+	bank := NewLruStringBank(10, time.Minute).(*lruStringBank)
+	defer bank.Stop()
+
+	var wg sync.WaitGroup
+	for i := 0; i < 5; i++ {
+		wg.Add(1)
+		go func(i int) {
+			defer wg.Done()
+			bank.LoadOrStore(fmt.Sprintf("k%d", i), "v")
+		}(i)
+	}
+	for i := 0; i < 3; i++ {
+		wg.Add(1)
+		go func() {
+			defer wg.Done()
+			bank.Clear()
+		}()
+	}
+
+	wg.Wait()
+}

+ 48 - 0
core/pkg/util/stringutil/mapbank.go

@@ -0,0 +1,48 @@
+package stringutil
+
+import "sync"
+
+type stringBank struct {
+	lock sync.Mutex
+	m    map[string]string
+}
+
+func newStringBank() *stringBank {
+	return &stringBank{
+		m: make(map[string]string),
+	}
+}
+
+func (sb *stringBank) LoadOrStore(key, value string) (string, bool) {
+	sb.lock.Lock()
+
+	if v, ok := sb.m[key]; ok {
+		sb.lock.Unlock()
+		return v, ok
+	}
+
+	sb.m[key] = value
+	sb.lock.Unlock()
+	return value, false
+}
+
+func (sb *stringBank) LoadOrStoreFunc(key string, f func() string) (string, bool) {
+	sb.lock.Lock()
+
+	if v, ok := sb.m[key]; ok {
+		sb.lock.Unlock()
+		return v, ok
+	}
+
+	// create the key and value using the func (the key could be deallocated later)
+	value := f()
+	sb.m[key] = value
+	sb.lock.Unlock()
+	return value, false
+}
+
+func (sb *stringBank) Clear() {
+	sb.lock.Lock()
+	sb.m = make(map[string]string)
+	sb.lock.Unlock()
+}

+ 17 - 0
core/pkg/util/stringutil/noopbank.go

@@ -0,0 +1,17 @@
+package stringutil
+
+type noOpStringBank struct{}
+
+func newNoOpStringBank() *noOpStringBank {
+	return new(noOpStringBank)
+}
+
+func (nsb *noOpStringBank) LoadOrStore(key, value string) (string, bool) {
+	return value, true
+}
+
+func (nsb *noOpStringBank) LoadOrStoreFunc(key string, f func() string) (string, bool) {
+	return f(), true
+}
+
+func (nsb *noOpStringBank) Clear() {}

+ 27 - 44
core/pkg/util/stringutil/stringutil.go

@@ -23,75 +23,58 @@ const (
 var alpha = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
 var alphanumeric = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
 
-type stringBank struct {
-	lock sync.Mutex
-	m    map[string]string
+type StringBank interface {
+	LoadOrStore(key, value string) (string, bool)
+	LoadOrStoreFunc(key string, f func() string) (string, bool)
+	Clear()
 }
 
-func newStringBank() *stringBank {
-	return &stringBank{
-		m: make(map[string]string),
-	}
-}
+var (
+	lock sync.RWMutex
 
-func (sb *stringBank) LoadOrStore(key, value string) (string, bool) {
-	sb.lock.Lock()
-
-	if v, ok := sb.m[key]; ok {
-		sb.lock.Unlock()
-		return v, ok
-	}
+	// stringBank is an unbounded string cache that is thread-safe. It is especially useful if
+	// storing a large frequency of dynamically allocated duplicate strings.
+	strings StringBank = newStringBank() // sync.Map
+)
 
-	sb.m[key] = value
-	sb.lock.Unlock()
-	return value, false
+func init() {
+	rand.Seed(time.Now().UnixNano())
 }
 
-func (sb *stringBank) LoadOrStoreFunc(key string, f func() string) (string, bool) {
-	sb.lock.Lock()
+func UpdateStringBank(sb StringBank) {
+	lock.Lock()
+	defer lock.Unlock()
 
-	if v, ok := sb.m[key]; ok {
-		sb.lock.Unlock()
-		return v, ok
-	}
-
-	// create the key and value using the func (the key could be deallocated later)
-	value := f()
-	sb.m[value] = value
-	sb.lock.Unlock()
-	return value, false
-}
-
-func (sb *stringBank) Clear() {
-	sb.lock.Lock()
-	sb.m = make(map[string]string)
-	sb.lock.Unlock()
+	strings.Clear()
+	strings = sb
 }
 
-// stringBank is an unbounded string cache that is thread-safe. It is especially useful if
-// storing a large frequency of dynamically allocated duplicate strings.
-var strings = newStringBank() // sync.Map
+// GetStringBank returns the _current_ StringBank implementation. Note that the read-lock is
+// not held for the duration of usage, so the returned string bank could be swapped out
+// after being retrieved.
+func GetStringBank() StringBank {
+	lock.RLock()
+	defer lock.RUnlock()
 
-func init() {
-	rand.Seed(time.Now().UnixNano())
+	return strings
 }
 
 // Bank will return a non-copy of a string if it has been used before. Otherwise, it will store
 // the string as the unique instance.
 func Bank(s string) string {
-	ss, _ := strings.LoadOrStore(s, s)
+	ss, _ := GetStringBank().LoadOrStore(s, s)
 	return ss
 }
 
 // BankFunc will use the provided s string to check for an existing allocation of the string. However,
 // if no allocation exists, the f parameter will be used to create the string and store in the bank.
 func BankFunc(s string, f func() string) string {
-	ss, _ := strings.LoadOrStoreFunc(s, f)
+	ss, _ := GetStringBank().LoadOrStoreFunc(s, f)
 	return ss
 }
 
 func ClearBank() {
-	strings.Clear()
+	GetStringBank().Clear()
 }
 
 // RandSeq generates a pseudo-random alphabetic string of the given length

+ 64 - 0
core/pkg/util/stringutil/stringutil_test.go

@@ -6,6 +6,7 @@ import (
 	"strings"
 	"sync"
 	"testing"
+	"time"
 
 	"github.com/opencost/opencost/core/pkg/util/stringutil"
 )
@@ -153,3 +154,66 @@ func BenchmarkStringBankFunc25PercentDuplicate(b *testing.B) {
 func BenchmarkStringBankFuncNoDuplicate(b *testing.B) {
 	benchmarkStringBank(b, standardBankTest, 1_000_000, 1_000_000, true)
 }
+
+const LruCapacity = 500_000
+const LruEvictInterval = 5 * time.Second
+
+func BenchmarkLruStringBankFunc90PercentDuplicate(b *testing.B) {
+	sb := stringutil.NewLruStringBank(LruCapacity, LruEvictInterval)
+	defer func() {
+		if lruBank, ok := sb.(interface{ Stop() }); ok {
+			lruBank.Stop()
+		}
+	}()
+
+	stringutil.UpdateStringBank(sb)
+	benchmarkStringBank(b, standardBankTest, 1_000_000, 100_000, true)
+}
+
+func BenchmarkLruStringBankFunc75PercentDuplicate(b *testing.B) {
+	sb := stringutil.NewLruStringBank(LruCapacity, LruEvictInterval)
+	defer func() {
+		if lruBank, ok := sb.(interface{ Stop() }); ok {
+			lruBank.Stop()
+		}
+	}()
+
+	stringutil.UpdateStringBank(sb)
+	benchmarkStringBank(b, standardBankTest, 1_000_000, 250_000, true)
+}
+
+func BenchmarkLruStringBankFunc50PercentDuplicate(b *testing.B) {
+	sb := stringutil.NewLruStringBank(LruCapacity, LruEvictInterval)
+	defer func() {
+		if lruBank, ok := sb.(interface{ Stop() }); ok {
+			lruBank.Stop()
+		}
+	}()
+
+	stringutil.UpdateStringBank(sb)
+	benchmarkStringBank(b, standardBankTest, 1_000_000, 100_000, true)
+}
+
+func BenchmarkLruStringBankFunc25PercentDuplicate(b *testing.B) {
+	sb := stringutil.NewLruStringBank(LruCapacity, LruEvictInterval)
+	defer func() {
+		if lruBank, ok := sb.(interface{ Stop() }); ok {
+			lruBank.Stop()
+		}
+	}()
+
+	stringutil.UpdateStringBank(sb)
+	benchmarkStringBank(b, standardBankTest, 1_000_000, 750_000, true)
+}
+
+func BenchmarkLruStringBankFuncNoDuplicate(b *testing.B) {
+	sb := stringutil.NewLruStringBank(LruCapacity, LruEvictInterval)
+	defer func() {
+		if lruBank, ok := sb.(interface{ Stop() }); ok {
+			lruBank.Stop()
+		}
+	}()
+
+	stringutil.UpdateStringBank(sb)
+	benchmarkStringBank(b, standardBankTest, 1_000_000, 1_000_000, true)
+}