Skip to content

Commit 2dfa950

Browse files
committed
Keep optional symbol tables out of hot symbol records
Most symbols do not need members, exports, or an export-symbol link. Keeping all three pointers in every symbol's shared metadata charges every property, parameter, and synthetic symbol for those features. Move them into optional shared metadata, reducing SymbolWithData from 104 to 88 bytes on amd64. Binder- and checker-owned typed arenas batch the sidecars without adding an allocator pointer to every symbol. Shared symbolData still sees sidecars created through either alias. Compared with the parent commit, which already includes the checker storage and direct-header AST changes. Six fresh-process interleaved samples parse, bind, and check ../vscode/src with the same single checker and normal GC settings. Both workloads report 321 diagnostics, 10284 files. Exact CLI diagnostics and compiler counts also match. CPU timings are noisy. Hardware instructions and allocation counts show no statistically established difference; do not claim CPU gains. goos: linux goarch: amd64 pkg: github.com/microsoft/TypeScript/tsc/internal/checker cpu: Intel(R) Core(TM) i9-10900K CPU @ 3.70GHz │ vscode-symbol-batched-before.txt │ vscode-symbol-batched.txt │ │ retained-B │ retained-B vs base │ ExternalProgram-20 4.180Gi ± 0% 4.129Gi ± 0% -1.21% (p=0.002 n=6) │ vscode-symbol-batched-before.txt │ vscode-symbol-batched.txt │ │ scan-B │ scan-B vs base │ ExternalProgram-20 3.738Gi ± 0% 3.687Gi ± 0% -1.34% (p=0.002 n=6) │ vscode-symbol-batched-before.txt │ vscode-symbol-batched.txt │ │ B/op │ B/op vs base │ ExternalProgram-20 5.659Gi ± 2% 5.606Gi ± 0% -0.93% (p=0.002 n=6) │ vscode-symbol-batched-before.txt │ vscode-symbol-batched.txt │ │ allocs/op │ allocs/op vs base │ ExternalProgram-20 26.06M ± 2% 26.08M ± 0% ~ (p=0.310 n=6) │ symbol-batched-before-instructions.txt │ symbol-batched-instructions.txt │ │ instructions/op │ instructions/op vs base │ ExternalInstructions-20 302.1G ± 4% 299.9G ± 3% ~ (p=0.589 n=6)
1 parent 72aae81 commit 2dfa950

5 files changed

Lines changed: 168 additions & 60 deletions

File tree

‎tsc/internal/ast/symbol.go‎

Lines changed: 60 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"strings"
55
"sync/atomic"
66

7+
"github.com/microsoft/TypeScript/tsc/internal/core"
78
"github.com/microsoft/TypeScript/tsc/internal/debug"
89
)
910

@@ -23,31 +24,81 @@ type symbolData struct {
2324
name string
2425
declarations []*Node
2526
valueDeclaration *Node
26-
members SymbolTable
27-
exports SymbolTable
2827
parent *Symbol
29-
exportSymbol *Symbol
28+
extra *symbolExtra
29+
}
30+
31+
type symbolExtra struct {
32+
members SymbolTable
33+
exports SymbolTable
34+
exportSymbol *Symbol
35+
}
36+
37+
// SymbolExtraArena batches optional symbol metadata without adding an allocator
38+
// pointer to every symbol. References from symbolData keep its blocks alive.
39+
type SymbolExtraArena struct {
40+
arena core.Arena[symbolExtra]
3041
}
3142

3243
func (s *Symbol) Flags() SymbolFlags { return s.flags }
3344
func (s *Symbol) CheckFlags() CheckFlags { return s.checkFlags }
3445
func (s *Symbol) Name() string { return s.data.name }
3546
func (s *Symbol) Declarations() []*Node { return s.data.declarations }
3647
func (s *Symbol) ValueDeclaration() *Node { return s.data.valueDeclaration }
37-
func (s *Symbol) Members() SymbolTable { return s.data.members }
38-
func (s *Symbol) Exports() SymbolTable { return s.data.exports }
3948
func (s *Symbol) Parent() *Symbol { return s.data.parent }
40-
func (s *Symbol) ExportSymbol() *Symbol { return s.data.exportSymbol }
49+
50+
func (s *Symbol) Members() SymbolTable {
51+
if s.data.extra == nil {
52+
return nil
53+
}
54+
return s.data.extra.members
55+
}
56+
57+
func (s *Symbol) Exports() SymbolTable {
58+
if s.data.extra == nil {
59+
return nil
60+
}
61+
return s.data.extra.exports
62+
}
63+
64+
func (s *Symbol) ExportSymbol() *Symbol {
65+
if s.data.extra == nil {
66+
return nil
67+
}
68+
return s.data.extra.exportSymbol
69+
}
4170

