Skip to content

JIT: Memoize non-null assertion PHI walks - #134916

Closed
Garume wants to merge 2 commits into
dotnet:mainfrom
Garume:jit/bound-nonnull-assertion-walks
Closed

Garume wants to merge 2 commits into
dotnet:mainfrom
Garume:jit/bound-nonnull-assertion-walks

Conversation

@Garume

@Garume Garume commented Sep 30, 2026 •

Copy link
Copy Markdown

Fixes #134462.

Repeated non-null PHI queries can revisit the same subgraph many times. This change caches completed PHI-walk results within each top-level non-null query. The cache is keyed by value number and exact remaining depth; the original depth limit of 10 is unchanged.

Direct proofs from the incoming assertion set are still checked on every call. Only the subsequent PHI walk is cached: it obtains assertions from the PHI's own predecessor edges, independently of the caller's incoming assertion set. Both successful and unsuccessful completed walks are stored. Recursive calls share the cache, and entries are read again after recursion so results recorded at other depths are preserved. The shared reaching-assertion visitor and subtype queries are unchanged.

The regression test covers the issue reproduction, nullable merges, loops, exceptions and independent queries. It remains a priority-1 test.

JIT compilation time and peak process RSS

Apple M4 Pro, macOS 26.5.1, ARM64. Baseline: 0fc86e67b3ed75a19c0bb34afdf76712d6b36c18. Matching runtime hosts were used, with only the JIT replaced for the candidate. Values below are medians from six fresh-process pairs for the N10 reproduction. Time measures RuntimeHelpers.PrepareMethod; RSS is whole-process peak resident memory, not managed allocation.

Configuration Base JIT time Patch JIT time Base peak RSS Patch peak RSS
Release 4,953.237 ms 0.242 ms 3,055.6 MB 33.8 MB
Checked 11,873.716 ms 0.867 ms 3,058.2 MB 37.9 MB

Small N2 compile times were essentially unchanged. The evidence also includes N6 and all individual measurements.

Validation

  • Release and Checked JIT builds passed.
  • The priority-1 regression passed with both JITs in Release and Checked, plus candidate Checked runs with JitStress=1, JitStress=2 and JitMinOpts=1.
  • Separate semantic runs passed 742,812 result/exception checks; the BDN workload preflight passed another 348,732 checks. These suites overlap in purpose; the counts are not distinct test cases.
  • Checked SuperPMI replayed 96,632 contexts from the realworld and benchmarks collections with no failures, missing contexts or codegen differences.
  • Corpus compiler CPU B/A ratios were 0.9887 for realworld (approximate paired 95% interval 0.9666 to 1.0112) and 0.9906 for benchmarks (0.9809 to 1.0005), eight pairs each. These do not establish a general compiler throughput improvement.
  • Native formatting checks passed. The clang-tidy check used the native Checked translation unit, parsed the PCH as source, supplied the installed compiler resource headers, and used the production assertion macro branch. The initial jit-format exit status alone was not sufficient; the direct check and its configuration are included in the evidence.

A separate Checked diagnostic build compared every non-null query with the original recursive implementation, including additional depth 0/1/9 checks, across both SuperPMI collections and the regression test. The comparisons passed, with no codegen differences. Counters confirmed cyclic VN paths and depths 0 through 10. Seeded-cache tests checked that direct proofs override stored false results; additional tests covered false/true entries at each cached depth and small, negative and large offsets. Natural false cache hits were not observed and are not claimed as coverage. This instrumentation is excluded from the proposed change and timing runs.

The uninstrumented SuperPMI compiler-allocation totals increased by 0.0265% (realworld) and 0.0144% (benchmarks). The largest per-context increases were 16,944 and 16,128 bytes, respectively; the largest relative increases were 0.82% and 1.25%. These are compiler arena allocation totals per compilation, not process RSS. The new maps use the compilation arena and remain allocated until that compilation ends.

BenchmarkDotNet execution time

BDN compares fully optimized steady-state execution, separately from the JIT-time table above. All 13 helper instruction listings and code sizes match the baseline after normalizing only NullExit call-target relocations. The additional null checks left by the earlier walk-budget implementation are gone.

The primary series measured 26 conditions in host order ABBABAAB, with 5 warmups and 15 measured iterations per launch, targeting 200 ms per iteration. Tiering, tiered PGO and ReadyToRun were disabled. Existing services stayed running on the shared Mac.

