Skip to content

Commit a4fca73

Browse files
committed
Make walking up the tree cheap enough to inline
Checker code walks up the tree constantly, mostly through FindAncestor with a closure. On main, FindAncestor is inlined into its callers, which then call the closure directly; here, reading a parent cost too much for that, so every walk went through an extra call. Two encoding details made reading a ref expensive. A zero ref needed its own test before the near case, and the chunk offset in the header was in words and had to be scaled. Marking near refs with the low bit and keeping the absent node in the first far slot lets zero fall into the far case with no test, and storing the chunk offset in bytes drops the scaling. With Parent written out directly, FindAncestor and FindAncestorKind fit the inlining budget again. │ old.txt │ new.txt │ │ check-sec │ check-sec vs base │ VSCode 7.275 ± 0% 7.160 ± 1% -1.58% (p=0.000 n=8) │ old.txt │ new.txt │ │ total-sec │ total-sec vs base │ VSCode 8.096 ± 0% 7.969 ± 1% -1.57% (p=0.000 n=8) │ old.txt │ new.txt │ │ mem-B │ mem-B vs base │ VSCode 3.207Gi ± 0% 3.208Gi ± 0% ~ (p=0.505 n=8)
1 parent ca2bc7e commit a4fca73

5 files changed

Lines changed: 65 additions & 53 deletions

File tree

‎tsc/internal/ast/ast.go‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,18 @@ func (n Node) SetFlags(flags NodeFlags) { n.hdr().flags = flags }
177177
func (n Node) SetLoc(loc core.TextRange) { n.hdr().loc = loc }
178178
func (n Node) SetParent(parent Node) { n.setRef(&n.hdr().parent, parent) }
179179

180-
func (n Node) Parent() Node { return n.deref(&n.hdr().parent) }
180+
// Parent is deref of the parent field, written out so that loops walking up
181+
// the tree stay small enough to inline.
182+
func (n Node) Parent() Node {
183+
h := (*nodeBaseData)(n.p)
184+
r := h.parent
185+
if r&refNear != 0 {
186+
return Node{unsafe.Add(n.p, int(int32(r))+parentNearBias)}
187+
}
188+
return (*chunkHeader)(unsafe.Add(n.p, -int(h.chunk))).store.far[r>>1]
189+
}
190+
191+
const parentNearBias = int(unsafe.Offsetof(nodeBaseData{}.parent)) - refNear
181192

182193
// Shared node accessors dispatch on Kind to the enclosing concrete node.
183194

‎tsc/internal/ast/list.go‎

Lines changed: 16 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ type NodeList struct {
1616
}
1717

