Skip to content

Commit 654dcfa

Browse files
committed
Avoid allocating normalized types on interning cache hits
Union and intersection normalization creates temporary lists even when an equivalent type already exists. Mapping and narrowing repeat this work throughout checking, producing unnecessary garbage. Reentrant, bounded workspaces let canonicalization reuse that storage while published types and subtype-reduction caches retain independently owned constituents. Fresh parse, bind, and check of the compiler fixture (six samples): │ before │ after │ │ B/op │ B/op vs base │ CheckFreshProgram-20 303.0Mi 277.2Mi -8.51% (p=0.002 n=6) │ before │ after │ │ allocs/op │ allocs/op vs base │ CheckFreshProgram-20 1.393M 1.139M -18.26% (p=0.002 n=6) VS Code src: identical diagnostics and compiler counts, 31.01M to 28.52M allocations. One fixed-work instruction sample fell from 344.57B to 324.08B; timing is noisy and not evidence of a reliable wall-time gain. Retained memory was effectively unchanged at 5.38 GiB.
1 parent 2a39414 commit 654dcfa

4 files changed

Lines changed: 211 additions & 19 deletions

File tree

‎tsc/internal/checker/checker.go‎

Lines changed: 36 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -681,6 +681,8 @@ type Checker struct {
681681
typeParameterArena core.Arena[TypeParameter]
682682
indexedAccessTypeArena core.Arena[IndexedAccessType]
683683
conditionalTypeArena core.Arena[ConditionalType]
684+
typeListArena core.Arena[*Type]
685+
freeTypeSets *temporaryTypeSet
684686
mergedSymbols map[*ast.Symbol]*ast.Symbol
685687
mergedExportsChecked collections.Set[*ast.Symbol]
686688
factory ast.NodeFactory
@@ -25751,13 +25753,13 @@ func (c *Checker) getPropagatingFlagsOfTypes(types []*Type, excludeKinds TypeFla
2575125753

2575225754
func (c *Checker) newUnionType(objectFlags ObjectFlags, types []*Type) *Type {
2575325755
data := c.unionTypeArena.New()
25754-
data.types = types
25756+
data.types = c.typeListArena.Clone(types)
2575525757
return c.newType(TypeFlagsUnion, objectFlags, data.AsType(), typeKindUnion)
2575625758
}
2575725759

2575825760
func (c *Checker) newIntersectionType(objectFlags ObjectFlags, types []*Type) *Type {
2575925761
data := c.intersectionTypeArena.New()
25760-
data.types = types
25762+
data.types = c.typeListArena.Clone(types)
2576125763
return c.newType(TypeFlagsIntersection, objectFlags, data.AsType(), typeKindIntersection)
2576225764
}
2576325765

@@ -26138,27 +26140,37 @@ func (c *Checker) mapTypeEx(t *Type, f func(*Type) *Type, noReductions bool) *Ty
2613826140
if u.origin != nil && u.origin.flags&TypeFlagsUnion != 0 {
2613926141
types = u.origin.Types()
2614026142
}
26141-
mappedTypes := make([]*Type, 0, 16)
26143+
var set *temporaryTypeSet
26144+
defer func() {
26145+
if set != nil {
26146+
c.releaseTemporaryTypeSet(set)
26147+
}
26148+
}()
26149+
var mappedTypes []*Type
2614226150
var changed bool
26143-
for _, s := range types {
26151+
for i, s := range types {
2614426152
var mapped *Type
2614526153
if s.flags&TypeFlagsUnion != 0 {
2614626154
mapped = c.mapTypeEx(s, f, noReductions)
2614726155
} else {
2614826156
mapped = f(s)
2614926157
}
26150-
if mapped != s {
26158+
if mapped != s && !changed {
2615126159
changed = true
26160+
set = c.getTemporaryTypeSet()
26161+
mappedTypes = set.values[:0]
26162+
mappedTypes = append(mappedTypes, types[:i]...)
2615226163
}
26153-
if mapped != nil {
26164+
if changed && mapped != nil {
2615426165
mappedTypes = append(mappedTypes, mapped)
2615526166
}
2615626167
}
2615726168
if changed {
26169+
set.values = mappedTypes
2615826170
if len(mappedTypes) == 0 {
2615926171
return nil
2616026172
}
26161-
return c.getUnionTypeEx(slices.Clone(mappedTypes), core.IfElse(noReductions, UnionReductionNone, UnionReductionLiteral), nil /*alias*/, nil /*origin*/)
26173+
return c.getUnionTypeEx(mappedTypes, core.IfElse(noReductions, UnionReductionNone, UnionReductionLiteral), nil /*alias*/, nil /*origin*/)
2616226174
}
2616326175
return t
2616426176
}
@@ -26215,7 +26227,10 @@ func (c *Checker) getUnionTypeEx(types []*Type, unionReduction UnionReduction, a
2621526227
}
2621626228

2621726229
func (c *Checker) getUnionTypeWorker(types []*Type, unionReduction UnionReduction, alias *TypeAlias, origin *Type) *Type {
26218-
typeSet, includes := c.addTypesToUnion(types)
26230+
set := c.getTemporaryTypeSet()
26231+
defer c.releaseTemporaryTypeSet(set)
26232+
typeSet, includes := c.addTypesToUnion(types, set.values[:0])
26233+
set.values = typeSet
2621926234
if unionReduction != UnionReductionNone {
2622026235
if includes&TypeFlagsAnyOrUnknown != 0 {
2622126236
if includes&TypeFlagsAny != 0 {
@@ -26322,8 +26337,7 @@ func (c *Checker) UnionTypes() iter.Seq[*Type] {
2632226337
return maps.Values(c.unionTypes)
2632326338
}
2632426339

26325-
func (c *Checker) addTypesToUnion(sourceTypes []*Type) ([]*Type, TypeFlags) {
26326-
types := make([]*Type, 0, len(sourceTypes))
26340+
func (c *Checker) addTypesToUnion(sourceTypes []*Type, types []*Type) ([]*Type, TypeFlags) {
2632726341
var includes TypeFlags
2632826342
addType := func(t *Type) {
2632926343
flags := t.flags
@@ -26581,8 +26595,9 @@ func (c *Checker) removeSubtypes(types []*Type, hasObjectTypes bool) []*Type {
2658126595
}
2658226596
}
2658326597
}
26584-
c.subtypeReductionCache[key] = types
26585-
return types
26598+
reduced := c.typeListArena.Clone(types)
26599+
c.subtypeReductionCache[key] = reduced
26600+
return reduced
2658626601
}
2658726602

2658826603
func (c *Checker) intersectTypes(type1 *Type, type2 *Type) *Type {
@@ -26618,10 +26633,10 @@ func (c *Checker) getIntersectionType(types []*Type) *Type {
2661826633
}
2661926634

2662026635
func (c *Checker) getIntersectionTypeEx(types []*Type, flags IntersectionFlags, alias *TypeAlias) *Type {
26621-
var orderedTypes orderedSet[*Type]
26622-
orderedTypes.values = make([]*Type, 0, len(types))
26623-
includes := c.addTypesToIntersection(&orderedTypes, 0, types)
26624-
typeSet := orderedTypes.values
26636+
set := c.getTemporaryTypeSet()
26637+
defer c.releaseTemporaryTypeSet(set)
26638+
includes := c.addTypesToIntersection(&set.orderedSet, 0, types)
26639+
typeSet := set.values
2662526640
objectFlags := ObjectFlagsNone
2662626641
// An intersection type is considered empty if it contains
2662726642
// the type never, or
@@ -27171,7 +27186,11 @@ func (c *Checker) removeType(t *Type, targetType *Type) *Type {
2717127186
return types[1-i]
2717227187
}
2717327188
// Remove the target type from the slice.
27174-
filtered := append(types[:i:i], types[i+1:]...)
27189+
set := c.getTemporaryTypeSet()
27190+
defer c.releaseTemporaryTypeSet(set)
27191+
set.values = append(set.values[:0], types[:i]...)
27192+
set.values = append(set.values, types[i+1:]...)
27193+
filtered := set.values
2717527194
return c.getUnionTypeFromSortedList(filtered, t.AsUnionType().objectFlags&(ObjectFlagsPrimitiveUnion|ObjectFlagsContainsIntersections), nil /*alias*/, nil /*origin*/)
2717627195
}
2717727196
return t

‎tsc/internal/checker/checker_test.go‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,9 @@ func BenchmarkCheckFreshProgram(b *testing.B) {
162162
rootPath := tspath.RootedDirectoryPathFromAbsolute(filepath.Join(repo.TestDataPath(), "fixtures/compiler"))
163163
host := compiler.NewCompilerHost(fs, bundled.LibPath(), nil, nil, nil)
164164
parsed, errors := tsoptions.GetParsedCommandLineOfConfigFile(rootPath.ResolveFile("tsconfig.json"), &core.CompilerOptions{}, nil, fs, nil)
165-
assert.Equal(b, len(errors), 0, "Expected no errors in parsed command line")
165+
for _, diagnostic := range errors {
166+
b.Fatal(diagnostic.String())
167+
}
166168
ctx := b.Context()
167169

168170
b.ReportAllocs()
@@ -171,7 +173,9 @@ func BenchmarkCheckFreshProgram(b *testing.B) {
171173
program.BindSourceFiles()
172174
c, _ := checker.NewChecker(program, nil)
173175
for _, file := range program.GetSourceFiles() {
174-
assert.Equal(b, len(c.GetDiagnostics(ctx, file)), 0)
176+
for _, diagnostic := range c.GetDiagnostics(ctx, file) {
177+
b.Fatal(diagnostic.String())
178+
}
175179
}
176180
assert.Equal(b, len(c.GetGlobalDiagnostics()), 0)
177181
}

‎tsc/internal/checker/types_test.go‎

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package checker
33
import (
44
"encoding/binary"
55
"runtime"
6+
"slices"
67
"strconv"
78
"strings"
89
"testing"
@@ -140,3 +141,133 @@ func BenchmarkTypeListKey(b *testing.B) {
140141
})
141142
}
142143
}
144+
145+
func newCompositeTestChecker() (*Checker, []*Type) {
146+
c := &Checker{
147+
strictNullChecks: true,
148+
unionTypes: make(map[CacheHashKey]*Type),
149+
unionOfUnionTypes: make(map[UnionOfUnionKey]*Type),
150+
intersectionTypes: make(map[CacheHashKey]*Type),
151+
subtypeReductionCache: make(map[CacheHashKey][]*Type),
152+
}
153+
c.compareSymbols = c.compareSymbolsWorker
154+
c.neverType = c.newIntrinsicType(TypeFlagsNever, "never")
155+
c.unknownType = c.newIntrinsicType(TypeFlagsUnknown, "unknown")
156+
types := []*Type{
157+
c.newIntrinsicType(TypeFlagsString, "string"),
158+
c.newIntrinsicType(TypeFlagsNumber, "number"),
159+
c.newIntrinsicType(TypeFlagsESSymbol, "symbol"),
160+
}
161+
return c, types
162+
}
163+
164+
func TestCompositeTypeOwnsConstituents(t *testing.T) {
165+
t.Parallel()
166+
c, types := newCompositeTestChecker()
167+
borrowed := slices.Clone(types)
168+
union := c.getUnionTypeFromSortedList(borrowed, ObjectFlagsNone, nil, nil)
169+
borrowed[0] = c.neverType
170+
assert.Assert(t, slices.Equal(union.Types(), types))
171+
objects := []*Type{
172+
c.newObjectType(ObjectFlagsInterface, nil),
173+
c.newObjectType(ObjectFlagsInterface, nil),
174+
c.newObjectType(ObjectFlagsInterface, nil),
175+
}
176+
intersection := c.getIntersectionType(objects)
177+
objects[0] = c.neverType
178+
assert.Assert(t, intersection.Types()[0] != c.neverType)
179+
reduced := c.removeSubtypes(borrowed, false)
180+
expected := slices.Clone(reduced)
181+
borrowed[0] = types[0]
182+
assert.Assert(t, slices.Equal(reduced, expected))
183+
}
184+
185+
func TestCompositeTypeCacheHitAllocations(t *testing.T) { //nolint:paralleltest
186+
if race.Enabled {
187+
t.Skip("race instrumentation changes hashing allocations")
188+
}
189+
c, types := newCompositeTestChecker()
190+
union := c.getUnionType(types)
191+
filtered := c.removeType(union, types[1])
192+
objects := []*Type{
193+
c.newObjectType(ObjectFlagsInterface, nil),
194+
c.newObjectType(ObjectFlagsInterface, nil),
195+
c.newObjectType(ObjectFlagsInterface, nil),
196+
}
197+
intersection := c.getIntersectionType(objects)
198+
assert.Equal(t, testing.AllocsPerRun(10, func() { c.getUnionType(types) }), float64(0))
199+
assert.Equal(t, testing.AllocsPerRun(10, func() { c.getIntersectionType(objects) }), float64(0))
200+
assert.Equal(t, testing.AllocsPerRun(10, func() { c.mapType(union, func(t *Type) *Type { return t }) }), float64(0))
201+
assert.Equal(t, testing.AllocsPerRun(10, func() {
202+
c.mapType(union, func(t *Type) *Type {
203+
if t == types[1] {
204+
return nil
205+
}
206+
return t
207+
})
208+
}), float64(0))
209+
assert.Equal(t, testing.AllocsPerRun(10, func() { c.removeType(union, types[1]) }), float64(0))
210+
assert.Equal(t, c.removeType(union, types[1]), filtered)
211+
assert.Equal(t, c.getIntersectionType(objects), intersection)
212+
}
213+
214+
func TestTemporaryTypeSetReuse(t *testing.T) {
215+
t.Parallel()
216+
c, primitives := newCompositeTestChecker()
217+
for _, count := range []int{3, 17, 80, 300, 5000, 3} {
218+
objects := make([]*Type, count)
219+
for i := range objects {
220+
objects[i] = c.newObjectType(ObjectFlagsInterface, nil)
221+
}
222+
intersection := c.getIntersectionType(objects)
223+
assert.Assert(t, slices.Equal(intersection.Types(), objects))
224+
union := c.getUnionType(objects)
225+
assert.Assert(t, slices.Equal(union.Types(), objects))
226+
mapped := c.mapType(union, func(typ *Type) *Type {
227+
c.getIntersectionType(objects[:2])
228+
if typ == objects[1] {
229+
return nil
230+
}
231+
return typ
232+
})
233+
expected := slices.Clone(objects)
234+
expected = slices.Delete(expected, 1, 2)
235+
assert.Assert(t, slices.Equal(mapped.Types(), expected))
236+
assert.Equal(t, c.removeType(union, objects[1]), mapped)
237+
assert.Assert(t, c.mapType(union, func(*Type) *Type { return nil }) == nil)
238+
assert.Equal(t, c.getIntersectionType(objects), intersection)
239+
for set := c.freeTypeSets; set != nil; set = set.next {
240+
assert.Equal(t, len(set.values), 0)
241+
assert.Equal(t, len(set.valuesByKey), 0)
242+
assert.Assert(t, cap(set.values) <= 4096)
243+
for _, typ := range set.values[:cap(set.values)] {
244+
assert.Assert(t, typ == nil)
245+
}
246+
for _, typ := range set.inline {
247+
assert.Assert(t, typ == nil)
248+
}
249+
}
250+
}
251+
left := c.getUnionType(primitives[:2])
252+
right := c.getUnionType(primitives[1:])
253+
assert.Equal(t, c.getIntersectionType([]*Type{left, right}), primitives[1])
254+
}
255+
256+
func BenchmarkCompositeTypeCacheHit(b *testing.B) {
257+
c, types := newCompositeTestChecker()
258+
union := c.getUnionType(types)
259+
objects := []*Type{
260+
c.newObjectType(ObjectFlagsInterface, nil),
261+
c.newObjectType(ObjectFlagsInterface, nil),
262+
c.newObjectType(ObjectFlagsInterface, nil),
263+
}
264+
c.getIntersectionType(objects)
265+
c.removeType(union, types[1])
266+
b.ReportAllocs()
267+
for b.Loop() {
268+
c.getUnionType(types)
269+
c.getIntersectionType(objects)
270+
c.mapType(union, func(t *Type) *Type { return t })
271+
c.removeType(union, types[1])
272+
}
273+
}

‎tsc/internal/checker/utilities.go‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -881,6 +881,44 @@ type orderedSet[T comparable] struct {
881881
values []T
882882
}
883883

884+
// Composite type construction often finds an existing interned type. Borrow
885+
// workspace for normalization, and copy constituents only when publishing a
886+
// new type. Each recursive construction borrows a separate set.
887+
type temporaryTypeSet struct {
888+
orderedSet[*Type]
889+
inline [16]*Type
890+
next *temporaryTypeSet
891+
}
892+
893+
func (c *Checker) getTemporaryTypeSet() *temporaryTypeSet {
894+
set := c.freeTypeSets
895+
if set == nil {
896+
set = &temporaryTypeSet{}
897+
set.values = set.inline[:0]
898+
} else {
899+
c.freeTypeSets = set.next
900+
set.next = nil
901+
}
902+
return set
903+
}
904+
905+
func (c *Checker) releaseTemporaryTypeSet(set *temporaryTypeSet) {
906+
clear(set.values)
907+
if cap(set.values) > len(set.inline) {
908+
clear(set.inline[:])
909+
}
910+
clear(set.valuesByKey)
911+
// Do not retain arbitrarily large temporary unions and their dedup maps.
912+
if cap(set.values) > 4096 {
913+
set.values = set.inline[:0]
914+
set.valuesByKey = nil
915+
} else {
916+
set.values = set.values[:0]
917+
}
918+
set.next = c.freeTypeSets
919+
c.freeTypeSets = set
920+
}
921+
884922
func (s *orderedSet[T]) contains(value T) bool {
885923
if s.valuesByKey == nil {
886924
return slices.Contains(s.values, value)

0 commit comments

Comments
 (0)