Repository navigation
JIT: let CSE uses rely on exceptions their defs rule out - #137032
Conversation
A def that evaluates an indirection proves the base address non-null, and a division proves the divisor nonzero, even when an earlier phase marked the node non-faulting and VN omitted the exception. Use these implied exceptions (for the def tree, and for the def's whole statement when the use is in another statement) when deciding whether a use with more exceptions can use the def. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
|
Azure Pipelines: Successfully started running 5 pipeline(s). 10 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
|
Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch |
|
You probably want to do this during value numbering instead of as a new walk. My prompted attempt: |
|
Thanks — I measured fbf37f1 against this PR on local SPMI (windows x64, base main 8237dfd; pmi / crossgen2 / coreclr_tests). Null-only rejected CSE uses go from 2,814 / 815 / 2,534 on main to 21 / 16 / 29 here vs 399 / 51 / 986 with the VN version: block-level dominance misses facts from non-dominating statements that CSE availability covers. Relative to this PR, the VN version is +6,097 / +3,949 / +12,246 bytes and PerfScore +0.030% / +0.009% / +0.135%. Changing the loads' VN exception sets also changes which occurrences CSE matches, e.g. NumericSortRect.NumHeapSort (Tier1-OSR) is +31% PerfScore because the hot-loop CSE #1 drops from 21 to 9 occurrences. There are also 52 extra SPMI misses in coreclr_tests. A VN-level approach is appealing because every consumer would see it (e.g. redundant branch opts' side-effect check), but it is broader and its results here are mixed, so I'd keep this change CSE-local and look at a VN version separately. The cost of the extra walk isn't measured yet; I'll get TP numbers. Note This reply was generated with the help of GitHub Copilot. |
If all occurrences of a candidate have the same exception set, uses are always covered by the defs, so skip the tree walks. No codegen change. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
|
Can you try again against https://github.com/NinoFloris/dotnet-runtime/tree/jit/vn-dominated-exceptions? It now handles the commas that likely caused the NumHeapSort regression, and I've added support for block joins so it's not just directly dominated checks. A big reason why I'd rather do it during value numbering is because this walk is quadratic, and once you add a hashtable you are halfway to what the value numbering case does. Maybe it should still be confined to CSE only but that depends on how high the tp cost of doing it during value numbering is. It's a good idea btw :) |
|
Re-measured 2244f42 (same setup; pmi / crossgen2 / coreclr_tests). It's now competitive: null-only rejected uses 70 / 58 / 51 (this PR: 21 / 16 / 29; main: 2,814 / 815 / 2,534), and relative to this PR it's -2,891 / -1,133 / +1,167 bytes with PerfScore +0.003% / -0.001% / ~0. NumHeapSort no longer regresses and the extra SPMI misses are gone (2 in pmi). So codegen is roughly a wash and TP will decide. CI tpdiff for this PR was +0.10% to +1.0% (FullOpts, depending on target) before f872555, which now only walks for candidates whose occurrences have differing exception sets (no codegen change locally). With it, tpdiff is +0.02% to +0.13% across all 9 targets (e.g. windows x64 +0.07% to +0.12%, linux arm64 +0.06% to +0.13%). Part of that remaining cost is the extra CSEs themselves. On the quadratic concern: it's one walk per def plus one cached walk per statement, now limited to mixed candidates, so it's only superlinear for nested defs. If your VN version's TP comes in lower, I'm happy to go that way — could you open it as a PR so CI measures it? Note This reply was generated with the help of GitHub Copilot. |
AndyAyersMS
left a comment
There was a problem hiding this comment.
Some nits -- how important is the "statement" level propagation? That seems like it's more risky / less useful?
Only use the exceptions ruled out by the def tree itself. QMARKs are gone before CSE. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
|
/azp run runtime-coreclr superpmi-diffs |
|
Azure Pipelines: Successfully started running 1 pipeline(s). |
|
@jakobbotsch thoughts on this? We should also reconcile with #137045. I'm not sure which is the preferred direction; here I steered Larry away from refining exception sets in VN. I feel like having VN create what essentially seem to be ORDER_SIDEEFF dependent VNs seems like a risk. The other annoyance is local AP's weakness in dealing propagating into loops, which creates many of these inconsistent cases in the first place. |
|
Some examples from other compilers:
It’s not surprising that centralizing the work pays off. Fitting it into RyuJIT is awkward because there’s zero(?) shared infrastructure for preserving contextual assumptions during code motion (GTF_ORDER_SIDEEFF already suffers from this). Each phase handles that ad hoc, which is where the actual risk is. #137045 is not really worse than in that respect, but it does expand that pattern… |
|
This looks like a pretty conceptually simple change and improvement to CSE. I would be happy with taking this if it improves things and handles some of these cases. #137045 looks like it is coming up with a new dependency/ordering framework within the JIT. While experimentation is interesting I think we would need to discuss what direction we want to take before we even get started on something like that. We have discussed that before and there are breadcrumbs in some places on GitHub. The parts in #137045 that are storing facts about blocks in side tables and reasoning about those seems reasonable as long as it does not have to be shared across phases, but I am not sure that is going to scale all that well once IR transformations make it into the mix and you want to use this framework throughout the JIT. My two cents: I would like a framework where ordering side effects of nodes can be classified at higher detail, similar to what |
|
diffs show some size increases and in some cases more size regressions than improvements. I don't think that's enough to hold this up, but wanted to mention it. It seems like our CSE heuristics likely need revisiting, as they struggle to balance prolog / call crossing costs vs benefits. Perf score is improved so we can hope we're still generally making good choices. |
CSE rejects a use when its exception set has items that the defs' exception sets don't. That is correct when the def might not have proven the exception impossible. But a def that has executed rules out more than its VN says:
That holds even when an earlier phase marked the node non-faulting and VN therefore left the exception out.
This change adds
optValnumCSE_ImpliedExcSet, which collects these items for a def's own tree, and uses it in two places:optValnumCSE_Availability: defs count their own liberal exception set plus the items implied by their tree. As before,defExcSetCurrentis the intersection over all defs, and later defs check earlier promises against the same set.The walks only run for candidates whose occurrences don't all have the same exception set (
csdMixedExcSets, set inoptValnumCSE_Index). Candidates whose occurrences all share one set are unaffected.There are no VN, IR or IR flag changes, so loop hoisting and other users of VN exception sets are unaffected.
Where the mismatched exception sets come from
Measured with temporary metrics in
optValnumCSE_Availabilityon main. Local SuperPMI, windows x64, FullOpts. Columns: libraries.pmi / libraries.crossgen2 / coreclr_tests.optNonNullAssertionProp_Indmarks one copy of a load non-faulting while another copy, for example the loop-test copy duplicated by loop inversion, stays faulting. Example:Microsoft.CodeAnalysis.BitVector:Invert, where thethis._bitsload is CSE'd only after global assertion prop, which runs too late.tmp1.f = arr[i]; ... tmp1.f. The store that provestmp1non-null is outside the def's tree.CHKCAST*helper call, or aCOMMA(CLASSPROFILE32 helper, x)in Instrumented Tier1 code. Each has the same normal VN as its operand, and the operand is accepted as a use, so no CSE is lost. This change does not target them.Effect on the counts
SuperPMI asmdiffs (local, windows x64, main → this PR)
¹ Excludes contexts with PerfScore > 1e5; the four RegexGenerator methods around 2e6 also improve. Including them gives -0.71%.
Dictionary<__Canon,__Canon>.FindValue(+32 B) andRemove(+23 B);TryInsertshrinks by 22 B.this->MT(mov r, [this], used for runtime lookups) lives across calls. That raises register pressure, so other locals spill. The crossgen2 regressions (OrderedDictionary,SegmentedDictionary) follow the same pattern. This is the usual tradeoff in the CSE heuristic, not a correctness problem.Relation to #136612
Relates to #136612. This follows up on @AndyAyersMS's review thread there: #136612 (comment).
#136612 forwards byref-exposed stores to same-address loads in VN. It also omits NullPtrExc from a forwarded load whose address the forwarding store already dereferenced, so that CSE accepts the load as a use.
This change does not cover that case. The CSE def there is the stored value, and the store that proves the address non-null is outside the def's tree. So #136612 keeps its VN NullPtrExc omission.
Note
Superseded: an earlier revision of this PR (before c53628c) also used what the def's whole statement rules out. That version made #136612's omission nearly redundant: dropping the omission left 9 / 59 / 19 small diffs, with unchanged codegen for
Utf8JsonReader.SkipWhiteSpaceon wasm and for theByrefStoreForwardingtest. Those measurements no longer apply, because the statement-level part was removed after review.Note
This PR description was generated with the help of GitHub Copilot.