1818
type listHeader struct {
19-
// chunk is the record's offset in words from the start of its chunk.
19+
// chunk is the record's offset in bytes from the start of its chunk.
2020
chunk uint32
2121
n uint32
2222
pos, end int32
@@ -105,14 +105,11 @@ func (l NodeList) At(k int) Node {
105105

106106
func (l NodeList) at(k int) Node {
107107
f := l.slot(k)
108-
r := int32(*f)
109-
if r&refFar == 0 {
110-
if r == 0 {
111-
return Node{}
112-
}
113-
return Node{unsafe.Add(unsafe.Pointer(f), int(r)*2)}
108+
r := *f
109+
if r&refNear != 0 {
110+
return Node{unsafe.Add(unsafe.Pointer(f), int(int32(r))-refNear)}
114111
}
115-
return l.store().far[uint32(r)>>1]
112+
return l.store().far[r>>1]
116113
}
117114

118115
// SetAt replaces the node at index k of l.
@@ -221,20 +218,11 @@ func (l ModifierList) Clone(f *NodeFactory) ModifierList {
221218

222219
// derefList returns the list that the field f of n's record refers to.
223220
func (n Node) derefList(f *listRef) NodeList {
224-
r := int32(*f)
225-
if r&refFar == 0 {
226-
if r == 0 {
227-
return NodeList{}
228-
}
229-
return NodeList{unsafe.Add(unsafe.Pointer(f), int(r)*2)}
221+
r := *f
222+
if r&refNear != 0 {
223+
return NodeList{unsafe.Add(unsafe.Pointer(f), int(int32(r))-refNear)}
230224
}
231-
return n.store().farLists[uint32(r)>>1]
232-
}
233-
234-
// listAt returns the list that the field at offset off of n's record
235-
// refers to.
236-
func (n Node) listAt(off uint8) NodeList {
237-
return n.derefList((*listRef)(unsafe.Add(n.rec(), off)))
225+
return n.store().farLists[r>>1]
238226
}
239227

240228
func (n Node) derefModifiers(f *listRef) ModifierList {
@@ -264,10 +252,16 @@ func setListRef(c *chunkHeader, f *listRef, v NodeList) {
264252
return
265253
}
266254
s := c.store
267-
if *f&refFar != 0 {
255+
if *f != 0 && *f&refNear == 0 {
268256
s.farLists[*f>>1] = v
269257
return
270258
}
271259
*f = listRef(farSlot(len(s.farLists)))
272260
s.farLists = append(s.farLists, v)
273261
}
262+
263+
// listAt returns the list that the field at offset off of n's record
264+
// refers to.
265+
func (n Node) listAt(off uint8) NodeList {
266+
return n.derefList((*listRef)(unsafe.Add(n.rec(), off)))
267+
}

‎tsc/internal/ast/store.go‎

Lines changed: 33 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -72,10 +72,15 @@ type chunkHeader struct {
7272
_ uint32
7373
}
7474

75-
const chunkHeaderWords = uint32(unsafe.Sizeof(chunkHeader{}) / wordSize)
75+
const (
76+
chunkHeaderSize = uint32(unsafe.Sizeof(chunkHeader{}))
77+
chunkHeaderWords = chunkHeaderSize / uint32(wordSize)
78+
)
7679

7780
func newNodeStore() *nodeStore {
78-
return &nodeStore{}
81+
// The first far slots are the absent node and list, which zero refs
82+
// refer to.
83+
return &nodeStore{far: make([]Node, 1), farLists: make([]NodeList, 1)}
7984
}
8085

8186
// nodeBaseData is the header at the start of every node record.
@@ -84,21 +89,25 @@ type nodeBaseData struct {
8489
loc core.TextRange
8590
parent nodeRef
8691
flags NodeFlags
87-
// chunk is the record's offset in words from the start of its chunk.
92+
// chunk is the record's offset in bytes from the start of its chunk.
8893
chunk uint32
8994
kind Kind
9095
}
9196

9297
// Every node has a header, so it must stay small.
9398
var _ [32 - unsafe.Sizeof(nodeBaseData{})]struct{}
9499

95-
// A nodeRef is a record field referring to a node. Zero is no node. With
96-
// the low bit clear, the referenced record is in the same chunk, at the
97-
// field's address plus twice the ref, read as a signed number of bytes;
98-
// otherwise, the rest is an index into the store's far nodes.
100+
// A nodeRef is a record field referring to a node. With the low bit set,
101+
// the referenced record is in the same chunk, at the field's address plus
102+
// the rest of the ref, read as a signed number of bytes; otherwise, the
103+
// rest is an index into the store's far nodes, whose first is the absent
104+
// node, so that zero is no node.
105+
//
106+
// Records are word aligned and refs are four byte aligned, so the distance
107+
// from a ref to a record never has the low bit set.
99108
type nodeRef uint32
100109

101-
const refFar = 1
110+
const refNear = 1
102111

103112
const wordSize = unsafe.Sizeof(uint64(0))
104113

@@ -108,9 +117,9 @@ func (n Node) rec() unsafe.Pointer { return n.p }
108117
func (n Node) hdr() *nodeBaseData { return (*nodeBaseData)(n.p) }
109118

110119
// chunkOf returns the header of the chunk holding the record at p, which
111-
// is chunk words from the chunk's start.
120+
// is chunk bytes from the chunk's start.
112121
func chunkOf(p unsafe.Pointer, chunk uint32) *chunkHeader {
113-
return (*chunkHeader)(unsafe.Add(p, -int(chunk)*int(wordSize)))
122+
return (*chunkHeader)(unsafe.Add(p, -int(chunk)))
114123
}
115124

116125
func (n Node) chunk() *chunkHeader { return chunkOf(n.p, n.hdr().chunk) }
@@ -120,14 +129,11 @@ func (n Node) store() *nodeStore { return n.chunk().store }
120129

121130
// deref returns the node that the field f of n's record refers to.
122131
func (n Node) deref(f *nodeRef) Node {
123-
r := int32(*f)
124-
if r&refFar == 0 {
125-
if r == 0 {
126-
return Node{}
127-
}
128-
return Node{unsafe.Add(unsafe.Pointer(f), int(r)*2)}
132+
r := *f
133+
if r&refNear != 0 {
134+
return Node{unsafe.Add(unsafe.Pointer(f), int(int32(r))-refNear)}
129135
}
130-
return n.store().far[uint32(r)>>1]
136+
return n.store().far[r>>1]
131137
}
132138

133139
// refAt returns the node that the field at offset off of n's record refers
@@ -157,7 +163,7 @@ func setRef(c *chunkHeader, f *nodeRef, v Node) {
157163
return
158164
}
159165
s := c.store
160-
if *f&refFar != 0 {
166+
if *f != 0 && *f&refNear == 0 {
161167
// Slots are never shared, so the field's own slot can be reused.
162168
s.far[*f>>1] = v
163169
return
@@ -169,14 +175,14 @@ func setRef(c *chunkHeader, f *nodeRef, v Node) {
169175
// near returns the ref from the field at f to the record at p in the same
170176
// chunk.
171177
func near[F any](f *F, p unsafe.Pointer) uint32 {
172-
return uint32(int32((int64(uintptr(p)) - int64(uintptr(unsafe.Pointer(f)))) / 2))
178+
return uint32(int32(uintptr(p)-uintptr(unsafe.Pointer(f)))) | refNear
173179
}
174180

175181
func farSlot(i int) uint32 {
176182
if i >= 1<<31 {
177183
panic("node store far ref overflow")
178184
}
179-
return uint32(i)<<1 | refFar
185+
return uint32(i) << 1
180186
}
181187

182188
// A nodeAnchor refers to a node of a store from outside its records: the
@@ -196,8 +202,8 @@ func (s *nodeStore) anchor(v Node) nodeAnchor {
196202
if v.p == nil {
197203
return 0
198204
}
199-
if c := v.chunk(); c.store == s && c.index < maxAnchorChunks && v.hdr().chunk < maxChunkWords {
200-
return nodeAnchor((c.index+1)<<anchorChunkShift | v.hdr().chunk)
205+
if c, w := v.chunk(), v.hdr().chunk/uint32(wordSize); c.store == s && c.index < maxAnchorChunks && w < maxChunkWords {
206+
return nodeAnchor((c.index+1)<<anchorChunkShift | w)
201207
}
202208
s.far = append(s.far, v)
203209
if len(s.far) > anchorFar {
@@ -234,7 +240,7 @@ func newRecord[T any](s *nodeStore, layout nodeLayout) (*T, Node) {
234240
}
235241

236242
// alloc allocates size zeroed bytes for a record, returning its address and
237-
// its word offset in its chunk.
243+
// its byte offset in its chunk.
238244
func (s *nodeStore) alloc(size uintptr) (unsafe.Pointer, uint32) {
239245
if s.frozen {
240246
panic("node store is frozen")
@@ -245,7 +251,7 @@ func (s *nodeStore) alloc(size uintptr) (unsafe.Pointer, uint32) {
245251
}
246252
p := unsafe.Pointer(unsafe.SliceData(s.free))
247253
s.free = s.free[words:]
248-
return p, uint32((uintptr(p) - uintptr(s.base)) / wordSize)
254+
return p, uint32(uintptr(p) - uintptr(s.base))
249255
}
250256

251257
// grow allocates words in a new chunk. A record never spans two chunks.
@@ -258,12 +264,12 @@ func (s *nodeStore) grow(words int) (unsafe.Pointer, uint32) {
258264
// A record too large for a normal chunk gets one of its own, and
259265
// the current chunk stays in use.
260266
base, _ := s.addChunk(words)
261-
return unsafe.Add(base, chunkHeaderWords*uint32(wordSize)), chunkHeaderWords
267+
return unsafe.Add(base, chunkHeaderSize), chunkHeaderSize
262268
}
263269
base, all := s.addChunk(n)
264270
s.base = base
265271
s.free = all[words:]
266-
return unsafe.Pointer(unsafe.SliceData(all)), chunkHeaderWords
272+
return unsafe.Pointer(unsafe.SliceData(all)), chunkHeaderSize
267273
}
268274

269275
// addChunk allocates a chunk with room for at least n words of records,
@@ -274,7 +280,7 @@ func (s *nodeStore) addChunk(n int) (unsafe.Pointer, []uint64) {
274280
*(*chunkHeader)(base) = chunkHeader{store: s, index: uint32(len(s.chunks))}
275281
s.chunks = append(s.chunks, base)
276282
s.words += n
277-
return base, unsafe.Slice((*uint64)(unsafe.Add(base, chunkHeaderWords*uint32(wordSize))), n)
283+
return base, unsafe.Slice((*uint64)(unsafe.Add(base, chunkHeaderSize)), n)
278284
}
279285

280286
// chunkClass rounds n up so that a chunk with n words of records is 2^k

‎tsc/internal/ast/store_test.go‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,10 +86,10 @@ func TestNodeStoreCrossStoreRefs(t *testing.T) {
8686
assert.Equal(t, second.Expression(), target)
8787
target.SetParent(first)
8888
assert.Equal(t, target.Parent(), first)
89-
assert.Equal(t, len(b.store.far), 2)
89+
assert.Equal(t, len(b.store.far), 3)
9090
target.SetParent(second)
9191
target.SetParent(first)
92-
assert.Equal(t, len(a.store.far), 1, "rewriting a far ref should reuse its slot")
92+
assert.Equal(t, len(a.store.far), 2, "rewriting a far ref should reuse its slot")
9393
first.AsExpressionStatement().SetExpression(Node{})
9494
assert.Assert(t, first.Expression().IsNil())
9595
}

‎tsc/internal/ast/utilities.go‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -967,7 +967,8 @@ func SetImportsOfSourceFile(node *SourceFile, imports []LiteralLikeNode) {
967967

968968
// Walks up the parents of a node to find the ancestor that matches the callback
969969
func FindAncestor(node Node, callback func(Node) bool) Node {
970-
for !node.IsNil() {
970+
// Compares the handle directly to stay within the inlining budget.
971+
for node.p != nil {
971972
if callback(node) {
972973
return node
973974
}

0 commit comments

Comments
 (0)