Skip to content

Commit 59c8a48

Browse files
committed
Store ordinary relationship keys as exact type ID pairs
Ordinary type comparisons need only two 32-bit IDs and intersection context, but retained a 128-bit hash for every cached result. Hashing and storing twice the necessary key width wastes allocations and memory. Keep exact 64-bit pairs in separate maps for the four intersection states. Generic-reference normalization still uses its original full-width hash. An explicit key tag separates both namespaces, including recursion guards, without sacrificing hash bits or changing constrained generic behavior. Six fresh-process interleaved VS Code comparisons against the parent commit, using normal GC, reduce retained heap 0.94% (roughly 33 MiB) and total allocated bytes 1.29%. GC scanning, allocation events, and instruction counts are not statistically different. Tests cover directional and identity comparisons, all intersection contexts, result replacement, exact high-bit IDs, namespace isolation, generic parameter normalization, and constrained versus unconstrained recursion keys. Production diagnostics and all semantic counts match exactly. goos: linux goarch: amd64 pkg: github.com/microsoft/TypeScript/tsc/internal/checker cpu: Intel(R) Core(TM) i9-10900K CPU @ 3.70GHz │ vscode-relation-before-6.txt │ vscode-relation-6.txt │ │ retained-B │ retained-B vs base │ ExternalProgram-20 3.456Gi ± 0% 3.424Gi ± 0% -0.94% (p=0.002 n=6) │ vscode-relation-before-6.txt │ vscode-relation-6.txt │ │ scan-B │ scan-B vs base │ ExternalProgram-20 2.998Gi ± 0% 2.997Gi ± 0% ~ (p=0.310 n=6) │ vscode-relation-before-6.txt │ vscode-relation-6.txt │ │ B/op │ B/op vs base │ ExternalProgram-20 5.021Gi ± 0% 4.956Gi ± 0% -1.29% (p=0.002 n=6) │ vscode-relation-before-6.txt │ vscode-relation-6.txt │ │ allocs/op │ allocs/op vs base │ ExternalProgram-20 31.08M ± 0% 31.07M ± 0% ~ (p=0.240 n=6) │ relation-before-instructions.txt │ relation-instructions.txt │ │ instructions/op │ instructions/op vs base │ ExternalInstructions-20 269.3G ± 2% 270.0G ± 3% ~ (p=0.937 n=6)
1 parent c4938e8 commit 59c8a48

3 files changed

Lines changed: 112 additions & 15 deletions

File tree

‎tsc/internal/checker/checker.go‎

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18070,7 +18070,7 @@ func getConditionalTypeKey(typeArguments []*Type, alias *TypeAlias, forConstrain
1807018070
return b.hash()
1807118071
}
1807218072