4271
func (s *Symbol) SetFlags(value SymbolFlags) { s.flags = value }
4372
func (s *Symbol) SetCheckFlags(value CheckFlags) { s.checkFlags = value }
4473
func (s *Symbol) SetName(value string) { s.data.name = value }
4574
func (s *Symbol) SetDeclarations(value []*Node) { s.data.declarations = value }
4675
func (s *Symbol) SetValueDeclaration(value *Node) { s.data.valueDeclaration = value }
47-
func (s *Symbol) SetMembers(value SymbolTable) { s.data.members = value }
48-
func (s *Symbol) SetExports(value SymbolTable) { s.data.exports = value }
4976
func (s *Symbol) SetParent(value *Symbol) { s.data.parent = value }
50-
func (s *Symbol) SetExportSymbol(value *Symbol) { s.data.exportSymbol = value }
77+
78+
func (s *Symbol) ensureExtra(arena *SymbolExtraArena) *symbolExtra {
79+
if s.data.extra == nil {
80+
s.data.extra = arena.arena.New()
81+
}
82+
return s.data.extra
83+
}
84+
85+
func (s *Symbol) SetMembers(value SymbolTable, arena *SymbolExtraArena) {
86+
if value != nil || s.data.extra != nil {
87+
s.ensureExtra(arena).members = value
88+
}
89+
}
90+
91+
func (s *Symbol) SetExports(value SymbolTable, arena *SymbolExtraArena) {
92+
if value != nil || s.data.extra != nil {
93+
s.ensureExtra(arena).exports = value
94+
}
95+
}
96+
97+
func (s *Symbol) SetExportSymbol(value *Symbol, arena *SymbolExtraArena) {
98+
if value != nil || s.data.extra != nil {
99+
s.ensureExtra(arena).exportSymbol = value
100+
}
101+
}
51102

