Skip to content

Commit 4ba9572

Browse files
committed
cue/ast/astutil: avoid walking to the root to reuse scopes
As suggested by Matthew, this can avoid some overhead when having more than a few levels of scope nesting. Plus, a pointer is smaller than a slice, so each scope is a bit smaller. Even with the AWS schema, which tops at a scope depth of 14, this does lead to a measurable savings in CPU cost. On the flip side, dealing with pointers makes the code a bit trickier, but overall it's a coin toss as we can remove two methods. │ old │ mid │ │ sec/op │ sec/op vs base │ FmtAwsSchema 2.210 ± 1% 2.167 ± 1% -1.91% (p=0.001 n=8) │ old │ mid │ │ B/op │ B/op vs base │ FmtAwsSchema 1.064Gi ± 0% 1.064Gi ± 0% -0.00% (p=0.005 n=8) │ old │ mid │ │ allocs/op │ allocs/op vs base │ FmtAwsSchema 12.92M ± 0% 12.92M ± 0% ~ (p=0.063 n=8) Signed-off-by: Daniel Martí <[email protected]> Change-Id: Iea3af538bc94da349456f9e9918052a0e6ae192b Reviewed-on: https://cue.gerrithub.io/c/cue-lang/cue/+/1225195 Reviewed-by: Matthew Sackman <[email protected]> Unity-Result: CUE porcuepine <[email protected]> TryBot-Result: CUEcueckoo <[email protected]>
1 parent e79d2ea commit 4ba9572

File tree

2 files changed

+39
-38
lines changed

2 files changed

+39
-38
lines changed

cue/ast/astutil/resolve.go

Lines changed: 28 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -83,15 +83,26 @@ type ErrFunc func(pos token.Pos, msg string, args ...interface{})
8383
// Unresolved identifiers are recorded in [ast.File.Unresolved].
8484
// It will not overwrite already resolved identifiers.
8585
func Resolve(f *ast.File, errFn ErrFunc) {
86-
visitor := &scope{errFn: errFn, identFn: resolveIdent}
86+
stack := make([]*scope, 0, 8)
87+
visitor := &scope{
88+
errFn: errFn,
89+
identFn: resolveIdent,
90+
scopeStack: &stack,
91+
}
8792
ast.Walk(f, visitor.Before, nil)
8893
}
8994

9095
// ResolveExpr resolves all identifiers in an expression.
9196
// It will not overwrite already resolved values.
9297
func ResolveExpr(e ast.Expr, errFn ErrFunc) {
9398
f := &ast.File{}
94-
visitor := &scope{file: f, errFn: errFn, identFn: resolveIdent}
99+
stack := make([]*scope, 0, 8)
100+
visitor := &scope{
101+
file: f,
102+
errFn: errFn,
103+
identFn: resolveIdent,
104+
scopeStack: &stack,
105+
}
95106
ast.Walk(e, visitor.Before, nil)
96107
}
97108

@@ -109,8 +120,9 @@ type scope struct {
109120
nameFn func(name string)
110121
errFn func(p token.Pos, msg string, args ...interface{})
111122

112-
// scopeStack is used to reuse scope allocations. Only set on the root scope.
113-
scopeStack []*scope
123+
// scopeStack is used to reuse scope allocations.
124+
// The pointer is shared between the root scope and all its children.
125+
scopeStack *[]*scope
114126
}
115127

116128
type entry struct {
@@ -119,49 +131,34 @@ type entry struct {
119131
field *ast.Field // Used for LabelAliases
120132
}
121133

122-
func (s *scope) rootScope() *scope {
123-
// If this ever shows up in a CPU profile because we have very deeply nested scopes,
124-
// we can consider a shortcut, such as a pointer to a shared slice or struct.
125-
for s.outer != nil {
126-
s = s.outer
127-
}
128-
return s
129-
}
130-
131134
func (s *scope) allocScope() *scope {
132-
root := s.rootScope()
133-
if n := len(root.scopeStack); n > 0 {
134-
scope := root.scopeStack[n-1]
135-
root.scopeStack = root.scopeStack[:n-1]
135+
if n := len(*s.scopeStack); n > 0 {
136+
scope := (*s.scopeStack)[n-1]
137+
*s.scopeStack = (*s.scopeStack)[:n-1]
136138
return scope
137139
}
138-
return &scope{index: make(map[string]entry, 4)}
140+
return &scope{
141+
index: make(map[string]entry, 4),
142+
scopeStack: s.scopeStack,
143+
}
139144
}
140145

141-
// putScope is not a method on scope on purpose, as we don't want to repeat
142-
// calls to [scope.rootScope] in [scope.freeScopesUntil].
143-
func putScope(root, s *scope) {
146+
func (s *scope) freeScope() {
144147
// Ensure no pointers remain, which can hold onto memory.
145-
// We only reuse the index map capacity.
146-
*s = scope{index: s.index}
148+
// We only reuse the index map capacity, and keep the scopeStack pointer.
149+
*s = scope{index: s.index, scopeStack: s.scopeStack}
147150
clear(s.index)
148-
root.scopeStack = append(root.scopeStack, s)
149-
}
150-
151-
func (s *scope) freeScope() {
152-
root := s.rootScope()
153-
putScope(root, s)
151+
*s.scopeStack = append(*s.scopeStack, s)
154152
}
155153

156154
// freeScopesUntil frees all scopes from s up to (but not including) 'ancestor'.
157155
func (s *scope) freeScopesUntil(ancestor *scope) {
158-
root := s.rootScope()
159156
for s != ancestor {
160157
if s == nil {
161158
panic("ancestor scope not found")
162159
}
163160
next := s.outer
164-
putScope(root, s)
161+
s.freeScope()
165162
s = next
166163
}
167164
}

cue/ast/astutil/sanitize.go

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -50,22 +50,26 @@ func Sanitize(f *ast.File) error {
5050
}
5151

5252
// Gather all names.
53+
stack := make([]*scope, 0, 8)
5354
s := &scope{
54-
errFn: z.errf,
55-
nameFn: z.addName,
56-
identFn: z.markUsed,
55+
errFn: z.errf,
56+
nameFn: z.addName,
57+
identFn: z.markUsed,
58+
scopeStack: &stack,
5759
}
5860
ast.Walk(f, s.Before, nil)
5961
if z.errs != nil {
6062
return z.errs
6163
}
6264

6365
// Add imports and unshadow.
66+
stack = stack[:0]
6467
s = &scope{
65-
file: f,
66-
errFn: z.errf,
67-
identFn: z.handleIdent,
68-
index: make(map[string]entry),
68+
file: f,
69+
errFn: z.errf,
70+
identFn: z.handleIdent,
71+
index: make(map[string]entry),
72+
scopeStack: &stack,
6973
}
7074
z.fileScope = s
7175
ast.Walk(f, s.Before, nil)

0 commit comments

Comments
 (0)