18073-
func getRelationKey(source *Type, target *Type, intersectionState IntersectionState, isIdentity bool, ignoreConstraints bool) (CacheHashKey, bool) {
18073+
func getRelationKey(source *Type, target *Type, intersectionState IntersectionState, isIdentity bool, ignoreConstraints bool) (relationKey, bool) {
1807418074
if isIdentity && source.id > target.id {
1807518075
source, target = target, source
1807618076
}
@@ -18080,12 +18080,15 @@ func getRelationKey(source *Type, target *Type, intersectionState IntersectionSt
1808018080
b.writeByte('g')
1808118081
constrained = b.writeGenericTypeReferences(source, target, ignoreConstraints)
1808218082
} else {
18083-
b.writeByte('s')
18084-
b.writeType(source)
18085-
b.writeType(target)
18083+
// Ordinary relationships need only the two exact IDs. Intersection
18084+
// states select separate maps, leaving generic hashes unmodified.
18085+
return relationKey{value: CacheHashKey{
18086+
Lo: uint64(source.id)<<32 | uint64(target.id),
18087+
Hi: uint64(intersectionState),
18088+
}}, false
1808618089
}
1808718090
b.writeUint32(uint32(intersectionState))
18088-
return b.hash(), constrained
18091+
return relationKey{value: b.hash(), generic: true}, constrained
1808918092
}
1809018093

1809118094
func getNodeListKey(nodes []*ast.Node) CacheHashKey {

‎tsc/internal/checker/relater.go‎

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -96,22 +96,43 @@ func asRecursionId[T *ast.Node | *ast.Symbol | *Type](value T) RecursionId {
9696
}
9797

9898
type Relation struct {
99-
results map[CacheHashKey]RelationComparisonResult
99+
genericResults map[CacheHashKey]RelationComparisonResult
100+
simpleResults [4]map[uint64]RelationComparisonResult
100101
}
101102

102-
func (r *Relation) get(key CacheHashKey) RelationComparisonResult {
103-
return r.results[key]
103+
type relationKey struct {
104+
value CacheHashKey
105+
generic bool
104106
}
105107

106-
func (r *Relation) set(key CacheHashKey, result RelationComparisonResult) {
107-
if r.results == nil {
108-
r.results = make(map[CacheHashKey]RelationComparisonResult)
108+
func (r *Relation) get(key relationKey) RelationComparisonResult {
109+
if key.generic {
110+
return r.genericResults[key.value]
111+
}
112+
return r.simpleResults[key.value.Hi][key.value.Lo]
113+
}
114+
115+
func (r *Relation) set(key relationKey, result RelationComparisonResult) {
116+
if key.generic {
117+
if r.genericResults == nil {
118+
r.genericResults = make(map[CacheHashKey]RelationComparisonResult)
119+
}
120+
r.genericResults[key.value] = result
121+
} else {
122+
results := &r.simpleResults[key.value.Hi]
123+
if *results == nil {
124+
*results = make(map[uint64]RelationComparisonResult)
125+
}
126+
(*results)[key.value.Lo] = result
109127
}
110-
r.results[key] = result
111128
}
112129

113130
func (r *Relation) size() int {
114-
return len(r.results)
131+
size := len(r.genericResults)
132+
for _, results := range r.simpleResults {
133+
size += len(results)
134+
}
135+
return size
115136
}
116137

117138
func (c *Checker) isTypeIdenticalTo(source *Type, target *Type) bool {
@@ -2584,8 +2605,8 @@ type Relater struct {
25842605
errorNode *ast.Node
25852606
errorChain *ErrorChain
25862607
relatedInfo []*ast.Diagnostic
2587-
maybeKeys []CacheHashKey
2588-
maybeKeysSet collections.Set[CacheHashKey]
2608+
maybeKeys []relationKey
2609+
maybeKeysSet collections.Set[relationKey]
25892610
sourceStack []*Type
25902611
targetStack []*Type
25912612
maybeCount int

‎tsc/internal/checker/types_test.go‎

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,79 @@ func TestKeyBuilder(t *testing.T) {
153153
}
154154
}
155155

156+
func TestRelationKeys(t *testing.T) {
157+
t.Parallel()
158+
source := &Type{id: 1}
159+
target := &Type{id: ^TypeId(0)}
160+
var relation Relation
161+
assert.Equal(t, relation.size(), 0)
162+
for state := IntersectionStateNone; state <= IntersectionStateSource|IntersectionStateTarget; state++ {
163+
key, constrained := getRelationKey(source, target, state, false, false)
164+
assert.Assert(t, !constrained)
165+
assert.Equal(t, relation.get(key), RelationComparisonResultNone)
166+
relation.set(key, RelationComparisonResultSucceeded)
167+
reverse, _ := getRelationKey(target, source, state, false, false)
168+
assert.Assert(t, key != reverse)
169+
assert.Equal(t, relation.get(reverse), RelationComparisonResultNone)
170+
identity, _ := getRelationKey(target, source, state, true, false)
171+
assert.Equal(t, key, identity)
172+
relation.set(key, RelationComparisonResultFailed|RelationComparisonResultReportsUnreliable)
173+
assert.Equal(t, relation.get(key), RelationComparisonResultFailed|RelationComparisonResultReportsUnreliable)
174+
}
175+
assert.Equal(t, relation.size(), 4)
176+
for state := IntersectionStateNone; state <= IntersectionStateSource|IntersectionStateTarget; state++ {
177+
key, _ := getRelationKey(source, target, state, false, false)
178+
assert.Equal(t, relation.get(key), RelationComparisonResultFailed|RelationComparisonResultReportsUnreliable)
179+
}
180+
}
181+
182+
func TestRelationKeyNamespaces(t *testing.T) {
183+
t.Parallel()
184+
simple, _ := getRelationKey(&Type{id: 1}, &Type{id: 2}, IntersectionStateNone, false, false)
185+
generic := relationKey{value: simple.value, generic: true}
186+
var relation Relation
187+
relation.set(simple, RelationComparisonResultSucceeded)
188+
assert.Equal(t, relation.get(generic), RelationComparisonResultNone)
189+
relation.set(generic, RelationComparisonResultFailed)
190+
assert.Equal(t, relation.get(simple), RelationComparisonResultSucceeded)
191+
assert.Equal(t, relation.get(generic), RelationComparisonResultFailed)
192+
assert.Equal(t, relation.size(), 2)
193+
}
194+
195+
func TestGenericRelationKeys(t *testing.T) {
196+
t.Parallel()
197+
c, types := newCompositeTestChecker()
198+
c.noConstraintType = c.newIntrinsicType(TypeFlagsNever, "no constraint")
199+
root := types[0]
200+
newParameter := func(constraint *Type) *Type {
201+
parameter := &TypeParameter{constraint: constraint}
202+
return c.newType(TypeFlagsTypeParameter, ObjectFlagsNone, parameter.AsType(), typeKindTypeParameter)
203+
}
204+
newReference := func(argument *Type) *Type {
205+
reference := &TypeReference{resolvedTypeArguments: []*Type{argument}}
206+
reference.target = root
207+
return c.newType(TypeFlagsObject, ObjectFlagsReference, reference.AsType(), typeKindTypeReference)
208+
}
209+
left := newReference(newParameter(c.noConstraintType))
210+
right := newReference(newParameter(c.noConstraintType))
211+
equivalentLeft := newReference(newParameter(c.noConstraintType))
212+
equivalentRight := newReference(newParameter(c.noConstraintType))
213+
key, constrained := getRelationKey(left, right, IntersectionStateNone, false, false)
214+
assert.Assert(t, !constrained)
215+
equivalent, _ := getRelationKey(equivalentLeft, equivalentRight, IntersectionStateNone, false, false)
216+
assert.Equal(t, key, equivalent)
217+
var relation Relation
218+
relation.set(key, RelationComparisonResultSucceeded)
219+
assert.Equal(t, relation.get(equivalent), RelationComparisonResultSucceeded)
220+
constrainedLeft := newReference(newParameter(types[1]))
221+
constrainedKey, constrained := getRelationKey(constrainedLeft, right, IntersectionStateNone, false, false)
222+
assert.Assert(t, constrained)
223+
assert.Assert(t, constrainedKey != key)
224+
broadest, constrained := getRelationKey(constrainedLeft, right, IntersectionStateNone, false, true)
225+
assert.Assert(t, !constrained)
226+
assert.Equal(t, broadest, key)
227+
}
228+
156229
func TestTypeListKeyAllocations(t *testing.T) { //nolint:paralleltest
157230
if race.Enabled {
158231
t.Skip("race instrumentation changes hashing allocations")

0 commit comments

Comments
 (0)