1+ // Package decoder decodes values in the data section.
12package decoder
23
3- import "sync"
4-
5- // stringCache provides bounded string interning using offset-based indexing.
6- // Similar to encoding/json/v2's intern.go but uses offsets instead of hashing.
7- // Thread-safe for concurrent use.
8- type stringCache struct {
9- // Fixed-size cache to prevent unbounded memory growth
10- // Using 512 entries for 8KiB total memory footprint (512 * 16 bytes per string)
11- cache [512 ]cacheEntry
12- // RWMutex for thread safety - allows concurrent reads, exclusive writes
13- mu sync.RWMutex
14- }
4+ import (
5+ "sync"
6+ )
157
8+ // cacheEntry represents a cached string with its offset and dedicated mutex.
169type cacheEntry struct {
1710 str string
1811 offset uint
12+ mu sync.RWMutex
1913}
2014
21- // newStringCache creates a new bounded string cache.
15+ // stringCache provides bounded string interning with per-entry mutexes for minimal contention.
16+ // This achieves thread safety while avoiding the global lock bottleneck.
17+ type stringCache struct {
18+ entries [512 ]cacheEntry
19+ }
20+
21+ // newStringCache creates a new per-entry mutex-based string cache.
2222func newStringCache () * stringCache {
2323 return & stringCache {}
2424}
2525
2626// internAt returns a canonical string for the data at the given offset and size.
27- // Uses the offset modulo cache size as the index, similar to json/v2's approach.
28- // Thread-safe for concurrent use.
27+ // Uses per-entry RWMutex for fine-grained thread safety with minimal contention.
2928func (sc * stringCache ) internAt (offset , size uint , data []byte ) string {
3029 const (
3130 minCachedLen = 2 // single byte strings not worth caching
@@ -37,30 +36,27 @@ func (sc *stringCache) internAt(offset, size uint, data []byte) string {
3736 return string (data [offset : offset + size ])
3837 }
3938
40- // Use offset as cache index (modulo cache size)
41- i := offset % uint (len (sc .cache ))
39+ // Use same cache index calculation as original: offset % cacheSize
40+ i := offset % uint (len (sc .entries ))
41+ entry := & sc .entries [i ]
4242
43- // Fast path: check for cache hit with read lock
44- sc .mu .RLock ()
45- entry := sc .cache [i ]
46- if entry .offset == offset && len (entry .str ) == int (size ) {
43+ // Fast path: read lock and check
44+ entry .mu .RLock ()
45+ if entry .offset == offset && entry .str != "" {
4746 str := entry .str
48- sc .mu .RUnlock ()
47+ entry .mu .RUnlock ()
4948 return str
5049 }
51- sc .mu .RUnlock ()
50+ entry .mu .RUnlock ()
5251
53- // Cache miss - create new string and store with write lock
52+ // Cache miss - create new string
5453 str := string (data [offset : offset + size ])
5554
56- sc .mu .Lock ()
57- // Double-check in case another goroutine added it while we were waiting
58- if sc .cache [i ].offset == offset && len (sc .cache [i ].str ) == int (size ) {
59- str = sc .cache [i ].str
60- } else {
61- sc .cache [i ] = cacheEntry {offset : offset , str : str }
62- }
63- sc .mu .Unlock ()
55+ // Store with write lock on this specific entry
56+ entry .mu .Lock ()
57+ entry .offset = offset
58+ entry .str = str
59+ entry .mu .Unlock ()
6460
6561 return str
6662}
0 commit comments