1
+ // Package decoder decodes values in the data section.
1
2
package decoder
2
3
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
+ )
15
7
8
+ // cacheEntry represents a cached string with its offset and dedicated mutex.
16
9
type cacheEntry struct {
17
10
str string
18
11
offset uint
12
+ mu sync.RWMutex
13
+ }
14
+
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
19
}
20
20
21
- // newStringCache creates a new bounded string cache.
21
+ // newStringCache creates a new per-entry mutex-based string cache.
22
22
func newStringCache () * stringCache {
23
23
return & stringCache {}
24
24
}
25
25
26
26
// 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.
29
28
func (sc * stringCache ) internAt (offset , size uint , data []byte ) string {
30
29
const (
31
30
minCachedLen = 2 // single byte strings not worth caching
@@ -37,30 +36,28 @@ func (sc *stringCache) internAt(offset, size uint, data []byte) string {
37
36
return string (data [offset : offset + size ])
38
37
}
39
38
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 ]
42
42
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 != "" {
47
46
str := entry .str
48
- sc .mu .RUnlock ()
47
+ entry .mu .RUnlock ()
49
48
return str
50
49
}
51
- sc .mu .RUnlock ()
50
+ entry .mu .RUnlock ()
52
51
53
- // Cache miss - create new string and store with write lock
52
+ // Cache miss - create new string
54
53
str := string (data [offset : offset + size ])
55
54
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 ()
64
60
65
61
return str
66
62
}
63
+
0 commit comments