Skip to content

Commit d8065db

Browse files
committed
Index checker members without redundant symbol maps
Instantiated and inherited object types retain a name-to-symbol map beside their ordered property list. Those maps duplicate symbol pointers and string headers across millions of types, increasing both live heap and the amount of memory the collector must scan. Use the existing property list as the source of truth for immutable named members. Small types need no separate index; larger types need only pointer-free integer slots. Keep map semantics for mutable module exports, reserved members, mismatched names, and recursive member resolution. VS Code src, six samples versus cold value-symbol links: goos: linux goarch: amd64 pkg: github.com/microsoft/TypeScript/tsc/internal/checker cpu: Intel(R) Core(TM) i9-10900K CPU @ 3.70GHz │ vscode-cold-links.txt │ vscode-member-index-owned.txt │ │ retained-B │ retained-B vs base │ ExternalProgram-20 4.874Gi ± 0% 4.503Gi ± 0% -7.61% (p=0.002 n=6) │ vscode-cold-links.txt │ vscode-member-index-owned.txt │ │ scan-B │ scan-B vs base │ ExternalProgram-20 4.508Gi ± 0% 4.062Gi ± 0% -9.89% (p=0.002 n=6) │ vscode-cold-links.txt │ vscode-member-index-owned.txt │ │ B/op │ B/op vs base │ ExternalProgram-20 6.043Gi ± 0% 5.982Gi ± 0% -1.00% (p=0.002 n=6) │ vscode-cold-links.txt │ vscode-member-index-owned.txt │ │ allocs/op │ allocs/op vs base │ ExternalProgram-20 26.23M ± 0% 26.04M ± 0% -0.71% (p=0.002 n=6) All 321 semantic diagnostics and 10284 files are identical. CLI diagnostic text and compiler counts match exactly. Instruction measurements remain noisy; this change does not claim an established CPU improvement. No GC tuning is used.
1 parent 7e77edb commit d8065db

5 files changed

Lines changed: 191 additions & 13 deletions

File tree

‎tsc/internal/checker/checker.go‎

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -690,6 +690,8 @@ type Checker struct {
690690
outerTypeParameterListArena core.Arena[[]*Type]
691691
valueSymbolExtraArena core.Arena[valueSymbolExtra]
692692
structuredTypeExtraArena core.Arena[structuredTypeExtra]
693+
memberIndexArena core.Arena[memberIndex]
694+
memberIndexSlotsArena core.Arena[uint32]
693695
freeTypeSets *temporaryTypeSet
694696
freePropertyCounts *temporaryPropertyCounts
695697
mergedSymbols map[*ast.Symbol]*ast.Symbol
@@ -10951,7 +10953,7 @@ func (c *Checker) getInstantiationExpressionType(exprType *Type, node *ast.Node)
1095110953
debug.Assert(t.symbol != nil, "Instantiation expression source type must have a symbol")
1095210954
symbol.SetDeclarations(t.symbol.Declarations())
1095310955
result := c.newObjectType(ObjectFlagsAnonymous|ObjectFlagsInstantiationExpressionType, symbol)
10954-
c.setStructuredTypeMembers(result, resolved.members, callSignatures, constructSignatures, resolved.getIndexInfos())
10956+
c.setStructuredTypeMembers(result, resolved.getMembers(), callSignatures, constructSignatures, resolved.getIndexInfos())
1095510957
result.AsInstantiationExpressionType().node = node
1095610958
return result
1095710959
}
@@ -16163,7 +16165,7 @@ func (c *Checker) cloneTypeAsModuleType(symbol *ast.Symbol, moduleType *Type, re
1616316165
links.target = symbol
1616416166
links.originatingImport = referenceParent
1616516167
resolvedModuleType := c.resolveStructuredTypeMembers(moduleType)
16166-
c.valueSymbolLinks.Get(result).resolvedType = c.newAnonymousType(result, resolvedModuleType.members, nil, nil, resolvedModuleType.getIndexInfos())
16168+
c.valueSymbolLinks.Get(result).resolvedType = c.newAnonymousType(result, resolvedModuleType.getMembers(), nil, nil, resolvedModuleType.getIndexInfos())
1616716169
return result
1616816170
}
1616916171

