4
4
5
5
//go:build goexperiment.exectracer2
6
6
7
- // Simple hash table for tracing. Provides a mapping
8
- // between variable-length data and a unique ID. Subsequent
9
- // puts of the same data will return the same ID.
7
+ // Simple append-only thread-safe hash map for tracing.
8
+ // Provides a mapping between variable-length data and a
9
+ // unique ID. Subsequent puts of the same data will return
10
+ // the same ID. The zero value is ready to use.
10
11
//
11
- // Uses a region-based allocation scheme and assumes that the
12
- // table doesn't ever grow very big .
12
+ // Uses a region-based allocation scheme internally, and
13
+ // reset clears the whole map .
13
14
//
14
- // This is definitely not a general-purpose hash table! It avoids
15
- // doing any high-level Go operations so it's safe to use even in
16
- // sensitive contexts.
15
+ // It avoids doing any high-level Go operations so it's safe
16
+ // to use even in sensitive contexts.
17
17
18
18
package runtime
19
19
20
20
import (
21
+ "internal/cpu"
22
+ "internal/goarch"
21
23
"internal/runtime/atomic"
22
24
"runtime/internal/sys"
23
25
"unsafe"
24
26
)
25
27
26
28
type traceMap struct {
27
- lock mutex // Must be acquired on the system stack
29
+ root atomic.UnsafePointer // *traceMapNode (can't use generics because it's notinheap)
30
+ _ cpu.CacheLinePad
28
31
seq atomic.Uint64
32
+ _ cpu.CacheLinePad
29
33
mem traceRegionAlloc
30
- tab [1 << 13 ]atomic.UnsafePointer // *traceMapNode (can't use generics because it's notinheap)
31
34
}
32
35
36
+ // traceMapNode is an implementation of a lock-free append-only hash-trie
37
+ // (a trie of the hash bits).
38
+ //
39
+ // Key features:
40
+ // - 4-ary trie. Child nodes are indexed by the upper 2 (remaining) bits of the hash.
41
+ // For example, top level uses bits [63:62], next level uses [61:60] and so on.
42
+ // - New nodes are placed at the first empty level encountered.
43
+ // - When the first child is added to a node, the existing value is not moved into a child.
44
+ // This means that you must check the key at each level, not just at the leaf.
45
+ // - No deletion or rebalancing.
46
+ // - Intentionally devolves into a linked list on hash collisions (the hash bits will all
47
+ // get shifted out during iteration, and new nodes will just be appended to the 0th child).
33
48
type traceMapNode struct {
34
- _ sys.NotInHeap
35
- link atomic.UnsafePointer // *traceMapNode (can't use generics because it's notinheap)
36
- hash uintptr
37
- id uint64
38
- data []byte
39
- }
49
+ _ sys.NotInHeap
40
50
41
- // next is a type-safe wrapper around link.
42
- func (n * traceMapNode ) next () * traceMapNode {
43
- return (* traceMapNode )(n .link .Load ())
51
+ children [4 ]atomic.UnsafePointer // *traceMapNode (can't use generics because it's notinheap)
52
+ hash uintptr
53
+ id uint64
54
+ data []byte
44
55
}
45
56
46
57
// stealID steals an ID from the table, ensuring that it will not
@@ -51,7 +62,7 @@ func (tab *traceMap) stealID() uint64 {
51
62
52
63
// put inserts the data into the table.
53
64
//
54
- // It's always safe to noescape data because its bytes are always copied .
65
+ // It's always safe for callers to noescape data because put copies its bytes .
55
66
//
56
67
// Returns a unique ID for the data and whether this is the first time
57
68
// the data has been added to the map.
@@ -60,59 +71,47 @@ func (tab *traceMap) put(data unsafe.Pointer, size uintptr) (uint64, bool) {
60
71
return 0 , false
61
72
}
62
73
hash := memhash (data , 0 , size )
63
- // First, search the hashtable w/o the mutex.
64
- if id := tab .find (data , size , hash ); id != 0 {
65
- return id , false
66
- }
67
- // Now, double check under the mutex.
68
- // Switch to the system stack so we can acquire tab.lock
69
- var id uint64
70
- var added bool
71
- systemstack (func () {
72
- lock (& tab .lock )
73
- if id = tab .find (data , size , hash ); id != 0 {
74
- unlock (& tab .lock )
75
- return
76
- }
77
- // Create new record.
78
- id = tab .seq .Add (1 )
79
- vd := tab .newTraceMapNode (data , size , hash , id )
80
74
81
- // Insert it into the table.
82
- //
83
- // Update the link first, since the node isn't published yet.
84
- // Then, store the node in the table as the new first node
85
- // for the bucket.
86
- part := int (hash % uintptr (len (tab .tab )))
87
- vd .link .StoreNoWB (tab .tab [part ].Load ())
88
- tab .tab [part ].StoreNoWB (unsafe .Pointer (vd ))
89
- unlock (& tab .lock )
90
-
91
- added = true
92
- })
93
- return id , added
94
- }
95
-
96
- // find looks up data in the table, assuming hash is a hash of data.
97
- //
98
- // Returns 0 if the data is not found, and the unique ID for it if it is.
99
- func (tab * traceMap ) find (data unsafe.Pointer , size , hash uintptr ) uint64 {
100
- part := int (hash % uintptr (len (tab .tab )))
101
- for vd := tab .bucket (part ); vd != nil ; vd = vd .next () {
102
- // Synchronization not necessary. Once published to the table, these
103
- // values are immutable.
104
- if vd .hash == hash && uintptr (len (vd .data )) == size {
105
- if memequal (unsafe .Pointer (& vd .data [0 ]), data , size ) {
106
- return vd .id
75
+ var newNode * traceMapNode
76
+ m := & tab .root
77
+ hashIter := hash
78
+ for {
79
+ n := (* traceMapNode )(m .Load ())
80
+ if n == nil {
81
+ // Try to insert a new map node. We may end up discarding
82
+ // this node if we fail to insert because it turns out the
83
+ // value is already in the map.
84
+ //
85
+ // The discard will only happen if two threads race on inserting
86
+ // the same value. Both might create nodes, but only one will
87
+ // succeed on insertion. If two threads race to insert two
88
+ // different values, then both nodes will *always* get inserted,
89
+ // because the equality checking below will always fail.
90
+ //
91
+ // Performance note: contention on insertion is likely to be
92
+ // higher for small maps, but since this data structure is
93
+ // append-only, either the map stays small because there isn't
94
+ // much activity, or the map gets big and races to insert on
95
+ // the same node are much less likely.
96
+ if newNode == nil {
97
+ newNode = tab .newTraceMapNode (data , size , hash , tab .seq .Add (1 ))
98
+ }
99
+ if m .CompareAndSwapNoWB (nil , unsafe .Pointer (newNode )) {
100
+ return newNode .id , true
101
+ }
102
+ // Reload n. Because pointers are only stored once,
103
+ // we must have lost the race, and therefore n is not nil
104
+ // anymore.
105
+ n = (* traceMapNode )(m .Load ())
106
+ }
107
+ if n .hash == hash && uintptr (len (n .data )) == size {
108
+ if memequal (unsafe .Pointer (& n .data [0 ]), data , size ) {
109
+ return n .id , false
107
110
}
108
111
}
112
+ m = & n .children [hashIter >> (8 * goarch .PtrSize - 2 )]
113
+ hashIter <<= 2
109
114
}
110
- return 0
111
- }
112
-
113
- // bucket is a type-safe wrapper for looking up a value in tab.tab.
114
- func (tab * traceMap ) bucket (part int ) * traceMapNode {
115
- return (* traceMapNode )(tab .tab [part ].Load ())
116
115
}
117
116
118
117
func (tab * traceMap ) newTraceMapNode (data unsafe.Pointer , size , hash uintptr , id uint64 ) * traceMapNode {
@@ -134,18 +133,10 @@ func (tab *traceMap) newTraceMapNode(data unsafe.Pointer, size, hash uintptr, id
134
133
135
134
// reset drops all allocated memory from the table and resets it.
136
135
//
137
- // tab.lock must be held. Must run on the system stack because of this.
138
- //
139
- //go:systemstack
136
+ // The caller must ensure that there are no put operations executing concurrently
137
+ // with this function.
140
138
func (tab * traceMap ) reset () {
141
- assertLockHeld (& tab .lock )
142
- tab .mem .drop ()
139
+ tab .root .Store (nil )
143
140
tab .seq .Store (0 )
144
- // Clear table without write barriers. The table consists entirely
145
- // of notinheap pointers, so this is fine.
146
- //
147
- // Write barriers may theoretically call into the tracer and acquire
148
- // the lock again, and this lock ordering is expressed in the static
149
- // lock ranking checker.
150
- memclrNoHeapPointers (unsafe .Pointer (& tab .tab ), unsafe .Sizeof (tab .tab ))
141
+ tab .mem .drop ()
151
142
}
0 commit comments