52103
// SymbolWithData is a helper structure that contains both a Symbol and its associated symbolData.
53104
type SymbolWithData struct {

‎tsc/internal/ast/symbol_test.go‎

Lines changed: 69 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ package ast_test
22

33
import (
44
"reflect"
5+
"runtime"
56
"testing"
7+
"unsafe"
68

79
"github.com/microsoft/TypeScript/tsc/internal/ast"
810
"gotest.tools/v3/assert"
@@ -17,6 +19,7 @@ func TestSymbolFieldsArePrivate(t *testing.T) {
1719

1820
func TestSymbolAccessors(t *testing.T) {
1921
t.Parallel()
22+
var arena ast.SymbolExtraArena
2023
symbol := ast.NewSymbol()
2124
assert.Equal(t, symbol.Flags(), ast.SymbolFlagsNone)
2225
assert.Equal(t, symbol.CheckFlags(), ast.CheckFlags(0))
@@ -43,10 +46,10 @@ func TestSymbolAccessors(t *testing.T) {
4346
symbol.SetName("C")
4447
symbol.SetDeclarations(declarations)
4548
symbol.SetValueDeclaration(declaration)
46-
symbol.SetMembers(members)
47-
symbol.SetExports(exports)
49+
symbol.SetMembers(members, &arena)
50+
symbol.SetExports(exports, &arena)
4851
symbol.SetParent(parent)
49-
symbol.SetExportSymbol(exportSymbol)
52+
symbol.SetExportSymbol(exportSymbol, &arena)
5053

5154
assert.Equal(t, symbol.Flags(), ast.SymbolFlagsClass|ast.SymbolFlagsTransient)
5255
assert.Equal(t, symbol.CheckFlags(), ast.CheckFlagsReadonly)
@@ -72,10 +75,10 @@ func TestSymbolAccessors(t *testing.T) {
7275
symbol.SetName("")
7376
symbol.SetDeclarations(nil)
7477
symbol.SetValueDeclaration(nil)
75-
symbol.SetMembers(nil)
76-
symbol.SetExports(nil)
78+
symbol.SetMembers(nil, &arena)
79+
symbol.SetExports(nil, &arena)
7780
symbol.SetParent(nil)
78-
symbol.SetExportSymbol(nil)
81+
symbol.SetExportSymbol(nil, &arena)
7982

8083
assert.Equal(t, symbol.Flags(), ast.SymbolFlagsNone)
8184
assert.Equal(t, symbol.CheckFlags(), ast.CheckFlags(0))
@@ -90,24 +93,76 @@ func TestSymbolAccessors(t *testing.T) {
9093

9194
func TestSymbolTableInitialization(t *testing.T) {
9295
t.Parallel()
96+
var arena ast.SymbolExtraArena
9397
symbol := ast.NewSymbol()
9498
member := ast.NewSymbol()
9599
export := ast.NewSymbol()
96100

97-
members := ast.GetMembers(symbol)
101+
members := ast.GetMembers(symbol, &arena)
98102
members["member"] = member
99103
assert.Equal(t, symbol.Members()["member"], member)
100-
assert.Equal(t, ast.GetMembers(symbol)["member"], member)
104+
assert.Equal(t, ast.GetMembers(symbol, &arena)["member"], member)
101105

102-
exports := ast.GetExports(symbol)
106+
exports := ast.GetExports(symbol, &arena)
103107
exports["export"] = export
104108
assert.Equal(t, symbol.Exports()["export"], export)
105-
assert.Equal(t, ast.GetExports(symbol)["export"], export)
109+
assert.Equal(t, ast.GetExports(symbol, &arena)["export"], export)
106110

107-
symbol.SetMembers(nil)
108-
symbol.SetExports(nil)
109-
assert.Assert(t, ast.GetMembers(symbol) != nil)
110-
assert.Assert(t, ast.GetExports(symbol) != nil)
111+
symbol.SetMembers(nil, &arena)
112+
symbol.SetExports(nil, &arena)
113+
assert.Assert(t, ast.GetMembers(symbol, &arena) != nil)
114+
assert.Assert(t, ast.GetExports(symbol, &arena) != nil)
111115
assert.Equal(t, len(symbol.Members()), 0)
112116
assert.Equal(t, len(symbol.Exports()), 0)
113117
}
118+
119+
func TestSymbolHotRecordLayout(t *testing.T) {
120+
t.Parallel()
121+
assert.Equal(t, unsafe.Sizeof(ast.SymbolWithData{}), unsafe.Sizeof(ast.Symbol{})+8*unsafe.Sizeof((*ast.Node)(nil)))
122+
}
123+
124+
func TestSymbolNilExtraFieldsDoNotAllocate(t *testing.T) { //nolint:paralleltest
125+
var arena ast.SymbolExtraArena
126+
symbol := ast.NewSymbol()
127+
allocs := testing.AllocsPerRun(10, func() {
128+
symbol.SetMembers(nil, &arena)
129+
symbol.SetExports(nil, &arena)
130+
symbol.SetExportSymbol(nil, &arena)
131+
})
132+
assert.Equal(t, allocs, float64(0))
133+
assert.Assert(t, reflect.ValueOf(symbol).Elem().FieldByName("data").Elem().FieldByName("extra").IsNil())
134+
}
135+
136+
func TestSymbolSharedExtraData(t *testing.T) {
137+
t.Parallel()
138+
var arena ast.SymbolExtraArena
139+
var otherArena ast.SymbolExtraArena
140+
symbol := ast.NewSymbol()
141+
alias := ast.NewSymbol()
142+
alias.SetSymbolData(symbol)
143+
member := ast.NewSymbol()
144+
export := ast.NewSymbol()
145+
alias.SetMembers(ast.SymbolTable{"member": member}, &arena)
146+
symbol.SetExports(ast.SymbolTable{"export": export}, &otherArena)
147+
alias.SetExportSymbol(export, &otherArena)
148+
runtime.GC()
149+
assert.Equal(t, symbol.Members()["member"], member)
150+
assert.Equal(t, alias.Exports()["export"], export)
151+
assert.Equal(t, symbol.ExportSymbol(), export)
152+
symbol.SetMembers(nil, &otherArena)
153+
alias.SetExports(nil, &arena)
154+
symbol.SetExportSymbol(nil, &arena)
155+
assert.Assert(t, alias.Members() == nil)
156+
assert.Assert(t, symbol.Exports() == nil)
157+
assert.Assert(t, alias.ExportSymbol() == nil)
158+
ast.GetMembers(alias, &otherArena)["new"] = export
159+
ast.GetExports(symbol, &arena)["new"] = member
160+
for range 1024 {
161+
ast.NewSymbol().SetExports(ast.SymbolTable{"export": export}, &arena)
162+
}
163+
arena = ast.SymbolExtraArena{}
164+
otherArena = ast.SymbolExtraArena{}
165+
runtime.GC()
166+
assert.Equal(t, symbol.Members()["new"], export)
167+
assert.Equal(t, alias.Exports()["new"], member)
168+
}

‎tsc/internal/ast/utilities.go‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -99,12 +99,12 @@ func GetSymbolTable(data *SymbolTable) SymbolTable {
9999
return *data
100100
}
101101

102-
func GetMembers(symbol *Symbol) SymbolTable {
103-
return GetSymbolTable(&symbol.data.members)
102+
func GetMembers(symbol *Symbol, arena *SymbolExtraArena) SymbolTable {
103+
return GetSymbolTable(&symbol.ensureExtra(arena).members)
104104
}
105105

106-
func GetExports(symbol *Symbol) SymbolTable {
107-
return GetSymbolTable(&symbol.data.exports)
106+
func GetExports(symbol *Symbol, arena *SymbolExtraArena) SymbolTable {
107+
return GetSymbolTable(&symbol.ensureExtra(arena).exports)
108108
}
109109

110110
func GetLocals(container *Node) SymbolTable {

0 commit comments

Comments
 (0)