Repository navigation
Conversation
After CSE candidates are located, remove occurrences that are the load in "*p = *p op x" when 'op' has a memory-operand form, so lowering can still emit "op [mem], x". The load is kept if 'op' is itself a CSE candidate with another remaining occurrence; this is iterated to a fixed point so chains of in-place updates still screen. 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 |
|
I took a look at this and the other PR because I'm working on CSE at the moment in #136950
That argument doesn't bear out, doing store to load forwarding here will cause it to cost more cycles in loops (and similar cases) due to stalls. Same for the other examples. It really is worse and only saves on size. |
|
Thanks for looking. I agree that "PerfScore only" was too generous for the loop cases. Here is the hot block in The field isn't loop-invariant, so both versions reload it every iteration, and the loop-carried store-to-load forward is the same in both. Nothing new stalls here. The real cost is one extra load per iteration: The screen wins when the other occurrence needs the value in a register anyway ( Note This reply was drafted with GitHub Copilot (AI-generated). |
Missed that, then yeah it's a small penalty only.
Keep it separate. I was mostly curious about the effects of the other PR because it affects how many loads can get CSE'd (also I have far too many PRs already that will have to be reviewed). |
|
This probably deserves weigh-in from @dotnet/intel and how it may impact some APX work. |
From the APX side, I see two effects. First, the screen frees a register. So, it shrinks the opportunities where eGPRs would have helped, though the resulting code is still smaller on both sides. Second, NDD would apply to the shared load/op/store form, but the PR's version is still preferable on size and registers, at the cost of one extra load: Can you think of any other APX impact we might have missed? |
Split from #136612. As requested in review, this does the screen in CSE (run normal candidate selection, then post-screen candidates that would become RMW on xarch) and lands it separately with benchmarks/aspnet diffs.
Change
On xarch, CSE can replace (or define) the load in
*p = *p op x. That keeps the loaded value in a register, so lowering can't emitop [mem], x. This PR adds a post-screen tooptValnumCSE_Locate,optValnumCSE_ScreenMemoryUpdates(TARGET_XARCHonly):STOREIND(addr, op(IND(addr), x)), the load's CSE occurrence is cleared.opmust have an RMW memory-operand form (OperIsRMWMemOp, orNOT/NEG). The load must be op1 unlessopis commutative. Addresses are matched withGenTree::Compare, as lowering does. The candidate's remaining occurrences can still be CSE'd.opis itself a CSE candidate with another remaining occurrence, since the result ofopthen has other uses. Removing a load can remove the only other occurrence of another load'sop(*p &= a; *p |= b;), so this is iterated to a fixed point.optValnumCSE_InitDataFlowskips cleared occurrences. Later phases already checkIS_CSE_INDEX.Example (
InPlaceMemoryUpdate.OrField,int old = holder.Flags; holder.Flags |= 0x40; return old;):Diffs
Local SuperPMI asmdiffs, windows x64, Checked JITs: base is main (5c99482), diff is this PR. Current main's JIT-EE GUID has no benchmarks/aspnet collections yet, so these replay the newest available ones with
-jit_ee_version:aec48fc3(Oct 29). Only 2 and 11 contexts miss.fa0c6a6f(Sep 23, the newest windows x64 aspnet2). 23,709 of 41,203 contexts miss and 567 error, the same for base and diff, so aspnet coverage is only about 41%.No context fails with only one of the two JITs.
Code size goes down almost everywhere. PerfScore goes up because it rates the memory-operand form as slower than load/op/store. For example,
inc dword ptr [mem]scores higher thaninc reg; mov [mem], reg, even when the value is already in a register.Notable examples:
Perf_Regex_Industry_SliceSlice:Count(run_pgo, Tier1-OSR, -4 B, +16,404 PerfScore; most of the run_pgo total). In a block with weight 10,935, a field is compared and then incremented. With one occurrence left, the field isn't CSE'd any more:RegexInterpreter:Scan,Regex:RunAllMatchesWithCallbackandSciMark2.kernel:RandomMatrixare the same shape in hot loops.System.Diagnostics.StackTrace:ToString(-60 B, +114 PerfScore) has inlinedStringBuilder.Append(char):inc esi; mov dword ptr [rbx+0x18], esibecomesinc dword ptr [rbx+0x18]in each append. This shape accounts for many of the small diffs.FrozenHashTable:CalcNumBuckets(benchmarks.run, -63 B, -56 PerfScore): lower register pressure removes several spills and reloads in the loop and shrinks the frame by 16 bytes.Benchstone.BenchI.Pi:ComputePi(-21 B): nopush rdi/push rsi, although PerfScore is +25 because of the RMW form in the loop.For the same change on top of #136612 (with VN store forwarding), libraries.pmi was -17,809 B with PerfScore +0.015%, and libraries.crossgen2 was -2,877 B with PerfScore +0.005%.
Testing
JIT/opt/CSE/InPlaceMemoryUpdatecoversor/add/commutedoron fields andand/not/negthrough a ref. Each method reads the value before the update, so main CSEs the load today. I compiled it with crossgen2 for windows and linux x64 and ran SuperFileCheck: all 6 checks fail with the base JIT and pass with this PR on both OSes. I didn't run the test itself (the managed asserts) locally on x64../build.sh clr.alljits+clr.spmi -c Checkedand./build.sh clr.alljits -c Release: no warnings or errors.jitformat.py: clean.Note
This PR description was drafted with GitHub Copilot (AI-generated) and reviewed before posting.