Reuse the per-method allocation scan for optimization opportunities#2261
Conversation
Performance Triage computed each method's allocation occurrences twice: once in the main path with escape classification (classifyEscapes: true) and again inside CollectOptimizationOpportunities without it (classifyEscapes: false). The two calls run the identical instruction/token scan -- classifyEscapes only controls whether a final ClassifyAllocationEscapes pass runs on top of the same raw occurrences. Scan once (raw, escape = Unknown), classify that result for the main allocation output, and hand the raw occurrences to CollectOptimizationOpportunities so it keys them by IL offset instead of rescanning. The opportunity pass never reads escape state, so it receives exactly the occurrences it computed before -- output is byte-identical. This removes one full allocation scan (instruction walk + newobj/newarr token resolution + size estimation) per method whenever opportunities are computed, i.e. the Performance Triage path. Validated: - Byte-identical CLI: 21 diffs across 7 real framework assemblies (incl. System.Private.CoreLib) for Performance Triage / Allocation Facts / combined, plus type-level Allocation-Safety-Cost and Performance Triage. - SemanticFacts / ParallelBuildDeterminism / IndexBuildInvariant / Targeted / TypeTargeted tests pass. - CPU (total work) on Performance Triage: System.Private.CoreLib 7.45s -> 6.01s (19%), ILInspector.Decompiler 3.45s -> 2.65s (23%); scales with allocation density. Wall-clock is unchanged on an idle box for large assemblies (the path is already parallel across cores), but the reduced CPU work translates to wall-clock under core contention (many concurrent analyses). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adversarial review reconciliationTwo reviewers, rotated from #2247 (GPT-5.5 + MAI) → this round GPT-5.5 + Gemini 3.1 Pro (MAI rotated out; I am Claude and cannot self-review).
Both independently confirmed byte-identical output (empty diffs on System.Private.CoreLib Performance Triage and the requested library/type selectors) and passing tests, and verified:
One reviewer divergence — verifiedGPT-5.5 noted No actionable findings; no resolution commits required. Ready to merge. |
Result: ~20% less CPU work on Performance Triage
Performance Triage computed each method's allocation occurrences twice: once in the main path with escape classification (
classifyEscapes: true), and again insideCollectOptimizationOpportunitieswithout it (classifyEscapes: false). Both run the identical instruction/token scan —classifyEscapesonly controls whether a finalClassifyAllocationEscapespass runs on top of the same raw occurrences.This scans once (raw), classifies that result for the main allocation output, and hands the raw occurrences to the opportunity pass (which keys them by IL offset and never reads escape state) — removing one full allocation scan per method whenever opportunities are computed.
-S "Performance Triage"-S "Performance Triage"Savings scale with allocation density. Note on wall-clock: on an idle box, large-assembly wall-clock is unchanged because that path is already parallel across cores (#2247) and the sequential render/merge dominates its wall time — but the ~20% CPU reduction translates directly to wall-clock under core contention (this environment runs many concurrent analyses).
Why it's byte-identical
classifyEscapes: falsereturns the rawcollectedoccurrences directly;truereturnsClassifyAllocationEscapes(collected, …)on top. So the base occurrences (offsets, kinds, sizes, types) are identical between the two — only the escape field differs. The opportunity pass consumed thefalse(raw,Unknown-escape) occurrences and keys them by IL offset without reading escape state, so handing it the shared raw scan gives it exactly what it computed before.Validation
Risk / review focus
Behavior-preserving. Attack point: does
CollectOptimizationOpportunitiesever read allocation escape state (which would differ between the raw scan it did before and… still the raw scan now — so this is actually a no-op risk), or does the main allocation output still get classification applied exactly as before? Confirm the raw-vs-classified split is faithful.