JIT: Defer physical promotion readbacks across basic blocks - #134048
jakobbotsch wants to merge 9 commits into
Conversation
Generalize the existing FindInducedParameterRegisterLocals transformation from entry-block-only rewriting in lowering to all-block rewriting during rationalization. Record parameter field reads, stores, and address uses in the existing execution-order visitor rather than adding a separate IR walk. After rationalization, propagate parameter kills through normal and EH successors, including loop backedges. Rewrite field reads only when every reaching path still observes the incoming parameter value, respecting read-before-kill ordering within each block. Reuse the existing extraction logic and parameter-register target mappings. Remove the entry-block discovery and unused reuse helper from lowering, while retaining mappings for independently promoted parameters. Account for the earlier parameter-register targets in async default-value analysis and invalidate recorded uses when rationalization discards their nodes. Physical promotion's eager readbacks remain unchanged; profitability of keeping packed values live across calls is follow-up work. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4c28b3c3-cf6c-4149-8eb1-964831492011
Visit blocks in reverse postorder and track pending field readbacks at block exits instead of eagerly initializing parameter replacements or materializing every pending readback at each block boundary. Keep replacement locals current by default. Carry the special pending state into a successor only when all reachable predecessors agree. At mixed joins, materialize readbacks on the pending predecessors rather than loading a potentially stale struct home in the successor. Require already-read-back locals at loop/backedge targets and handler entries. Retain regular-successor traversal and explicit EH barriers, including the existing materialization before throwing operations. Readbacks otherwise occur at uses, allowing generalized rationalization to extract parameter fields in the blocks that actually need them. This changes readback placement, not promotion selection or its costing. It avoids unnecessary eager extractions on early-return paths such as Guid.CompareTo while leaving broader profitability tuning as follow-up. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4c28b3c3-cf6c-4149-8eb1-964831492011
|
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. |
|
Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch |
Keep upstream's merged parameter-register rewriting implementation and preserve the lazy physical-promotion readback changes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4c28b3c3-cf6c-4149-8eb1-964831492011
Index lazy-readback state by DFS postorder and use postorder bitsets for block flags. Track original-field currency across forward joins to avoid writing back replacements that have only been read, retaining conservative loop and handler entry state. Remove the low-weight parameter extraction costing heuristic now that readbacks can be deferred. Keep the existing pending-readback intersection policy at joins. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4c28b3c3-cf6c-4149-8eb1-964831492011
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 628eb6b2-f468-4a67-b2c9-f8de24969633
Estimate lazy readback and reconciliation sites from liveness use/def sets. Let replacement materialize planned readbacks using its exact state, removing post-replacement commoning and may-definition tracking. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 628eb6b2-f468-4a67-b2c9-f8de24969633
Cache materialization boundaries for planning and replacement, short-circuit successor traversal, skip planning without eligible incoming fields, and avoid unnecessary definition queries. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 628eb6b2-f468-4a67-b2c9-f8de24969633
Move field tracking setup, boundary computation, and planning into PrepareReadBacks, called after constructing ReplaceVisitor. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 628eb6b2-f468-4a67-b2c9-f8de24969633
|
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. |
|
cc @dotnet/jit-contrib PTAL @AndyAyersMS Diffs. Positive perfscore diffs, but generally size regressions. That's expected: inserting the readbacks eagerly only in the entry block is generally smaller code, while delaying them to the actual points where they are used results in multiple readbacks (but overall fewer executed readbacks). There are some TP regressions. The detailed TP regressions look like this. It looks like a lot of different inlining decisions, but I cannot exclude that the changes to This is the "proper" fix for #133603. |
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Add coverage for common readback placement and validate codegen and performance impacts.
Get a fresh assessment by requesting another Copilot review.
Review effort: Lite
Findings: 1
What changed in this PR
This PR defers physical-promotion readbacks across basic blocks using RPO tracking and PGO-guided placement.
Changes:
- Adds deferred readback tracking and reconciliation.
- Adds dominator-based common readback planning.
- Adds branch, loop, EH, and partial-write tests.
| File | Description |
|---|---|
src/tests/JIT/Directed/physicalpromotion/physicalpromotion.cs |
Adds physical-promotion regression coverage. |
src/coreclr/jit/promotionliveness.cpp |
Adds replacement use/definition queries. |
src/coreclr/jit/promotion.h |
Adds readback tracking and planning state. |
src/coreclr/jit/promotion.cpp |
Implements deferred readback planning and insertion. |
| // Remarks: | ||
| // This is a profitability model, not a correctness analysis. Uses are assumed | ||
| // to materialize pending fields, and definitions end the incoming value. | ||
| // Replacement retains its exact state transitions and inserts any readbacks | ||
| // still required, including after partial writes and at EH boundaries. | ||
| // | ||
| void ReplaceVisitor::PlanReadBacks() |
There was a problem hiding this comment.
Like these remarks mention, this is only a hint to the replacement phase after to materialize a readback eagerly even when it wouldn't normally be necessary. Whatever this function computes should not matter for correctness.
|
Hmm, I need to double check that this can't place readbacks in blocks that will be skipped when we redirect flow for OSR. |
|
This diff in G_M35304_IG01: ; bbWeight=1, gcrefRegs=0000 {}, byrefRegs=0000 {}, byref, prolog, nogc
- push r15
- push r14
- push rdi
- push rsi
- push rbp
- push rbx
- ;; size=8 bbWeight=1 PerfScore 6.00
+ ;; size=0 bbWeight=1 PerfScore 0.00
G_M35304_IG02: ; bbWeight=1, gcrefRegs=0000 {}, byrefRegs=0006 {rcx rdx}, byref
; byrRegs +[rcx rdx]
mov r8d, dword ptr [rdx]
- movsx r10, word ptr [rdx+0x04]
- movsx r9, word ptr [rdx+0x06]
- movzx r11, byte ptr [rdx+0x08]
- movzx rbx, byte ptr [rdx+0x09]
- movzx rsi, byte ptr [rdx+0x0A]
- movzx rdi, byte ptr [rdx+0x0B]
- movzx rbp, byte ptr [rdx+0x0C]
- movzx r14, byte ptr [rdx+0x0D]
- movzx r15, byte ptr [rdx+0x0E]
- movzx rdx, byte ptr [rdx+0x0F]
- ; byrRegs -[rdx]
- cmp r8d, dword ptr [rcx]
+ mov r10d, dword ptr [rcx]
+ cmp r8d, r10d
jne G_M35304_IG15
- ;; size=60 bbWeight=1 PerfScore 25.00
-G_M35304_IG03: ; bbWeight=0.50, gcrefRegs=0000 {}, byrefRegs=0002 {rcx}, byref, isz
- movsx r8, word ptr [rcx+0x04]
- cmp r10d, r8d
+ ;; size=15 bbWeight=1 PerfScore 5.25
+G_M35304_IG03: ; bbWeight=0.50, gcrefRegs=0000 {}, byrefRegs=0006 {rcx rdx}, byref, isz
+ movsx r8, word ptr [rdx+0x04]
+ movsx r10, word ptr [rcx+0x04]
+ cmp r8d, r10d
jne G_M35304_IG14
- movsx r8, word ptr [rcx+0x06]
- cmp r9d, r8d
+ movsx r8, word ptr [rdx+0x06]
+ movsx r10, word ptr [rcx+0x06]
+ cmp r8d, r10d
jne G_M35304_IG13
- movzx r8, byte ptr [rcx+0x08]
- cmp r11d, r8d
+ movzx r8, byte ptr [rdx+0x08]
+ movzx r10, byte ptr [rcx+0x08]
+ cmp r8d, r10d
jne G_M35304_IG12
- movzx r8, byte ptr [rcx+0x09]
- cmp ebx, r8d
+ movzx r8, byte ptr [rdx+0x09]
+ movzx r10, byte ptr [rcx+0x09]
+ cmp r8d, r10d
jne G_M35304_IG11
- movzx r8, byte ptr [rcx+0x0A]
- cmp esi, r8d
+ movzx r8, byte ptr [rdx+0x0A]
+ movzx r10, byte ptr [rcx+0x0A]
+ cmp r8d, r10d
jne G_M35304_IG10
- movzx r8, byte ptr [rcx+0x0B]
- cmp edi, r8d
+ movzx r8, byte ptr [rdx+0x0B]
+ movzx r10, byte ptr [rcx+0x0B]
+ cmp r8d, r10d
jne G_M35304_IG09
- movzx r8, byte ptr [rcx+0x0C]
- cmp ebp, r8d
+ movzx r8, byte ptr [rdx+0x0C]
+ movzx r10, byte ptr [rcx+0x0C]
+ cmp r8d, r10d
jne SHORT G_M35304_IG08
- movzx r8, byte ptr [rcx+0x0D]
- cmp r14d, r8d
+ movzx r8, byte ptr [rdx+0x0D]
+ movzx r10, byte ptr [rcx+0x0D]
+ cmp r8d, r10d
jne SHORT G_M35304_IG07
- movzx r8, byte ptr [rcx+0x0E]
- cmp r15d, r8d
+ movzx r8, byte ptr [rdx+0x0E]
+ movzx r10, byte ptr [rcx+0x0E]
+ cmp r8d, r10d
jne SHORT G_M35304_IG06
- movzx r8, byte ptr [rcx+0x0F]
- cmp edx, r8d
+ movzx r8, byte ptr [rdx+0x0F]
+ movzx r10, byte ptr [rcx+0x0F]
+ cmp r8d, r10d
jne SHORT G_M35304_IG05
xor eax, eax
- ;; size=126 bbWeight=0.50 PerfScore 16.38
+ ;; size=176 bbWeight=0.50 PerfScore 26.38
G_M35304_IG04: ; bbWeight=0.50, epilog, nogc, extend
- pop rbx
- pop rbp
- pop rsi
- pop rdi
- pop r14
- pop r15
ret
- ;; size=9 bbWeight=0.50 PerfScore 2.00
+ ;; size=1 bbWeight=0.50 PerfScore 0.50
G_M35304_IG05: ; bbWeight=0.50, gcVars=0000000000000000 {}, gcrefRegs=0000 {}, byrefRegs=0000 {}, gcvars, byref
- ; byrRegs -[rcx]
+ ; byrRegs -[rcx rdx]
mov eax, -1
- mov r11d, 1
- cmp r8d, edx
- cmovge eax, r11d
+ mov ecx, 1
+ cmp r10d, r8d
+ cmovge eax, ecx
jmp G_M35304_IG16
- ;; size=23 bbWeight=0.50 PerfScore 1.62
+ ;; size=21 bbWeight=0.50 PerfScore 1.62
G_M35304_IG06: ; bbWeight=0.50, gcrefRegs=0000 {}, byrefRegs=0000 {}, byref
mov eax, -1
- mov r9d, 1
- cmp r8d, r15d
- cmovge eax, r9d
+ mov ecx, 1
+ cmp r10d, r8d
+ cmovge eax, ecx
jmp G_M35304_IG16
- ;; size=23 bbWeight=0.50 PerfScore 1.62
+ ;; size=21 bbWeight=0.50 PerfScore 1.62
G_M35304_IG07: ; bbWeight=0.50, gcrefRegs=0000 {}, byrefRegs=0000 {}, byref
mov eax, -1
- mov r10d, 1
- cmp r8d, r14d
- cmovge eax, r10d
+ mov ecx, 1
+ cmp r10d, r8d
+ cmovge eax, ecx
jmp G_M35304_IG16
- ;; size=23 bbWeight=0.50 PerfScore 1.62
+ ;; size=21 bbWeight=0.50 PerfScore 1.62
G_M35304_IG08: ; bbWeight=0.50, gcrefRegs=0000 {}, byrefRegs=0000 {}, byref, isz
mov eax, -1
mov ecx, 1
- cmp r8d, ebp
+ cmp r10d, r8d
cmovge eax, ecx
jmp SHORT G_M35304_IG16
;; size=18 bbWeight=0.50 PerfScore 1.62
G_M35304_IG09: ; bbWeight=0.50, gcrefRegs=0000 {}, byrefRegs=0000 {}, byref, isz
mov eax, -1
mov ecx, 1
- cmp r8d, edi
-(There might be some missing forward sub, need to take a closer look at why we don't get containment here) |

Before this PR the model in physical promotion is that promoted locals are always up-to-date in their replacement locals at the end of basic blocks. In certain cases, however, that causes us to be overly eager in insertion of readbacks.
One common case is for struct parameters. Currently the above model means we will always extract fields from the parameters at the end of the entry basic block. If the parameter has many fields in it this leads to creating a lot of live state. Also, if some fields are only used rarely it is wasteful to extract them in the entry block (the extraction may require bitwise operations).
This PR tries to improve the situation. It changes physical promotion to run the replacement phase in RPO. Then, it introduces logic to delay the readbacks:
Backedges are handled conservatively by always ensuring replacement fields are up to date into blocks that are targets of backedges.
The above may result in suboptimal insertion of reconciling readbacks in some cases compared to the current behavior. To handle this we additionally have a plan phase where we compute the dominator of all blocks where we expect to insert reconciling readbacks. Then, we use profile information to evaluate whether inserting a common readback in the dominator would be beneficial instead of inserting the multiple reconciling readbacks.