[ty] Avoid storing constraint nodes twice - #28375
Conversation
Typing conformance resultsNo changes detected ✅Current numbersThe percentage of diagnostics emitted that were expected errors held steady at 97.84%. The percentage of expected errors that received a diagnostic held steady at 96.45%. The number of fully passing files held steady at 128/145. |
Memory usage reportMemory usage unchanged ✅ |
|
Merging this PR will improve performance by 18.9%
Performance Changes
Tip Curious why performance improved? Use the CodSpeed MCP and ask your agent. Comparing Footnotes
|
281966c to
ae61600
Compare
ae61600 to
5134676
Compare
|
Codspeed and codex seem very happy about this. I don't know if this memory improvement is "real", though -- the structs optimised here are only temporary builders that are dropped pretty soon after they've served their purpose. The memory and performance improvements only show up on microbenchmarks, not on any real-world benchmarks. |
|
Okay, I asked codex to dig in a bit more into how worthwhile this change really would be, and this seems more convincing to me. Codex analysisI’d merge it as a reduction in peak memory during indexing. The savings generalize beyond the existing benchmarks, including to a real pathological file. Broad speed improvements are much less convincing. I found no correctness blocker in the four-file diff. 1. The memory improvement is realI compared current
Input | Peak RSS before → after | Reduction
-- | -- | --
Black’s src | 98.6 → 96.7 MiB | 1.9%
typing_extensions’ src | 85.1 → 83.5 MiB | 1.8%
Original reproducer: 6,000 statement calls | 43.8 → 43.4 MiB | 0.7%
320 conditional assignments | 71.6 → 58.8 MiB | 17.9%
640 conditional assignments | 197.2 → 141.2 MiB | 28.4%
640 conditional assignments under suppression | 221.3 → 160.0 MiB | 27.7%
800 locals and 800 calls inside try | 111.2 → 85.5 MiB | 23.1%
CPU improvements were approximately 1–4% for the two projects and conditional-assignment cases, and 11% for the The improvements at twice the existing benchmark size are useful evidence against overfitting to its exact parameters. 2. It also helped an actual pathological source fileI traced the reachability graph limit back to Tauon’s enormous event loop, which motivated #23399. Running the original, pinned 49,912-line
That comparison used three measured runs after warmup. I checked the file in isolation without Tauon’s full dependency environment, so this establishes a benefit on actual pathological source, not a complete Tauon project measurement. Diagnostics matched between versions. For the generated Qt UI problem behind #3986, however, I would expect little additional benefit: its 6,000-call reproducer was nearly unchanged, as was the variant with an already-narrowed binding. The expensive repeated graph evaluation that originally motivated that issue was addressed by earlier changes. 3. Being temporary doesn’t make these allocations insignificantEach node contains four Also, builders for the file’s scopes remain alive until the entire file has been visited and its use-def maps are finalized. These allocations can therefore overlap substantially and determine peak memory. Builder lifetime. The unchanged retained-memory report is expected: the final graph representation is unchanged. 4. The limits are importantThis preserves the same graph construction, node counts, operation caches, and node limits. It doesn’t improve asymptotic complexity or delay the precision fallback, because that fallback counts nodes rather than bytes. It makes large graphs cheaper to hold while constructing them. Existing limit. Smaller tables can reduce allocation and memory-access costs, but lookups now introduce an extra indirection into the node vector. Consequently, a universal CPU improvement doesn’t follow from the implementation. My assessment: the implementation is a general, reasonably small storage optimization. The benchmark headline overstates how broadly its gains apply, but the memory saving itself is useful—especially for unusually large control-flow graphs. A pinned Tauon project benchmark would provide stronger ongoing evidence than additional variations of the current synthetic loops. I’d merge it as a reduction in peak memory during indexing. The savings generalize beyond the existing benchmarks, including to a real pathological file. Broad speed improvements are much less convincing. I found no correctness blocker in the four-file diff. 1. The memory improvement is real I compared current main (a159176) against that same revision with this PR’s patch applied, avoiding unrelated differences from the PR branch being behind main. These are fresh-process, single-threaded profiling builds on macOS arm64, using five measured runs per version after warmup. Input Peak RSS before → after Reduction Black’s src 98.6 → 96.7 MiB 1.9% typing_extensions’ src 85.1 → 83.5 MiB 1.8% Original reproducer: 6,000 statement calls 43.8 → 43.4 MiB 0.7% 320 conditional assignments 71.6 → 58.8 MiB 17.9% 640 conditional assignments 197.2 → 141.2 MiB 28.4% 640 conditional assignments under suppression 221.3 → 160.0 MiB 27.7% 800 locals and 800 calls inside try 111.2 → 85.5 MiB 23.1%CPU improvements were approximately 1–4% for the two projects and conditional-assignment cases, and 11% for the try case. The smaller changes are insufficient evidence for promising a general speedup.
|
dcreager
left a comment
There was a problem hiding this comment.
Interesting! I think this is a good pattern that we might want to apply elsewhere too. But I don't think it's worth prematurely generalizing in this PR.
I looked into whether we could do this kind of thing in a few other places in #28647, but it doesn't look convincing right now :/ |
…aliases * origin/main: Bump version to 0.16.8 (#28648) [ty] Bound aliased intersection expansion during inference (#28546) renovate: update uv hashes correctly with setup-uv (#28621) [ty] Compact reachable binding and declaration histories (#28349) [ty] Avoid storing constraint nodes twice (#28375) [ty] Compare bound-method receivers before signatures (#28384) [`flake8-type-checking`] Prefer lazy imports over `TYPE_CHECKING` on 3.15+ (`TC001`, `TC002`, `TC003`) (#28541) [ty] Watch script dependencies in CLI watch mode (#28125) [flake8-tidy-imports] Add `extend-banned-api` (#28644) [ty] Support `type[A & B]` (#27124) # Conflicts: # crates/ty_python_semantic/src/types/set_theoretic/builder.rs
Summary
Building large reachability and narrowing graphs stores each interior node twice: once in
interiorsand again as the key of the interning map. Store only the node ID in ahashbrown::HashTable, usinginteriorsfor hashing and equality. This reduces temporary memory during semantic-index construction while preserving the resulting nodes and their IDs.Performance
Median peak RSS across four runs of profiling CLI builds on macOS arm64, using the existing microbenchmark inputs:
repeated_narrowed_assignmentsrepeated_narrowed_assignments_suppressing_context_managersrepeated_statement_calls_in_tryCPU time fell by 3–11% in these three cases and was within ±4% in the remaining measured cases. These savings are in peak memory; the retained constraint graphs are unchanged.