diff --git a/tsc/internal/checker/checker.go b/tsc/internal/checker/checker.go index 10ddb3c0c88b4..5f650b74fa71b 100644 --- a/tsc/internal/checker/checker.go +++ b/tsc/internal/checker/checker.go @@ -920,7 +920,7 @@ type Checker struct { ctx context.Context packagesMap map[string]bool activeMappers []*TypeMapper - activeTypeMappersCaches []map[CacheHashKey]*Type + activeTypeMappersCaches []clearableMap[*Type] ambientModulesOnce sync.Once ambientModules []*ast.Symbol withinUnreachableCode bool @@ -22624,16 +22624,15 @@ func (c *Checker) instantiateTypeWithAlias(t *Type, m *TypeMapper, alias *TypeAl return c.errorType } index := c.findActiveMapper(m) + var key CacheHashKey if index == -1 { + // A newly pushed mapper has an empty cache, and the result is not cached below. c.pushActiveMapper(m) - } - var b keyBuilder - b.writeType(t) - b.writeAlias(alias) - key := b.hash() - cache := c.activeTypeMappersCaches[core.IfElse(index != -1, index, len(c.activeTypeMappersCaches)-1)] - if cachedType, ok := cache[key]; ok { - return cachedType + } else { + key = getActiveMapperCacheKey(t, alias) + if cachedType, ok := c.activeTypeMappersCaches[index].get(key); ok { + return cachedType + } } c.TotalInstantiationCount++ c.instantiationCount++ @@ -22642,13 +22641,25 @@ func (c *Checker) instantiateTypeWithAlias(t *Type, m *TypeMapper, alias *TypeAl if index == -1 { c.popActiveMapper() } else { - cache[key] = result + c.activeTypeMappersCaches[index].set(key, result) } c.instantiationStack[len(c.instantiationStack)-1] = nil c.instantiationStack = c.instantiationStack[:len(c.instantiationStack)-1] return result } +// Without an alias the key is just the type ID, which avoids hashing. Hashed keys have +// a nonzero high half with overwhelming probability, so the two kinds do not collide. +func getActiveMapperCacheKey(t *Type, alias *TypeAlias) CacheHashKey { + if alias == nil { + return CacheHashKey{Lo: uint64(t.id)} + } + var b keyBuilder + b.writeType(t) + b.writeAlias(alias) + return b.hash() +} + func (c *Checker) getCircularTypeNames() []string { typeCounts := make(map[*Type]int) var circularTypeNames []string @@ -22674,13 +22685,10 @@ func (c *Checker) pushActiveMapper(mapper *TypeMapper) { lastIndex := len(c.activeTypeMappersCaches) if cap(c.activeTypeMappersCaches) > lastIndex { - // The cap may contain an empty map from popActiveMapper; reuse it. + // The cap may contain an empty cache from popActiveMapper; reuse it. c.activeTypeMappersCaches = c.activeTypeMappersCaches[:lastIndex+1] - if c.activeTypeMappersCaches[lastIndex] == nil { - c.activeTypeMappersCaches[lastIndex] = make(map[CacheHashKey]*Type, 1) - } } else { - c.activeTypeMappersCaches = append(c.activeTypeMappersCaches, make(map[CacheHashKey]*Type, 1)) + c.activeTypeMappersCaches = append(c.activeTypeMappersCaches, clearableMap[*Type]{}) } } @@ -22688,9 +22696,9 @@ func (c *Checker) popActiveMapper() { c.activeMappers[len(c.activeMappers)-1] = nil c.activeMappers = c.activeMappers[:len(c.activeMappers)-1] - // Clear the map, but leave it in the list for later reuse. + // Clear the cache, but leave it in the list for later reuse. lastIndex := len(c.activeTypeMappersCaches) - 1 - clear(c.activeTypeMappersCaches[lastIndex]) + c.activeTypeMappersCaches[lastIndex].clear() c.activeTypeMappersCaches = c.activeTypeMappersCaches[:lastIndex] } @@ -22699,8 +22707,8 @@ func (c *Checker) findActiveMapper(mapper *TypeMapper) int { } func (c *Checker) clearActiveMapperCaches() { - for _, cache := range c.activeTypeMappersCaches { - clear(cache) + for i := range c.activeTypeMappersCaches { + c.activeTypeMappersCaches[i].clear() } } diff --git a/tsc/internal/checker/clearablemap.go b/tsc/internal/checker/clearablemap.go new file mode 100644 index 0000000000000..08564219e70b7 --- /dev/null +++ b/tsc/internal/checker/clearablemap.go @@ -0,0 +1,87 @@ +package checker + +import "math/bits" + +// clearableMap is a hash table keyed by 128-bit keys that can be cleared in constant time. It +// is used for short-lived caches that are cleared and reused many times: a Go map takes time +// proportional to its capacity to clear, which stays large once it has grown. Here each entry +// records the generation it was written in, and clearing starts a new one. +type clearableMap[V any] struct { + entries []clearableMapEntry[V] + generation uint32 + count int + shift uint8 +} + +type clearableMapEntry[V any] struct { + key CacheHashKey + value V + generation uint32 +} + +func (m *clearableMap[V]) slot(key CacheHashKey) int { + return int(((key.Lo ^ key.Hi) * 0x9e3779b97f4a7c15) >> m.shift) +} + +func (m *clearableMap[V]) get(key CacheHashKey) (V, bool) { + var zero V + if m.count == 0 { + return zero, false + } + mask := len(m.entries) - 1 + for i := m.slot(key); ; i = (i + 1) & mask { + e := &m.entries[i] + if e.generation != m.generation { + return zero, false + } + if e.key == key { + return e.value, true + } + } +} + +func (m *clearableMap[V]) set(key CacheHashKey, value V) { + if (m.count+1)*4 > len(m.entries)*3 { + m.grow() + } + mask := len(m.entries) - 1 + for i := m.slot(key); ; i = (i + 1) & mask { + e := &m.entries[i] + if e.generation != m.generation { + *e = clearableMapEntry[V]{key: key, value: value, generation: m.generation} + m.count++ + return + } + if e.key == key { + e.value = value + return + } + } +} + +func (m *clearableMap[V]) grow() { + old := m.entries + oldGeneration := m.generation + size := max(len(old)*2, 8) + m.entries = make([]clearableMapEntry[V], size) + m.shift = uint8(64 - bits.TrailingZeros(uint(size))) + m.generation = 1 + m.count = 0 + for i := range old { + if e := &old[i]; e.generation == oldGeneration { + m.set(e.key, e.value) + } + } +} + +func (m *clearableMap[V]) clear() { + if m.count == 0 { + return + } + m.count = 0 + m.generation++ + if m.generation == 0 { + clear(m.entries) + m.generation = 1 + } +} diff --git a/tsc/internal/checker/inference.go b/tsc/internal/checker/inference.go index 02c7d009b9ec5..9fadf41334ef7 100644 --- a/tsc/internal/checker/inference.go +++ b/tsc/internal/checker/inference.go @@ -26,7 +26,7 @@ type InferenceState struct { bivariant bool expandingFlags ExpandingFlags propagationType *Type - visited map[InferenceKey]InferencePriority + visited clearableMap[InferencePriority] sourceStack []*Type targetStack []*Type next *InferenceState @@ -42,7 +42,7 @@ func (c *Checker) getInferenceState() *InferenceState { } func (c *Checker) putInferenceState(n *InferenceState) { - clear(n.visited) + n.visited.clear() *n = InferenceState{ inferences: n.inferences[:0], visited: n.visited, @@ -344,15 +344,15 @@ func (c *Checker) inferFromContravariantTypesIfStrictFunctionTypes(n *InferenceS // such that we would go on inferring forever, even though we would never infer // between the same pair of types. func (c *Checker) invokeOnce(n *InferenceState, source *Type, target *Type, action func(c *Checker, n *InferenceState, source *Type, target *Type)) { - key := InferenceKey{source: source.id, target: target.id, priority: n.priority, contravariant: n.contravariant, bivariant: n.bivariant} - if status, ok := n.visited[key]; ok { + key := CacheHashKey{ + Lo: uint64(source.id)<<32 | uint64(target.id), + Hi: uint64(uint32(n.priority))<<2 | uint64(core.IfElse(n.contravariant, 2, 0)) | uint64(core.IfElse(n.bivariant, 1, 0)), + } + if status, ok := n.visited.get(key); ok { n.inferencePriority = min(n.inferencePriority, status) return } - if n.visited == nil { - n.visited = make(map[InferenceKey]InferencePriority) - } - n.visited[key] = InferencePriorityCircularity + n.visited.set(key, InferencePriorityCircularity) saveInferencePriority := n.inferencePriority n.inferencePriority = InferencePriorityMaxValue // We stop inferring and report a circularity if we encounter duplicate recursion identities on both @@ -374,7 +374,7 @@ func (c *Checker) invokeOnce(n *InferenceState, source *Type, target *Type, acti n.targetStack = n.targetStack[:len(n.targetStack)-1] n.sourceStack = n.sourceStack[:len(n.sourceStack)-1] n.expandingFlags = saveExpandingFlags - n.visited[key] = n.inferencePriority + n.visited.set(key, n.inferencePriority) n.inferencePriority = min(n.inferencePriority, saveInferencePriority) }