Case Input Base ns Patch ns B/A Approx. paired 95% interval
ChainN10 Pattern=0 6.506 6.540 1.0053 0.9241 to 1.0937
ChainN10 Pattern=1 3.018 3.054 1.0122 0.9812 to 1.0441
ChainN2 Pattern=0 2.034 2.029 0.9974 0.9527 to 1.0441
ChainN2 Pattern=1 1.316 1.420 1.0793 1.0044 to 1.1599
ChainN4 Pattern=0 3.208 3.380 1.0537 0.9639 to 1.1519
ChainN4 Pattern=1 1.793 1.860 1.0373 0.9745 to 1.1042
ChainN6 Pattern=0 4.549 4.689 1.0309 0.9126 to 1.1644
ChainN6 Pattern=1 2.275 2.292 1.0074 0.9593 to 1.0580
ChainN8 Pattern=0 5.294 5.429 1.0254 0.9703 to 1.0837
ChainN8 Pattern=1 2.568 2.664 1.0374 0.9821 to 1.0958
DirectGuard Pattern=0 1.003 1.001 0.9975 0.9587 to 1.0379
DirectGuard Pattern=1 0.999 1.007 1.0081 0.9903 to 1.0263
LoopNonNull Pattern=0 5.927 6.143 1.0363 0.9932 to 1.0813
LoopNonNull Pattern=1 5.404 5.372 0.9941 0.9241 to 1.0694
NullablePhi Pattern=0 1.076 1.077 1.0003 0.9826 to 1.0184
NullablePhi Pattern=1 1.615 1.582 0.9795 0.8162 to 1.1756
OriginalN10Length Pattern=0 6.557 6.598 1.0062 0.9461 to 1.0700
OriginalN10Length Pattern=1 3.041 3.104 1.0208 0.9771 to 1.0665
PairedN10 Pattern=0 2.670 2.698 1.0102 0.9835 to 1.0376
PairedN10 Pattern=1 2.209 2.246 1.0168 0.9887 to 1.0458
PairedN6 Pattern=0 1.949 1.966 1.0086 0.9767 to 1.0415
PairedN6 Pattern=1 1.733 1.742 1.0054 0.9937 to 1.0172
PairedN8 Pattern=0 2.186 2.212 1.0119 0.9945 to 1.0296
PairedN8 Pattern=1 1.964 1.983 1.0095 0.9885 to 1.0308
Phi2 Pattern=0 1.274 1.276 1.0020 0.9868 to 1.0173
Phi2 Pattern=1 1.277 1.282 1.0036 0.9738 to 1.0343

B/A below 1 means faster. The table uses geometric means of four independent launch pairs; intervals are exploratory and not adjusted for multiple comparisons.

ChainN2, Pattern 1 was 7.93% slower in the primary series. A selected ABBA follow-up measured -0.41% for that condition. It also included ChainN8, DirectGuard and OriginalN10Length, both input patterns, and captured the actual worker's helper and benchmark-wrapper assembly. Those instructions matched across all four follow-up passes. The original slowdown was not reproduced, but this does not prove it was noise or establish execution-time equivalence. Both series and their intervals are retained; no execution-time speedup is claimed.

Source, full raw BDN results, follow-up results, disassembly and reproduction commands.

The tested native patch was subsequently formatted with whitespace-only changes. Validation is local to macOS ARM64; it does not replace upstream cross-platform CI.

Share a walk budget across recursive branches while retaining the depth
limit and direct proofs. Add regression coverage for the repeated PHI
walk and nullable control flow.

Fix dotnet#134462
@github-actions github-actions Bot added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Sep 30, 2026
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 5 pipeline(s).
11 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

@dotnet-policy-service dotnet-policy-service Bot added the community-contribution Indicates that the PR has been added by a community member label Sep 30, 2026
@Garume

Garume commented Sep 30, 2026

Copy link
Copy Markdown
Author

I haven't run BenchmarkDotNet comparisons for this change yet. The measurements in the description cover JIT compilation time, process RSS and SuperPMI compiler CPU time.

I'll compare application execution time using BenchmarkDotNet with the baseline and patched Release CoreRun builds on ARM64, including cases where the walk budget could leave additional null checks. I'll post the results here, including any regressions.

Written with AI assistance.

@jakobbotsch

Copy link
Copy Markdown
Member

We should rather avoid doing the repeated work than limit it via a budget.

Replace the shared walk cutoff with query-local results keyed by VN and
remaining depth. Keep direct assertion checks and the original depth
limit. Restore the regression test to priority 1.
@Garume Garume changed the title JIT: Bound non-null assertion PHI walks JIT: Memoize non-null assertion PHI walks Sep 30, 2026
@Garume

Garume commented Sep 30, 2026 •

Copy link
Copy Markdown
Author

@jakobbotsch I replaced the shared walk budget with memoization of completed PHI walks. The cache is local to each non-null query and keyed by VN and remaining depth. Incoming assertions are checked before the cache, and the original depth limit of 10 is unchanged.

For the N10 reproduction on macOS ARM64, the Release results were:

Metric Baseline This change
Median JIT compilation time 4,953 ms 0.242 ms
Peak process RSS 3,055.6 MB 33.8 MB

Checked SuperPMI passed all 96,632 contexts with no codegen differences. The extra null checks from the earlier budget version are also gone.

I added all 26 BDN conditions to the PR description. ChainN2, Pattern 1 showed a slowdown in the initial measurements:

BDN series Execution time change vs. baseline
Initial measurements 7.93% slower
ABBA follow-up 0.41% faster

The slowdown did not reproduce in the follow-up, and the helper and wrapper instructions matched. That is not enough to rule out a regression, so I kept both sets of results in the description.

Raw results, disassembly and reproduction commands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI community-contribution Indicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

JIT: (bug) Exponential compile time and memory in assertion prop (unmemoized PHI walk in optAssertionVNIsNonNull)

2 participants