@@ -19363,7 +19365,7 @@ func (c *Checker) getPropertyOfTypeEx(t *Type, name string, skipObjectFunctionPr
1936319365
switch {
1936419366
case t.flags&TypeFlagsObject != 0:
1936519367
resolved := c.resolveStructuredTypeMembers(t)
19366-
symbol := resolved.members[name]
19368+
symbol := resolved.getMember(name)
1936719369
if symbol != nil {
1936819370
if !includeTypeOnlyMembers && t.symbol != nil && t.symbol.Flags()&ast.SymbolFlagsValueModule != 0 && c.moduleSymbolLinks.Get(t.symbol).typeOnlyExportStarMap[name] != nil {
1936919371
// If this is the type of a module, `resolved.members.get(name)` might have effectively skipped over
@@ -19571,6 +19573,7 @@ func (c *Checker) resolveTypeReferenceMembers(t *Type) {
1957119573
func (c *Checker) resolveObjectTypeMembers(t *Type, source *Type, typeParameters []*Type, typeArguments []*Type) {
1957219574
var mapper *TypeMapper
1957319575
var members ast.SymbolTable
19576+
var properties []*ast.Symbol
1957419577
var callSignatures []*Signature
1957519578
var constructSignatures []*Signature
1957619579
var indexInfos []*IndexInfo
@@ -19584,7 +19587,19 @@ func (c *Checker) resolveObjectTypeMembers(t *Type, source *Type, typeParameters
1958419587
} else {
1958519588
instantiated = true
1958619589
mapper = newTypeMapper(typeParameters, typeArguments)
19587-
members = c.instantiateSymbolTable(resolved.declaredMembers, mapper)
19590+
properties = c.getNamedMembers(resolved.declaredMembers, t.symbol)
19591+
for _, symbol := range properties {
19592+
if resolved.declaredMembers[symbol.Name()] != symbol {
19593+
members = c.instantiateSymbolTable(resolved.declaredMembers, mapper)
19594+
properties = nil
19595+
break
19596+
}
19597+
}
19598+
if members == nil {
19599+
for i, symbol := range properties {
19600+
properties[i] = c.instantiateSymbol(symbol, mapper)
19601+
}
19602+
}
1958819603
callSignatures = c.instantiateSignatures(resolved.declaredCallSignatures, mapper)
1958919604
constructSignatures = c.instantiateSignatures(resolved.declaredConstructSignatures, mapper)
1959019605
indexInfos = c.instantiateIndexInfos(resolved.declaredIndexInfos, mapper)
@@ -19593,6 +19608,11 @@ func (c *Checker) resolveObjectTypeMembers(t *Type, source *Type, typeParameters
1959319608
if len(baseTypes) != 0 {
1959419609
if !instantiated {
1959519610
members = maps.Clone(members)
19611+
} else if members == nil {
19612+
members = make(ast.SymbolTable, len(properties))
19613+
for _, symbol := range properties {
19614+
members[symbol.Name()] = symbol
19615+
}
1959619616
}
1959719617
thisArgument := core.LastOrNil(typeArguments)
1959819618
for _, baseType := range baseTypes {
@@ -19615,6 +19635,11 @@ func (c *Checker) resolveObjectTypeMembers(t *Type, source *Type, typeParameters
1961519635
}
1961619636
}
1961719637
c.setStructuredTypeMembers(t, members, callSignatures, constructSignatures, indexInfos)
19638+
if instantiated && len(baseTypes) == 0 && members == nil {
19639+
data := t.AsStructuredType()
19640+
data.properties = properties
19641+
data.members = c.newPropertyIndex(properties, nil)
19642+
}
1961819643
}
1961919644

1962019645
func findIndexInfo(indexInfos []*IndexInfo, keyType *Type) *IndexInfo {
@@ -21870,7 +21895,7 @@ func (c *Checker) includeMixinType(t *Type, types []*Type, mixinFlags []bool, in
2187021895
func (c *Checker) getPropertyOfObjectType(t *Type, name string) *ast.Symbol {
2187121896
if t.flags&TypeFlagsObject != 0 {
2187221897
resolved := c.resolveStructuredTypeMembers(t)
21873-
symbol := resolved.members[name]
21898+
symbol := resolved.getMember(name)
2187421899
if symbol != nil && c.symbolIsValue(symbol) {
2187521900
return symbol
2187621901
}
@@ -25741,8 +25766,18 @@ func (c *Checker) cloneTypeReference(source *Type) *Type {
2574125766
func (c *Checker) setStructuredTypeMembers(t *Type, members ast.SymbolTable, callSignatures []*Signature, constructSignatures []*Signature, indexInfos []*IndexInfo) {
2574225767
t.objectFlags |= ObjectFlagsMembersResolved
2574325768
data := t.AsStructuredType()
25744-
data.members = members
25769+
// Named-member filtering may recursively query this type before it finishes.
25770+
data.members = nil
25771+
if members != nil {
25772+
data.members = c.memberIndexArena.New()
25773+
data.members.fallback = members
25774+
}
2574525775
data.properties = c.getNamedMembers(members, t.symbol)
25776+
if data.members != nil && t.symbol != nil && t.symbol.Flags()&ast.SymbolFlagsValueModule != 0 {
25777+
data.members.fallback = members
25778+
} else {
25779+
data.members = c.newMemberIndex(members, data.properties, data.members)
25780+
}
2574625781
c.setStructuredTypeSignatures(data, callSignatures, constructSignatures, indexInfos)
2574725782
}
2574825783

@@ -31804,7 +31839,7 @@ func (c *Checker) isFunctionObjectType(t *Type) bool {
3180431839
// We do a quick check for a "bind" property before performing the more expensive subtype
3180531840
// check. This gives us a quicker out in the common case where an object type is not a function.
3180631841
resolved := c.resolveStructuredTypeMembers(t)
31807-
return len(resolved.getSignatures()) != 0 || resolved.members["bind"] != nil && c.isTypeSubtypeOf(t, c.globalFunctionType)
31842+
return len(resolved.getSignatures()) != 0 || resolved.getMember("bind") != nil && c.isTypeSubtypeOf(t, c.globalFunctionType)
3180831843
}
3180931844

3181031845
func (c *Checker) getTypeWithFacts(t *Type, include TypeFacts) *Type {
@@ -32300,7 +32335,7 @@ func (c *Checker) getSymbolAtLocation(node *ast.Node, ignoreErrors bool) *ast.Sy
3230032335
// member should more exactly be the kind of (declarationless) symbol we want.
3230132336
// (See #44364 and #45031 for relevant implementation PRs)
3230232337
if metaProp.KeywordToken == ast.KindImportKeyword && node.Text() == "meta" {
32303-
return c.getGlobalImportMetaExpressionType().AsObjectType().members["meta"]
32338+
return c.getGlobalImportMetaExpressionType().AsObjectType().getMember("meta")
3230432339
}
3230532340
// no other meta properties are valid syntax, thus no others should have symbols
3230632341
return nil

‎tsc/internal/checker/nodebuilderimpl.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -576,7 +576,7 @@ func (b *NodeBuilderImpl) getResolvedTypeWithoutAbstractConstructSignatures(t *S
576576
t.extra.objectTypeWithoutAbstractConstructSignatures = t.AsType()
577577
return t.AsType()
578578
}
579-
typeCopy := b.ch.newAnonymousType(t.symbol, t.members, t.CallSignatures(), core.IfElse(len(constructSignatures) > 0, constructSignatures, []*Signature{}), t.getIndexInfos())
579+
typeCopy := b.ch.newAnonymousType(t.symbol, t.getMembers(), t.CallSignatures(), core.IfElse(len(constructSignatures) > 0, constructSignatures, []*Signature{}), t.getIndexInfos())
580580
t.extra.objectTypeWithoutAbstractConstructSignatures = typeCopy
581581
typeCopy.AsStructuredType().ensureExtra(b.ch).objectTypeWithoutAbstractConstructSignatures = typeCopy
582582
return typeCopy

‎tsc/internal/checker/services.go‎

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -154,10 +154,17 @@ func (c *Checker) ForEachExportAndPropertyOfModule(moduleSymbol *ast.Symbol, cb
154154
if reducedType.flags&TypeFlagsStructuredType == 0 {
155155
return
156156
}
157-
for name, symbol := range c.resolveStructuredTypeMembers(reducedType).members {
158-
if c.isNamedMember(symbol, name) {
159-
cb(symbol, name)
157+
resolved := c.resolveStructuredTypeMembers(reducedType)
158+
if resolved.members != nil && resolved.members.fallback != nil {
159+
for name, symbol := range resolved.members.fallback {
160+
if c.isNamedMember(symbol, name) {
161+
cb(symbol, name)
162+
}
160163
}
164+
return
165+
}
166+
for _, symbol := range resolved.properties {
167+
cb(symbol, symbol.Name())
161168
}
162169
}
163170

‎tsc/internal/checker/types.go‎

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"github.com/microsoft/TypeScript/tsc/internal/collections"
1111
"github.com/microsoft/TypeScript/tsc/internal/core"
1212
"github.com/microsoft/TypeScript/tsc/internal/evaluator"
13+
"github.com/zeebo/xxh3"
1314
)
1415

1516
//go:generate npx hereby generate:checker
@@ -1114,7 +1115,7 @@ func (t *ConstrainedType) AsConstrainedType() *ConstrainedType { return t }
11141115

11151116
type StructuredType struct {
11161117
ConstrainedType
1117-
members ast.SymbolTable
1118+
members *memberIndex
11181119
properties []*ast.Symbol
11191120
extra *structuredTypeExtra
11201121
}
@@ -1168,6 +1169,102 @@ func (t *StructuredType) Properties() []*ast.Symbol {
11681169
return t.properties
11691170
}
11701171

1172+
type memberIndex struct {
1173+
slots []uint32
1174+
fallback ast.SymbolTable
1175+
}
1176+
1177+
// Property order is semantic. Keep it unchanged and index it with pointer-free
1178+
// slots instead of retaining a second map of names and symbol pointers.
1179+
func (c *Checker) newMemberIndex(members ast.SymbolTable, properties []*ast.Symbol, index *memberIndex) *memberIndex {
1180+
needsFallback := len(members) != len(properties)
1181+
for _, symbol := range properties {
1182+
if members[symbol.Name()] != symbol {
1183+
needsFallback = true
1184+
break
1185+
}
1186+
}
1187+
if needsFallback {
1188+
if index == nil {
1189+
index = c.memberIndexArena.New()
1190+
}
1191+
index.fallback = members
1192+
return index
1193+
}
1194+
if index != nil {
1195+
index.fallback = nil
1196+
}
1197+
return c.newPropertyIndex(properties, index)
1198+
}
1199+
1200+
func (c *Checker) newPropertyIndex(properties []*ast.Symbol, index *memberIndex) *memberIndex {
1201+
if len(properties) <= 8 {
1202+
return nil
1203+
}
1204+
if uint64(len(properties)) > uint64(^uint32(0)) {
1205+
panic("Member index exhausted")
1206+
}
1207+
if index == nil {
1208+
index = c.memberIndexArena.New()
1209+
}
1210+
size := 1 << bits.Len(uint(len(properties))*2-1)
1211+
index.slots = c.memberIndexSlotsArena.NewSlice(size)
1212+
mask := uint64(size - 1)
1213+
for i, symbol := range properties {
1214+
slot := xxh3.HashString(symbol.Name()) & mask
1215+
for index.slots[slot] != 0 {
1216+
slot = (slot + 1) & mask
1217+
}
1218+
index.slots[slot] = uint32(i + 1)
1219+
}
1220+
return index
1221+
}
1222+
1223+
func (t *StructuredType) getMember(name string) *ast.Symbol {
1224+
if t.members == nil {
1225+
for _, symbol := range t.properties {
1226+
if symbol.Name() == name {
1227+
return symbol
1228+
}
1229+
}
1230+
return nil
1231+
}
1232+
if t.members.fallback != nil {
1233+
return t.members.fallback[name]
1234+
}
1235+
slots := t.members.slots
1236+
if len(slots) == 0 {
1237+
return nil
1238+
}
1239+
mask := uint64(len(slots) - 1)
1240+
slot := xxh3.HashString(name) & mask
1241+
for {
1242+
index := slots[slot]
1243+
if index == 0 {
1244+
return nil
1245+
}
1246+
symbol := t.properties[index-1]
1247+
if symbol.Name() == name {
1248+
return symbol
1249+
}
1250+
slot = (slot + 1) & mask
1251+
}
1252+
}
1253+
1254+
func (t *StructuredType) getMembers() ast.SymbolTable {
1255+
if t.members != nil && t.members.fallback != nil {
1256+
return t.members.fallback
1257+
}
1258+
if len(t.properties) == 0 {
1259+
return nil
1260+
}
1261+
members := make(ast.SymbolTable, len(t.properties))
1262+
for _, symbol := range t.properties {
1263+
members[symbol.Name()] = symbol
1264+
}
1265+
return members
1266+
}
1267+
11711268
// Except for tuple type references and reverse mapped types, all object types have an associated symbol.
11721269
// Possible object type instances are listed in the following.
11731270

‎tsc/internal/checker/types_test.go‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -536,6 +536,45 @@ func TestCompactValueSymbolLinksLayout(t *testing.T) {
536536
assert.Equal(t, links.getContainingType(), types[2])
537537
}
538538

539+
func TestStructuredMembersLookup(t *testing.T) {
540+
t.Parallel()
541+
c, _ := newCompositeTestChecker()
542+
for _, count := range []int{0, 1, 8, 20, 300} {
543+
members := make(ast.SymbolTable, count)
544+
for i := range count {
545+
name := strconv.Itoa(i)
546+
members[name] = c.newSymbol(ast.SymbolFlagsProperty, name)
547+
}
548+
typ := c.newAnonymousType(nil, members, nil, nil, nil)
549+
resolved := typ.AsStructuredType()
550+
assert.Equal(t, len(resolved.properties), count)
551+
for name, symbol := range members {
552+
assert.Equal(t, resolved.getMember(name), symbol)
553+
}
554+
assert.Assert(t, resolved.getMember("missing") == nil)
555+
other := c.newAnonymousType(nil, resolved.getMembers(), nil, nil, nil)
556+
for name, symbol := range members {
557+
assert.Equal(t, other.AsStructuredType().getMember(name), symbol)
558+
}
559+
members["new"] = c.newSymbol(ast.SymbolFlagsProperty, "new")
560+
assert.Assert(t, resolved.getMember("new") == nil)
561+
members[ast.InternalSymbolNameCall] = c.newSymbol(ast.SymbolFlagsSignature, ast.InternalSymbolNameCall)
562+
withReserved := c.newAnonymousType(nil, members, nil, nil, nil).AsStructuredType()
563+
assert.Equal(t, withReserved.getMember(ast.InternalSymbolNameCall), members[ast.InternalSymbolNameCall])
564+
for range 300 {
565+
c.newMemberIndex(members, withReserved.properties, nil)
566+
}
567+
runtime.GC()
568+
for name, symbol := range members {
569+
assert.Equal(t, withReserved.getMember(name), symbol)
570+
}
571+
}
572+
renamed := c.newSymbol(ast.SymbolFlagsProperty, "original")
573+
member := c.newAnonymousType(nil, ast.SymbolTable{"different": renamed}, nil, nil, nil).AsStructuredType()
574+
assert.Equal(t, member.getMember("different"), renamed)
575+
assert.Assert(t, member.getMember("original") == nil)
576+
}
577+
539578
func TestCompactStructuredTypeLayout(t *testing.T) {
540579
t.Parallel()
541580
assert.Equal(t, unsafe.Sizeof(StructuredType{}), unsafe.Sizeof(ConstrainedType{})+5*unsafe.Sizeof(uintptr(0)))

0 commit comments

Comments
 (0)