Repository navigation
Use small slices for union and intersection property sources - #64816
Closed
Jake Bailey (jakebailey) wants to merge 17 commits into
Closed
Jake Bailey (jakebailey) wants to merge 17 commits into
Jake Bailey (jakebailey) wants to merge 17 commits into
Conversation
Jake Bailey (jakebailey)
added this pull request to stack #64806
October 14, 2026 14:20
The compact compiler layouts depend on every header belonging to the concrete allocation identified by its tag. Unchecked kinds, retagging, and detached headers can otherwise turn a routine mistake into an invalid memory access. Keep that invariant mechanically enforced without adding layout tags or checks to hot accessors.
The owning packages may allocate records directly, but a fresh record carries a zero tag until newType, newNode, or its mapper tag assignment runs. If it escaped before then, the mistake surfaced only as a later cast panic. Fresh allocations must now be upcast straight into their constructor, or held in a local used only through its own fields until that upcast; mappers must be tagged immediately.
A zero-value node or type record has never been tagged, and returning its address yields a compiler record that skipped checked construction. Named or aliased unsafe.Pointer types also hid reinterpreting conversions from the cast boundary. Both left holes in the invariant the analyzer exists to enforce.
Enumerating copy and declaration forms one at a time left gaps: range assignments could retag records, named results and parameters could carry untagged zero values, and index, receive, and call expressions could copy a record off its header. Checking every value-producing expression and signature for records held by value closes the class rather than individual forms.
A record copied into a range variable or a value receiver detaches from the arena slot that owns its tagged header, so later writes are lost and reads can observe a stale kind. Those declarations never appear as typed expressions, so the copy check could not see them. Checking every defined variable also covers parameters and results, which replaces the narrower function signature rule.
Mapper storage held by value starts with a zero tag. The lint exempted mappers from its zero-value rule so coallocated inference mappers could live inside their owning context, but that also let a standalone mapper, or the address of untagged coallocated storage, escape and later panic in Map on kind 0. The exemption now applies only to mappers stored as struct fields, and their storage may only be addressed, explicitly or via a pointer-receiver method call, as an argument to an init*TypeMapper function that tags it.
A local that shadows the constructor's kind parameter passed the spelling-only check, so newType or newNode could tag a header with an unrelated value while the lint reported checked construction. Bulk copies through copy or variadic append also duplicated records by value without any diagnostic, because their arguments are slices.
Mapper kind constants are now public TypeMapperKind values, so the lint's pairing between concrete mappers and their kind constants has to look them up by the new names.
Dense checker link stores still keep a pointer for every occupied slot. Those tables cost memory and require GC scanning even though values already live in arenas. Use four-byte indexes in the page tables. Retain bounded arena blocks so references stay stable without geometrically over-allocating large stores. Keep map-backed stores unchanged: compressing those did not improve allocation usage.
A presence query needs only the page entry, not its value. Resolving an occupied index adds arena lookup work to a boolean check. Keep presence checks on the compressed page table, and apply the repository linter fixes to the link-store tests.
The checker's link-store wrappers answered Has by calling TryGet, so presence checks still resolved arena indexes even though the paged store can answer from the page entry alone. Route them to the store presence checks so checker queries get the cheaper path.
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.
The name index and the instantiated fast path both assumed a table whose properties are found under their own names has no other keys. A symbol stored under its own name and an alias satisfies every reverse lookup, so the alias was silently dropped. Checking each (key, symbol) pair directly keeps such tables on the map path.
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)
Complete non-augmented properties are valid for augmented lookup, but
copying each result into both caches retains duplicate name tables.
MUI keeps 2,734,919 identical entries this way, nearly doubling its
union/intersection cache storage without adding semantic information.
Preserve independent augmented results and partial-property behavior
while sharing complete entries. A failed lookup needs no empty table.
The MUI census falls from 230242 tables and 5628201 entries to 116635
tables and 2893282 entries; all distinct cached identities remain.
Six fresh-process interleaved single-checker comparisons against the
parent commit, with ordinary GC, save 135.63 MiB of retained MUI heap
(-8.15%) and 132.00 MiB of GC scanning (-9.22%). Total allocated bytes
fall 4.45% and allocation events fall 1.46%. VS Code's corresponding
reductions are small: retained -0.15%, scan -0.17%, allocated -0.16%.
Instruction differences in these single-checker comparisons are
inconclusive; elapsed time is noisy on the shared machine.
Six default-threaded production CLI pairs confirm MUI's reported heap
falls 9.29% and instructions fall 2.60%. VS Code and Webpack have small
heap reductions and inconclusive instruction differences. These are
incremental savings on top of the earlier branch optimizations.
CLI-reported heap is not the forced-GC retained-heap measure above.
MUI uses the CI-pinned source and restored scenario dependencies at
6780195595c12252a130aa5ae5c5bd461229c6b7. VS Code uses
8bf178e1e1c2be3723f14683f7b1e5fdb3f780e4. Webpack uses an isolated
2289141381f2c1c965cb65118fa0ffe2e9e19834 with restored dependencies.
Production diagnostics and all semantic counts match exactly.
Tests cover both query orders, both composite kinds, cache growth and
GC reachability, missing entries, read/write partial flags, differing
mode-specific identities, and real function-property augmentation.
The storage regressions fail before this change.
Single-checker retained heap and allocation measurements:
│ mui-shared-union-before.txt │ mui-shared-union-shared.txt │
│ retained-B │ retained-B vs base │
ExternalMemory-20 1.624Gi ± 0% 1.492Gi ± 0% -8.15% (p=0.002 n=6)
│ mui-shared-union-before.txt │ mui-shared-union-shared.txt │
│ scan-B │ scan-B vs base │
ExternalMemory-20 1.398Gi ± 0% 1.270Gi ± 0% -9.22% (p=0.002 n=6)
│ mui-shared-union-before.txt │ mui-shared-union-shared.txt │
│ B/op │ B/op vs base │
ExternalMemory-20 5.364Gi ± 0% 5.125Gi ± 0% -4.45% (p=0.002 n=6)
│ mui-shared-union-before.txt │ mui-shared-union-shared.txt │
│ allocs/op │ allocs/op vs base │
ExternalMemory-20 25.17M ± 0% 24.80M ± 1% -1.46% (p=0.002 n=6)
│ vscode-shared-union-before.txt │ vscode-shared-union-shared.txt │
│ retained-B │ retained-B vs base │
ExternalMemory-20 2.619Gi ± 0% 2.615Gi ± 0% -0.15% (p=0.002 n=6)
│ vscode-shared-union-before.txt │ vscode-shared-union-shared.txt │
│ scan-B │ scan-B vs base │
ExternalMemory-20 2.291Gi ± 0% 2.287Gi ± 0% -0.17% (p=0.002 n=6)
│ vscode-shared-union-before.txt │ vscode-shared-union-shared.txt │
│ B/op │ B/op vs base │
ExternalMemory-20 3.754Gi ± 0% 3.748Gi ± 0% -0.16% (p=0.002 n=6)
│ vscode-shared-union-before.txt │ vscode-shared-union-shared.txt │
│ allocs/op │ allocs/op vs base │
ExternalMemory-20 23.61M ± 0% 23.59M ± 0% -0.08% (p=0.002 n=6)
Single-checker instructions:
│ mui-shared-union-before.instructions │ mui-shared-union-shared.instructions │
│ instructions/op │ instructions/op vs base │
ExternalInstructions-20 218.9G ± 3% 217.3G ± 2% ~ (p=0.310 n=6)
│ vscode-shared-union-before.instructions │ vscode-shared-union-shared.instructions │
│ instructions/op │ instructions/op vs base │
ExternalInstructions-20 188.8G ± 3% 192.1G ± 2% ~ (p=0.394 n=6)
Default-threaded production CLI:
│ shared-union-cli-before.txt │ shared-union-cli-after.txt │
│ instructions/op │ instructions/op vs base │
mui-20 283.3G ± 1% 275.9G ± 2% -2.60% (p=0.015 n=6)
vscode-20 308.2G ± 7% 293.0G ± 6% ~ (p=0.818 n=6)
webpack-20 51.96G ± 5% 51.82G ± 4% ~ (p=1.000 n=6)
geomean 165.6G 161.2G -2.62%
│ shared-union-cli-before.txt │ shared-union-cli-after.txt │
│ heap-B/op │ heap-B/op vs base │
mui-20 2.059Gi ± 0% 1.868Gi ± 0% -9.29% (p=0.002 n=6)
vscode-20 2.835Gi ± 0% 2.829Gi ± 0% -0.21% (p=0.004 n=6)
webpack-20 666.9Mi ± 0% 663.0Mi ± 0% -0.57% (p=0.002 n=6)
geomean 1.561Gi 1.507Gi -3.45%
│ shared-union-cli-before.txt │ shared-union-cli-after.txt │
│ allocs/op │ allocs/op vs base │
mui-20 29.84M ± 0% 29.55M ± 0% -0.97% (p=0.002 n=6)
vscode-20 36.67M ± 2% 36.64M ± 0% ~ (p=0.132 n=6)
webpack-20 6.235M ± 2% 6.208M ± 0% -0.43% (p=0.002 n=6)
geomean 18.97M 18.87M -0.49%
│ shared-union-cli-before.txt │ shared-union-cli-after.txt │
│ check-sec/op │ check-sec/op vs base │
mui-20 8.986 ± 6% 8.559 ± 4% -4.75% (p=0.026 n=6)
vscode-20 8.345 ± 4% 8.900 ± 10% ~ (p=0.132 n=6)
webpack-20 1.851 ± 8% 1.818 ± 8% ~ (p=0.310 n=6)
geomean 5.178 5.173 -0.08%
getReducedType runs somePropertyReducesToNever on intersections. It
used to create and cache a synthetic property for every property name
shared by more than one constituent, only to inspect its flags. With
large JSX prop intersections, as in MUI, most of those properties were
never looked up again, yet they stayed in the intersection's property
cache with their symbols and links.
Collect the constituent properties without building the combined
symbol, and only create the real property in the rare merged
instantiation case.
mui-docs:
│ before │ after │
│ memory-used-B │ memory-used-B vs base │
MUIDocs 1.857Gi ± 0% 1.717Gi ± 0% -7.55% (p=0.002 n=6)
│ before │ after │
│ allocs │ allocs vs base │
MUIDocs 27.29M ± 0% 25.19M ± 0% -7.72% (p=0.002 n=6)
│ before │ after │
│ allocated-B │ allocated-B vs base │
MUIDocs 4.730Gi ± 0% 4.556Gi ± 0% -3.68% (p=0.002 n=6)
│ before │ after │
│ symbols │ symbols vs base │
MUIDocs 4.332M ± 0% 3.663M ± 0% -15.43% (p=0.002 n=6)
Synthesizing or probing a union or intersection property gathers the
distinct constituent properties into a set. That set rarely holds more
than a handful of symbols, but collections.OrderedSet always allocates
both a map and a key slice, and this runs for every property of every
combined type that is enumerated during relation checks.
Keep the distinct properties in a plain slice, and index them with a
map only once the slice passes the same threshold used by orderedSet.
mui-docs:
│ before │ after │
│ allocs │ allocs vs base │
MUIDocs 25.19M ± 0% 23.32M ± 1% -7.41% (p=0.002 n=6)
│ before │ after │
│ allocated-B │ allocated-B vs base │
MUIDocs 4.556Gi ± 0% 4.389Gi ± 1% -3.66% (p=0.002 n=6)
Jake Bailey (jakebailey)
force-pushed
the
jakebailey/mem/small-property-source-sets
branch
from
October 14, 2026 16:02
3839c69 to
e8b256d
Compare
Jake Bailey (jakebailey)
removed this pull request from stack #64806
October 14, 2026 16:55
Jake Bailey (jakebailey)
force-pushed
the
jakebailey/mem/never-reduction-probes
branch
from
October 14, 2026 16:56
2473949 to
e2fcf0f
Compare
Member
Author
|
Folded into #64815. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Synthesizing or probing a union or intersection property collects the distinct constituent properties in a
collections.OrderedSet, which always allocates a map and a key slice even though it rarely holds more than a handful of symbols.orderedSetuses.Part of the memory optimization stack summarized in #64734.