Skip to content

JIT: Handle primitive-sized remainders overlapping padding/promotions in physical promotion - #88109

Merged
jakobbotsch merged 3 commits into
dotnet:mainfrom
jakobbotsch:physical-promotion-covering-segment
Jul 4, 2023
Merged

JIT: Handle primitive-sized remainders overlapping padding/promotions in physical promotion#88109
jakobbotsch merged 3 commits into
dotnet:mainfrom
jakobbotsch:physical-promotion-covering-segment

Conversation

@jakobbotsch

@jakobbotschjakobbotsch commented Jun 27, 2023

Copy link
Copy Markdown
Member

The remainder may be separated by a bit of padding or other promoted fields but still fit into a primitive; in this case it is still beneficial to copy it all as a primitive, instead of falling back to a full block copy.

Example.

privateS_s;voidFoo(){Ss=new();s.A=10;s.D=20;s.F=30;// A, D, F gets promoted_s=s;}privatestructS{publicbyteA;publicbyteB;publicbyteC;publicbyteD;publicbyteE;publicbyteF;}
 Processing block operation [000018] that involves replacements
dst+003 <- V04 (V01.[003..004)) (last use)
dst+005 <- V05 (V01.[005..006)) (last use)
Block op remainder: [001..003) [004..005)
- => Remainder strategy: retain a full block op+ => Remainder strategy: int at +001
 ; V00 this [V00,T01] ( 3, 3 ) ref -> rcx this class-hnd single-def
;* V01 loc0 [V01 ] ( 0, 0 ) struct ( 8) zero-ref do-not-enreg[SF] ld-addr-op
;# V02 OutArgs [V02 ] ( 1, 1 ) struct ( 0) [rsp+00H] do-not-enreg[XS] addr-exposed "OutgoingArgSpace"
;* V03 tmp1 [V03 ] ( 0, 0 ) ubyte -> zero-ref "V01.[000..001)"
;* V04 tmp2 [V04 ] ( 0, 0 ) ubyte -> zero-ref "V01.[003..004)"
;* V05 tmp3 [V05 ] ( 0, 0 ) ubyte -> zero-ref "V01.[005..006)"
; V06 tmp4 [V06,T00] ( 5, 10 ) byref -> rcx single-def "Spilling address for field-by-field copy"
;
; Lcl frame size = 0
G_M52879_IG01: ;; offset=0000H
;; size=0 bbWeight=1 PerfScore 0.00
G_M52879_IG02: ;; offset=0000H
add rcx, 8
xor eax, eax
- mov dword ptr [rcx], eax- mov dword ptr [rcx+02H], eax+ mov dword ptr [rcx+01H], eax
mov byte ptr [rcx], 10
mov byte ptr [rcx+03H], 20
mov byte ptr [rcx+05H], 30
- ;; size=22 bbWeight=1 PerfScore 5.50-G_M52879_IG03: ;; offset=0016H+ ;; size=20 bbWeight=1 PerfScore 4.50+G_M52879_IG03: ;; offset=0014H
ret ;; size=1 bbWeight=1 PerfScore 1.00
-; Total bytes of code 23, prolog size 0, PerfScore 8.80, instruction count 8, allocated bytes for code 23 (MethodHash=1da13170) for method Program:Foo():this (FullOpts)+; Total bytes of code 21, prolog size 0, PerfScore 7.60, instruction count 7, allocated bytes for code 21 (MethodHash=1da13170) for method Program:Foo():this (FullOpts)
; ============================================================

We have to be careful, however, since the covering segment can now contain promoted fields. If this happens we need to make sure we write the promoted field after the remainder.

@ghostghost added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Jun 27, 2023
@ghost

Copy link
Copy Markdown

Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.

Issue Details

The remainder may be separated by a bit of padding but still fit into a primitive; in this case it is still beneficial to copy it all as a primitive, instead of falling back to a full block copy.

Example.
Before:

Processing block operation [000737] that involves replacements
dst+000<-V101 (V47.[000..008)) (last use)
dst+008<-V102 (V47.[008..016)) (last use)
dst+016<-V103 (V47.[016..024)) (last use)
dst+024<-V104 (V47.[024..028)) (last use)
dst+028<-V105 (V47.[028..029)) (last use)
Block op remainder: [032..033) [036..040)
=>Remainderstrategy: retain a full block op

After:

Processing block operation [000737] that involves replacements
dst+000<-V101 (V47.[000..008)) (last use)
dst+008<-V102 (V47.[008..016)) (last use)
dst+016<-V103 (V47.[016..024)) (last use)
dst+024<-V104 (V47.[024..028)) (last use)
dst+028<-V105 (V47.[028..029)) (last use)
Block op remainder: [032..033) [036..040)
=>Remainderstrategy: long at +032

This leads to ASM diffs like the following:

 xor edx, edx
mov qword ptr [rsp+148H], rdx
- ;; size=20 bbWeight=0.33 PerfScore 1.57-G_M6338_IG24: ; bbWeight=0.33, nogc, extend- vmovdqu ymm0, ymmword ptr [rsp+128H]- vmovdqu ymmword ptr [rsp+D8H], ymm0- mov rdx, qword ptr [rsp+148H]- mov qword ptr [rsp+F8H], rdx- ;; size=34 bbWeight=0.33 PerfScore 2.32-G_M6338_IG25: ; bbWeight=0.33, extend
mov gword ptr [rsp+D8H], r12
- xor rdx, rdx- ; gcrRegs +[rdx]+ ;; size=28 bbWeight=0.33 PerfScore 1.90+G_M6338_IG24: ; bbWeight=0.33, gcrefRegs=1028 {rbx rbp r12}, byrefRegs=4000 {r14}, byref
mov gword ptr [rsp+E0H], rdx
- ;; size=18 bbWeight=0.33 PerfScore 0.75-G_M6338_IG26: ; bbWeight=0.33, gcrefRegs=1028 {rbx rbp r12}, byrefRegs=4000 {r14}, byref- ; gcrRegs -[rdx]+ ;; size=8 bbWeight=0.33 PerfScore 0.33+G_M6338_IG25: ; bbWeight=0.33, gcrefRegs=1028 {rbx rbp r12}, byrefRegs=4000 {r14}, byref
mov gword ptr [rsp+E8H], rdx
mov dword ptr [rsp+F0H], r9d
mov byte ptr [rsp+F4H], 0
+ ;; size=24 bbWeight=0.33 PerfScore 0.99+G_M6338_IG26: ; bbWeight=0.33, gcrefRegs=1028 {rbx rbp r12}, byrefRegs=4000 {r14}, byref+ mov qword ptr [rsp+F8H], rdx

We have to be careful, however, since the covering segment can now contain promoted fields. If this happens we need to make sure we write the promoted field after the remainder.

Author:jakobbotsch
Assignees:-
Labels:

area-CodeGen-coreclr

Milestone:-

…l promotion
The remainder may be separated by a bit of padding but still fit into a
primitive; in this case it is still beneficial to copy it all as a
primitive, instead of falling back to a full block copy.
Example.
Before:
```
Processing block operation [000737] that involves replacements
dst+000 <- V101 (V47.[000..008)) (last use)
dst+008 <- V102 (V47.[008..016)) (last use)
dst+016 <- V103 (V47.[016..024)) (last use)
dst+024 <- V104 (V47.[024..028)) (last use)
dst+028 <- V105 (V47.[028..029)) (last use)
Block op remainder: [032..033) [036..040)
=> Remainder strategy: retain a full block op
```
After:
```
Processing block operation [000737] that involves replacements
dst+000 <- V101 (V47.[000..008)) (last use)
dst+008 <- V102 (V47.[008..016)) (last use)
dst+016 <- V103 (V47.[016..024)) (last use)
dst+024 <- V104 (V47.[024..028)) (last use)
dst+028 <- V105 (V47.[028..029)) (last use)
Block op remainder: [032..033) [036..040)
=> Remainder strategy: long at +032
```
This leads to ASM diffs like the following:
```diff
xor edx, edx
mov qword ptr [rsp+148H], rdx
- ;; size=20 bbWeight=0.33 PerfScore 1.57
-G_M6338_IG24: ; bbWeight=0.33, nogc, extend
- vmovdqu ymm0, ymmword ptr [rsp+128H]
- vmovdqu ymmword ptr [rsp+D8H], ymm0
- mov rdx, qword ptr [rsp+148H]
- mov qword ptr [rsp+F8H], rdx
- ;; size=34 bbWeight=0.33 PerfScore 2.32
-G_M6338_IG25: ; bbWeight=0.33, extend
mov gword ptr [rsp+D8H], r12
- xor rdx, rdx
- ; gcrRegs +[rdx]
+ ;; size=28 bbWeight=0.33 PerfScore 1.90
+G_M6338_IG24: ; bbWeight=0.33, gcrefRegs=1028 {rbx rbp r12}, byrefRegs=4000 {r14}, byref
mov gword ptr [rsp+E0H], rdx
- ;; size=18 bbWeight=0.33 PerfScore 0.75
-G_M6338_IG26: ; bbWeight=0.33, gcrefRegs=1028 {rbx rbp r12}, byrefRegs=4000 {r14}, byref
- ; gcrRegs -[rdx]
+ ;; size=8 bbWeight=0.33 PerfScore 0.33
+G_M6338_IG25: ; bbWeight=0.33, gcrefRegs=1028 {rbx rbp r12}, byrefRegs=4000 {r14}, byref
mov gword ptr [rsp+E8H], rdx
mov dword ptr [rsp+F0H], r9d
mov byte ptr [rsp+F4H], 0
+ ;; size=24 bbWeight=0.33 PerfScore 0.99
+G_M6338_IG26: ; bbWeight=0.33, gcrefRegs=1028 {rbx rbp r12}, byrefRegs=4000 {r14}, byref
+ mov qword ptr [rsp+F8H], rdx
```
We have to be careful, however, since the covering segment can now
contain promoted fields. If this happens we need to make sure we write
the promoted field _after_ the remainder.
Unfortunately doing this requires quite a bit of refactoring. I have
extracted all common code for handling creation of derived accesses of
the destination/source into a common class called LocationAccess.
@jakobbotsch
jakobbotschforce-pushed the physical-promotion-covering-segment branch from 4ec5010 to 0c9b1c5CompareJune 30, 2023 22:07
@jakobbotsch

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-coreclr superpmi-diffs

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@jakobbotsch

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-coreclr superpmi-diffs

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@jakobbotsch
jakobbotsch marked this pull request as ready for review July 3, 2023 08:16
@jakobbotsch

Copy link
Copy Markdown
MemberAuthor

cc @dotnet/jit-contrib PTAL @EgorBo

Diffs. This is not just about padding, but also when the remainder is overlapping with promoted fields. A C# example of the difference:

privateS_s;voidFoo(){Ss=new();s.A=10;s.D=20;s.F=30;// A, D, F gets promoted_s=s;}privatestructS{publicbyteA;publicbyteB;publicbyteC;publicbyteD;publicbyteE;publicbyteF;}
 Processing block operation [000018] that involves replacements
dst+003 <- V04 (V01.[003..004)) (last use)
dst+005 <- V05 (V01.[005..006)) (last use)
Block op remainder: [001..003) [004..005)
- => Remainder strategy: retain a full block op+ => Remainder strategy: int at +001
 ; V00 this [V00,T01] ( 3, 3 ) ref -> rcx this class-hnd single-def
;* V01 loc0 [V01 ] ( 0, 0 ) struct ( 8) zero-ref do-not-enreg[SF] ld-addr-op
;# V02 OutArgs [V02 ] ( 1, 1 ) struct ( 0) [rsp+00H] do-not-enreg[XS] addr-exposed "OutgoingArgSpace"
;* V03 tmp1 [V03 ] ( 0, 0 ) ubyte -> zero-ref "V01.[000..001)"
;* V04 tmp2 [V04 ] ( 0, 0 ) ubyte -> zero-ref "V01.[003..004)"
;* V05 tmp3 [V05 ] ( 0, 0 ) ubyte -> zero-ref "V01.[005..006)"
; V06 tmp4 [V06,T00] ( 5, 10 ) byref -> rcx single-def "Spilling address for field-by-field copy"
;
; Lcl frame size = 0
G_M52879_IG01: ;; offset=0000H
;; size=0 bbWeight=1 PerfScore 0.00
G_M52879_IG02: ;; offset=0000H
add rcx, 8
xor eax, eax
- mov dword ptr [rcx], eax- mov dword ptr [rcx+02H], eax+ mov dword ptr [rcx+01H], eax
mov byte ptr [rcx], 10
mov byte ptr [rcx+03H], 20
mov byte ptr [rcx+05H], 30
- ;; size=22 bbWeight=1 PerfScore 5.50-G_M52879_IG03: ;; offset=0016H+ ;; size=20 bbWeight=1 PerfScore 4.50+G_M52879_IG03: ;; offset=0014H
ret ;; size=1 bbWeight=1 PerfScore 1.00
-; Total bytes of code 23, prolog size 0, PerfScore 8.80, instruction count 8, allocated bytes for code 23 (MethodHash=1da13170) for method Program:Foo():this (FullOpts)+; Total bytes of code 21, prolog size 0, PerfScore 7.60, instruction count 7, allocated bytes for code 21 (MethodHash=1da13170) for method Program:Foo():this (FullOpts)
; ============================================================

@jakobbotsch
jakobbotsch requested a review from EgorBoJuly 3, 2023 08:36
@jakobbotschjakobbotsch changed the title JIT: Handle primitive-sized remainders overlapping padding in physical promotionJIT: Handle primitive-sized remainders overlapping padding/promotions in physical promotionJul 3, 2023

@EgorBoEgorBo left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are regressions like this

@@ -13,46 +13,48 @@
; V02 arg1 [V02,T02] ( 3, 3 ) ref -> r8 class-hnd single-def
; V03 arg2 [V03,T03] ( 3, 3 ) struct ( 8) r9 single-def
;# V04 OutArgs [V04 ] ( 1, 1 ) struct ( 0) [rsp+00H] do-not-enreg[XS] addr-exposed "OutgoingArgSpace"
-; V05 tmp1 [V05,T01] ( 3, 6 ) struct (16) [rsp+00H] do-not-enreg[SF] must-init ld-addr-op "NewObj constructor temp"+; V05 tmp1 [V05,T04] ( 2, 4 ) struct (16) [rsp+08H] do-not-enreg[SF] must-init ld-addr-op "NewObj constructor temp"
;* V06 tmp2 [V06 ] ( 0, 0 ) long -> zero-ref "spilling helperCall"
;* V07 tmp3 [V07 ] ( 0, 0 ) ref -> zero-ref single-def "V05.[000..008)"
+; V08 tmp4 [V08,T01] ( 3, 6 ) byref -> rsi single-def "Spilling address for field-by-field copy"
;
-; Lcl frame size = 16+; Lcl frame size = 24
G_M36974_IG01: ; bbWeight=1, gcrefRegs=0000 {}, byrefRegs=0000 {}, byref, nogc <-- Prolog IG
- push rdi
push rsi
push rbx
- sub rsp, 16+ sub rsp, 24
xor eax, eax
- mov qword ptr [rsp], rax+ mov qword ptr [rsp+08H], rax
mov rbx, rcx
; byrRegs +[rbx]
- ;; size=16 bbWeight=1 PerfScore 4.75+ ;; size=16 bbWeight=1 PerfScore 3.75
G_M36974_IG02: ; bbWeight=1, gcrefRegs=0100 {r8}, byrefRegs=0008 {rbx}, byref
; gcrRegs +[r8]
- mov qword ptr [rsp+08H], r9- mov gword ptr [rsp], r8- lea rdi, bword ptr [rbx+08H]- ; byrRegs +[rdi]- lea rsi, bword ptr [rsp]+ mov qword ptr [rsp+10H], r9+ lea rsi, bword ptr [rbx+08H]
; byrRegs +[rsi]
- call CORINFO_HELP_ASSIGN_BYREF- ; gcrRegs -[r8]- movsq + mov rcx, rsi+ ; byrRegs +[rcx]+ mov rdx, r8+ ; gcrRegs +[rdx]+ call CORINFO_HELP_CHECKED_ASSIGN_REF+ ; gcrRegs -[rdx r8]+ ; byrRegs -[rcx]+ mov rax, qword ptr [rsp+10H]+ mov qword ptr [rsi+08H], rax
xor rax, rax
; gcrRegs +[rax]
mov gword ptr [rbx], rax
- ;; size=29 bbWeight=1 PerfScore 6.25+ ;; size=34 bbWeight=1 PerfScore 6.25
G_M36974_IG03: ; bbWeight=1, epilog, nogc, extend
- add rsp, 16+ add rsp, 24
pop rbx
pop rsi
- pop rdi
ret - ;; size=8 bbWeight=1 PerfScore 2.75+ ;; size=7 bbWeight=1 PerfScore 2.25-; Total bytes of code 53, prolog size 13, PerfScore 19.05, instruction count 20, allocated bytes for code 53 (MethodHash=ebd16f91) for method System.Linq.Parallel.HashLookupValueList`2[System.__Canon,System.Nullable`1[int]]:.ctor(System.__Canon,System.Nullable`1[int]):this (FullOpts)+; Total bytes of code 57, prolog size 13, PerfScore 17.95, instruction count 19, allocated bytes for code 57 (MethodHash=ebd16f91) for method System.Linq.Parallel.HashLookupValueList`2[System.__Canon,System.Nullable`1[int]]:.ctor(System.__Canon,System.Nullable`1[int]):this (FullOpts)
; ============================================================

expected?

@jakobbotsch

Copy link
Copy Markdown
MemberAuthor

Generally we do expect to replace CORINFO_HELP_ASSIGN_BYREF with CORINFO_HELP_CHECKED_ASSIGN_REF in the cases where we switch the remainder from a block copy (where the block has GC pointers) to a TYP_REF primitive copy.
That has the side effect that we no longer get the destination/source increments "for free" as part of that helper, so I suppose that can sometimes result in bigger code. But notice that in this case we get rid of a movsq which is a very expensive instruction, so perf wise I would expect this to be beneficial.

Now, this case is a bit peculiar because the fact that we no longer uses the full block copy for the remainder turns off an unrelated optimization:

@@ -1,20 +1,28 @@
STMT00001 ( 0x000[E-] ... ??? )
[000027] nA-XG------ ▌ STORE_BLK struct<System.Linq.Parallel.Pair`2, 16> (copy)
[000025] ---X------- ├──▌ FIELD_ADDR byref <unknown class>:<unknown field>
[000026] ----------- │ └──▌ LCL_VAR byref V00 this [000024] ----------- └──▌ LCL_VAR struct<System.Linq.Parallel.Pair`2, 16> V05 tmp1 (last use)
Processing block operation [000027] that involves replacements
dst+000 <- V07 (V05.[000..008)) (last use)
Block op remainder: [008..009) [012..016)
- => Remainder strategy: retain a full block op- Will write back V07 (V05.[000..008)) to avoid an additional write barrier- Skipping dst+000 <- V07 (V05.[000..008)); it is up-to-date in its struct local and will be handled as part of the remainder+ => Remainder strategy: long at +008++lvaGrabTemp returning 8 (V08 tmp4) called for Spilling address for field-by-field copy.++Local V05 should not be enregistered because: was accessed as a local field
New statement:
STMT00001 ( 0x000[E-] ... ??? )
- [000040] -A-XG------ ▌ COMMA void - [000039] UA--------- ├──▌ STORE_LCL_FLD ref V05 tmp1 [+0]- [000038] ----------- │ └──▌ LCL_VAR ref V07 tmp3 - [000027] nA-XG------ └──▌ STORE_BLK struct<System.Linq.Parallel.Pair`2, 16> (copy)- [000025] ---X------- ├──▌ FIELD_ADDR byref <unknown class>:<unknown field>- [000026] ----------- │ └──▌ LCL_VAR byref V00 this - [000024] ----------- └──▌ LCL_VAR struct<System.Linq.Parallel.Pair`2, 16> V05 tmp1 + [000048] -A-XG------ ▌ COMMA void + [000038] DA-X------- ├──▌ STORE_LCL_VAR byref V08 tmp4 + [000025] ---X------- │ └──▌ FIELD_ADDR byref <unknown class>:<unknown field>+ [000026] ----------- │ └──▌ LCL_VAR byref V00 this + [000047] -A--G------ └──▌ COMMA void + [000042] nA--G------ ├──▌ STOREIND ref + [000041] ----------- │ ├──▌ LCL_VAR byref V08 tmp4 + [000040] ----------- │ └──▌ LCL_VAR ref V07 tmp3 (last use)+ [000046] nA--G------ └──▌ STOREIND long + [000045] ----------- ├──▌ ADD byref + [000039] ----------- │ ├──▌ LCL_VAR byref V08 tmp4 + [000044] ----------- │ └──▌ CNS_INT long 8+ [000043] ----------- └──▌ LCL_FLD long V05 tmp1 [+8]

Specifically, in the base we notice that we have a promoted TYP_REF field that is being written to heap, which will involve a write barrier, in addition to the write barrier imposed for the block copy. So we write it back to stack first, to avoid this extra write barrier.
This has the side effect that we don't need to write it back after the write barrier call. It also means we don't need to save the target address to a local because we end up only using the address once.

In the diff we don't do this optimization anymore and instead just copy the promoted TYP_REF directly to the heap, incurring the write barrier. It doesn't really matter because overall we still only end up needing one write barrier.

We should be able to avoid the spilled local address in this case by teaching physical promotion to peel FIELD_ADDR nodes, though it comes with a bit of complexity to get the right null checking behavior. It would probably have quite decent diffs, though. Will open an issue for that.

@jakobbotsch
jakobbotsch merged commit 210a7a5 into dotnet:mainJul 4, 2023
@jakobbotsch
jakobbotsch deleted the physical-promotion-covering-segment branch July 4, 2023 16:02
@ghostghost locked as resolved and limited conversation to collaborators Aug 3, 2023
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@jakobbotsch@EgorBo
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
JIT: Handle primitive-sized remainders overlapping padding/promotions in physical promotion by jakobbotsch · Pull Request #88109 · dotnet/runtime · GitHub
Skip to content

JIT: Handle primitive-sized remainders overlapping padding/promotions in physical promotion - #88109

Merged
jakobbotsch merged 3 commits into
dotnet:mainfrom
jakobbotsch:physical-promotion-covering-segment
Jul 4, 2023
Merged

JIT: Handle primitive-sized remainders overlapping padding/promotions in physical promotion#88109
jakobbotsch merged 3 commits into
dotnet:mainfrom
jakobbotsch:physical-promotion-covering-segment

Conversation

@jakobbotsch

@jakobbotschjakobbotsch commented Jun 27, 2023

Copy link
Copy Markdown
Member

The remainder may be separated by a bit of padding or other promoted fields but still fit into a primitive; in this case it is still beneficial to copy it all as a primitive, instead of falling back to a full block copy.

Example.

privateS_s;voidFoo(){Ss=new();s.A=10;s.D=20;s.F=30;// A, D, F gets promoted_s=s;}privatestructS{publicbyteA;publicbyteB;publicbyteC;publicbyteD;publicbyteE;publicbyteF;}
 Processing block operation [000018] that involves replacements
dst+003 <- V04 (V01.[003..004)) (last use)
dst+005 <- V05 (V01.[005..006)) (last use)
Block op remainder: [001..003) [004..005)
- => Remainder strategy: retain a full block op+ => Remainder strategy: int at +001
 ; V00 this [V00,T01] ( 3, 3 ) ref -> rcx this class-hnd single-def
;* V01 loc0 [V01 ] ( 0, 0 ) struct ( 8) zero-ref do-not-enreg[SF] ld-addr-op
;# V02 OutArgs [V02 ] ( 1, 1 ) struct ( 0) [rsp+00H] do-not-enreg[XS] addr-exposed "OutgoingArgSpace"
;* V03 tmp1 [V03 ] ( 0, 0 ) ubyte -> zero-ref "V01.[000..001)"
;* V04 tmp2 [V04 ] ( 0, 0 ) ubyte -> zero-ref "V01.[003..004)"
;* V05 tmp3 [V05 ] ( 0, 0 ) ubyte -> zero-ref "V01.[005..006)"
; V06 tmp4 [V06,T00] ( 5, 10 ) byref -> rcx single-def "Spilling address for field-by-field copy"
;
; Lcl frame size = 0
G_M52879_IG01: ;; offset=0000H
;; size=0 bbWeight=1 PerfScore 0.00
G_M52879_IG02: ;; offset=0000H
add rcx, 8
xor eax, eax
- mov dword ptr [rcx], eax- mov dword ptr [rcx+02H], eax+ mov dword ptr [rcx+01H], eax
mov byte ptr [rcx], 10
mov byte ptr [rcx+03H], 20
mov byte ptr [rcx+05H], 30
- ;; size=22 bbWeight=1 PerfScore 5.50-G_M52879_IG03: ;; offset=0016H+ ;; size=20 bbWeight=1 PerfScore 4.50+G_M52879_IG03: ;; offset=0014H
ret ;; size=1 bbWeight=1 PerfScore 1.00
-; Total bytes of code 23, prolog size 0, PerfScore 8.80, instruction count 8, allocated bytes for code 23 (MethodHash=1da13170) for method Program:Foo():this (FullOpts)+; Total bytes of code 21, prolog size 0, PerfScore 7.60, instruction count 7, allocated bytes for code 21 (MethodHash=1da13170) for method Program:Foo():this (FullOpts)
; ============================================================

We have to be careful, however, since the covering segment can now contain promoted fields. If this happens we need to make sure we write the promoted field after the remainder.

@ghostghost added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Jun 27, 2023
@ghost

Copy link
Copy Markdown

Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.

Issue Details

The remainder may be separated by a bit of padding but still fit into a primitive; in this case it is still beneficial to copy it all as a primitive, instead of falling back to a full block copy.

Example.
Before:

Processing block operation [000737] that involves replacements
dst+000<-V101 (V47.[000..008)) (last use)
dst+008<-V102 (V47.[008..016)) (last use)
dst+016<-V103 (V47.[016..024)) (last use)
dst+024<-V104 (V47.[024..028)) (last use)
dst+028<-V105 (V47.[028..029)) (last use)
Block op remainder: [032..033) [036..040)
=>Remainderstrategy: retain a full block op

After:

Processing block operation [000737] that involves replacements
dst+000<-V101 (V47.[000..008)) (last use)
dst+008<-V102 (V47.[008..016)) (last use)
dst+016<-V103 (V47.[016..024)) (last use)
dst+024<-V104 (V47.[024..028)) (last use)
dst+028<-V105 (V47.[028..029)) (last use)
Block op remainder: [032..033) [036..040)
=>Remainderstrategy: long at +032

This leads to ASM diffs like the following:

 xor edx, edx
mov qword ptr [rsp+148H], rdx
- ;; size=20 bbWeight=0.33 PerfScore 1.57-G_M6338_IG24: ; bbWeight=0.33, nogc, extend- vmovdqu ymm0, ymmword ptr [rsp+128H]- vmovdqu ymmword ptr [rsp+D8H], ymm0- mov rdx, qword ptr [rsp+148H]- mov qword ptr [rsp+F8H], rdx- ;; size=34 bbWeight=0.33 PerfScore 2.32-G_M6338_IG25: ; bbWeight=0.33, extend
mov gword ptr [rsp+D8H], r12
- xor rdx, rdx- ; gcrRegs +[rdx]+ ;; size=28 bbWeight=0.33 PerfScore 1.90+G_M6338_IG24: ; bbWeight=0.33, gcrefRegs=1028 {rbx rbp r12}, byrefRegs=4000 {r14}, byref
mov gword ptr [rsp+E0H], rdx
- ;; size=18 bbWeight=0.33 PerfScore 0.75-G_M6338_IG26: ; bbWeight=0.33, gcrefRegs=1028 {rbx rbp r12}, byrefRegs=4000 {r14}, byref- ; gcrRegs -[rdx]+ ;; size=8 bbWeight=0.33 PerfScore 0.33+G_M6338_IG25: ; bbWeight=0.33, gcrefRegs=1028 {rbx rbp r12}, byrefRegs=4000 {r14}, byref
mov gword ptr [rsp+E8H], rdx
mov dword ptr [rsp+F0H], r9d
mov byte ptr [rsp+F4H], 0
+ ;; size=24 bbWeight=0.33 PerfScore 0.99+G_M6338_IG26: ; bbWeight=0.33, gcrefRegs=1028 {rbx rbp r12}, byrefRegs=4000 {r14}, byref+ mov qword ptr [rsp+F8H], rdx

We have to be careful, however, since the covering segment can now contain promoted fields. If this happens we need to make sure we write the promoted field after the remainder.

Author:jakobbotsch
Assignees:-
Labels:

area-CodeGen-coreclr

Milestone:-

…l promotion
The remainder may be separated by a bit of padding but still fit into a
primitive; in this case it is still beneficial to copy it all as a
primitive, instead of falling back to a full block copy.
Example.
Before:
```
Processing block operation [000737] that involves replacements
dst+000 <- V101 (V47.[000..008)) (last use)
dst+008 <- V102 (V47.[008..016)) (last use)
dst+016 <- V103 (V47.[016..024)) (last use)
dst+024 <- V104 (V47.[024..028)) (last use)
dst+028 <- V105 (V47.[028..029)) (last use)
Block op remainder: [032..033) [036..040)
=> Remainder strategy: retain a full block op
```
After:
```
Processing block operation [000737] that involves replacements
dst+000 <- V101 (V47.[000..008)) (last use)
dst+008 <- V102 (V47.[008..016)) (last use)
dst+016 <- V103 (V47.[016..024)) (last use)
dst+024 <- V104 (V47.[024..028)) (last use)
dst+028 <- V105 (V47.[028..029)) (last use)
Block op remainder: [032..033) [036..040)
=> Remainder strategy: long at +032
```
This leads to ASM diffs like the following:
```diff
xor edx, edx
mov qword ptr [rsp+148H], rdx
- ;; size=20 bbWeight=0.33 PerfScore 1.57
-G_M6338_IG24: ; bbWeight=0.33, nogc, extend
- vmovdqu ymm0, ymmword ptr [rsp+128H]
- vmovdqu ymmword ptr [rsp+D8H], ymm0
- mov rdx, qword ptr [rsp+148H]
- mov qword ptr [rsp+F8H], rdx
- ;; size=34 bbWeight=0.33 PerfScore 2.32
-G_M6338_IG25: ; bbWeight=0.33, extend
mov gword ptr [rsp+D8H], r12
- xor rdx, rdx
- ; gcrRegs +[rdx]
+ ;; size=28 bbWeight=0.33 PerfScore 1.90
+G_M6338_IG24: ; bbWeight=0.33, gcrefRegs=1028 {rbx rbp r12}, byrefRegs=4000 {r14}, byref
mov gword ptr [rsp+E0H], rdx
- ;; size=18 bbWeight=0.33 PerfScore 0.75
-G_M6338_IG26: ; bbWeight=0.33, gcrefRegs=1028 {rbx rbp r12}, byrefRegs=4000 {r14}, byref
- ; gcrRegs -[rdx]
+ ;; size=8 bbWeight=0.33 PerfScore 0.33
+G_M6338_IG25: ; bbWeight=0.33, gcrefRegs=1028 {rbx rbp r12}, byrefRegs=4000 {r14}, byref
mov gword ptr [rsp+E8H], rdx
mov dword ptr [rsp+F0H], r9d
mov byte ptr [rsp+F4H], 0
+ ;; size=24 bbWeight=0.33 PerfScore 0.99
+G_M6338_IG26: ; bbWeight=0.33, gcrefRegs=1028 {rbx rbp r12}, byrefRegs=4000 {r14}, byref
+ mov qword ptr [rsp+F8H], rdx
```
We have to be careful, however, since the covering segment can now
contain promoted fields. If this happens we need to make sure we write
the promoted field _after_ the remainder.
Unfortunately doing this requires quite a bit of refactoring. I have
extracted all common code for handling creation of derived accesses of
the destination/source into a common class called LocationAccess.
@jakobbotsch
jakobbotschforce-pushed the physical-promotion-covering-segment branch from 4ec5010 to 0c9b1c5CompareJune 30, 2023 22:07
@jakobbotsch

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-coreclr superpmi-diffs

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@jakobbotsch

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-coreclr superpmi-diffs

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@jakobbotsch
jakobbotsch marked this pull request as ready for review July 3, 2023 08:16
@jakobbotsch

Copy link
Copy Markdown
MemberAuthor

cc @dotnet/jit-contrib PTAL @EgorBo

Diffs. This is not just about padding, but also when the remainder is overlapping with promoted fields. A C# example of the difference:

privateS_s;voidFoo(){Ss=new();s.A=10;s.D=20;s.F=30;// A, D, F gets promoted_s=s;}privatestructS{publicbyteA;publicbyteB;publicbyteC;publicbyteD;publicbyteE;publicbyteF;}
 Processing block operation [000018] that involves replacements
dst+003 <- V04 (V01.[003..004)) (last use)
dst+005 <- V05 (V01.[005..006)) (last use)
Block op remainder: [001..003) [004..005)
- => Remainder strategy: retain a full block op+ => Remainder strategy: int at +001
 ; V00 this [V00,T01] ( 3, 3 ) ref -> rcx this class-hnd single-def
;* V01 loc0 [V01 ] ( 0, 0 ) struct ( 8) zero-ref do-not-enreg[SF] ld-addr-op
;# V02 OutArgs [V02 ] ( 1, 1 ) struct ( 0) [rsp+00H] do-not-enreg[XS] addr-exposed "OutgoingArgSpace"
;* V03 tmp1 [V03 ] ( 0, 0 ) ubyte -> zero-ref "V01.[000..001)"
;* V04 tmp2 [V04 ] ( 0, 0 ) ubyte -> zero-ref "V01.[003..004)"
;* V05 tmp3 [V05 ] ( 0, 0 ) ubyte -> zero-ref "V01.[005..006)"
; V06 tmp4 [V06,T00] ( 5, 10 ) byref -> rcx single-def "Spilling address for field-by-field copy"
;
; Lcl frame size = 0
G_M52879_IG01: ;; offset=0000H
;; size=0 bbWeight=1 PerfScore 0.00
G_M52879_IG02: ;; offset=0000H
add rcx, 8
xor eax, eax
- mov dword ptr [rcx], eax- mov dword ptr [rcx+02H], eax+ mov dword ptr [rcx+01H], eax
mov byte ptr [rcx], 10
mov byte ptr [rcx+03H], 20
mov byte ptr [rcx+05H], 30
- ;; size=22 bbWeight=1 PerfScore 5.50-G_M52879_IG03: ;; offset=0016H+ ;; size=20 bbWeight=1 PerfScore 4.50+G_M52879_IG03: ;; offset=0014H
ret ;; size=1 bbWeight=1 PerfScore 1.00
-; Total bytes of code 23, prolog size 0, PerfScore 8.80, instruction count 8, allocated bytes for code 23 (MethodHash=1da13170) for method Program:Foo():this (FullOpts)+; Total bytes of code 21, prolog size 0, PerfScore 7.60, instruction count 7, allocated bytes for code 21 (MethodHash=1da13170) for method Program:Foo():this (FullOpts)
; ============================================================

@jakobbotsch
jakobbotsch requested a review from EgorBoJuly 3, 2023 08:36
@jakobbotschjakobbotsch changed the title JIT: Handle primitive-sized remainders overlapping padding in physical promotionJIT: Handle primitive-sized remainders overlapping padding/promotions in physical promotionJul 3, 2023

@EgorBoEgorBo left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are regressions like this

@@ -13,46 +13,48 @@
; V02 arg1 [V02,T02] ( 3, 3 ) ref -> r8 class-hnd single-def
; V03 arg2 [V03,T03] ( 3, 3 ) struct ( 8) r9 single-def
;# V04 OutArgs [V04 ] ( 1, 1 ) struct ( 0) [rsp+00H] do-not-enreg[XS] addr-exposed "OutgoingArgSpace"
-; V05 tmp1 [V05,T01] ( 3, 6 ) struct (16) [rsp+00H] do-not-enreg[SF] must-init ld-addr-op "NewObj constructor temp"+; V05 tmp1 [V05,T04] ( 2, 4 ) struct (16) [rsp+08H] do-not-enreg[SF] must-init ld-addr-op "NewObj constructor temp"
;* V06 tmp2 [V06 ] ( 0, 0 ) long -> zero-ref "spilling helperCall"
;* V07 tmp3 [V07 ] ( 0, 0 ) ref -> zero-ref single-def "V05.[000..008)"
+; V08 tmp4 [V08,T01] ( 3, 6 ) byref -> rsi single-def "Spilling address for field-by-field copy"
;
-; Lcl frame size = 16+; Lcl frame size = 24
G_M36974_IG01: ; bbWeight=1, gcrefRegs=0000 {}, byrefRegs=0000 {}, byref, nogc <-- Prolog IG
- push rdi
push rsi
push rbx
- sub rsp, 16+ sub rsp, 24
xor eax, eax
- mov qword ptr [rsp], rax+ mov qword ptr [rsp+08H], rax
mov rbx, rcx
; byrRegs +[rbx]
- ;; size=16 bbWeight=1 PerfScore 4.75+ ;; size=16 bbWeight=1 PerfScore 3.75
G_M36974_IG02: ; bbWeight=1, gcrefRegs=0100 {r8}, byrefRegs=0008 {rbx}, byref
; gcrRegs +[r8]
- mov qword ptr [rsp+08H], r9- mov gword ptr [rsp], r8- lea rdi, bword ptr [rbx+08H]- ; byrRegs +[rdi]- lea rsi, bword ptr [rsp]+ mov qword ptr [rsp+10H], r9+ lea rsi, bword ptr [rbx+08H]
; byrRegs +[rsi]
- call CORINFO_HELP_ASSIGN_BYREF- ; gcrRegs -[r8]- movsq + mov rcx, rsi+ ; byrRegs +[rcx]+ mov rdx, r8+ ; gcrRegs +[rdx]+ call CORINFO_HELP_CHECKED_ASSIGN_REF+ ; gcrRegs -[rdx r8]+ ; byrRegs -[rcx]+ mov rax, qword ptr [rsp+10H]+ mov qword ptr [rsi+08H], rax
xor rax, rax
; gcrRegs +[rax]
mov gword ptr [rbx], rax
- ;; size=29 bbWeight=1 PerfScore 6.25+ ;; size=34 bbWeight=1 PerfScore 6.25
G_M36974_IG03: ; bbWeight=1, epilog, nogc, extend
- add rsp, 16+ add rsp, 24
pop rbx
pop rsi
- pop rdi
ret - ;; size=8 bbWeight=1 PerfScore 2.75+ ;; size=7 bbWeight=1 PerfScore 2.25-; Total bytes of code 53, prolog size 13, PerfScore 19.05, instruction count 20, allocated bytes for code 53 (MethodHash=ebd16f91) for method System.Linq.Parallel.HashLookupValueList`2[System.__Canon,System.Nullable`1[int]]:.ctor(System.__Canon,System.Nullable`1[int]):this (FullOpts)+; Total bytes of code 57, prolog size 13, PerfScore 17.95, instruction count 19, allocated bytes for code 57 (MethodHash=ebd16f91) for method System.Linq.Parallel.HashLookupValueList`2[System.__Canon,System.Nullable`1[int]]:.ctor(System.__Canon,System.Nullable`1[int]):this (FullOpts)
; ============================================================

expected?

@jakobbotsch

Copy link
Copy Markdown
MemberAuthor

Generally we do expect to replace CORINFO_HELP_ASSIGN_BYREF with CORINFO_HELP_CHECKED_ASSIGN_REF in the cases where we switch the remainder from a block copy (where the block has GC pointers) to a TYP_REF primitive copy.
That has the side effect that we no longer get the destination/source increments "for free" as part of that helper, so I suppose that can sometimes result in bigger code. But notice that in this case we get rid of a movsq which is a very expensive instruction, so perf wise I would expect this to be beneficial.

Now, this case is a bit peculiar because the fact that we no longer uses the full block copy for the remainder turns off an unrelated optimization:

@@ -1,20 +1,28 @@
STMT00001 ( 0x000[E-] ... ??? )
[000027] nA-XG------ ▌ STORE_BLK struct<System.Linq.Parallel.Pair`2, 16> (copy)
[000025] ---X------- ├──▌ FIELD_ADDR byref <unknown class>:<unknown field>
[000026] ----------- │ └──▌ LCL_VAR byref V00 this [000024] ----------- └──▌ LCL_VAR struct<System.Linq.Parallel.Pair`2, 16> V05 tmp1 (last use)
Processing block operation [000027] that involves replacements
dst+000 <- V07 (V05.[000..008)) (last use)
Block op remainder: [008..009) [012..016)
- => Remainder strategy: retain a full block op- Will write back V07 (V05.[000..008)) to avoid an additional write barrier- Skipping dst+000 <- V07 (V05.[000..008)); it is up-to-date in its struct local and will be handled as part of the remainder+ => Remainder strategy: long at +008++lvaGrabTemp returning 8 (V08 tmp4) called for Spilling address for field-by-field copy.++Local V05 should not be enregistered because: was accessed as a local field
New statement:
STMT00001 ( 0x000[E-] ... ??? )
- [000040] -A-XG------ ▌ COMMA void - [000039] UA--------- ├──▌ STORE_LCL_FLD ref V05 tmp1 [+0]- [000038] ----------- │ └──▌ LCL_VAR ref V07 tmp3 - [000027] nA-XG------ └──▌ STORE_BLK struct<System.Linq.Parallel.Pair`2, 16> (copy)- [000025] ---X------- ├──▌ FIELD_ADDR byref <unknown class>:<unknown field>- [000026] ----------- │ └──▌ LCL_VAR byref V00 this - [000024] ----------- └──▌ LCL_VAR struct<System.Linq.Parallel.Pair`2, 16> V05 tmp1 + [000048] -A-XG------ ▌ COMMA void + [000038] DA-X------- ├──▌ STORE_LCL_VAR byref V08 tmp4 + [000025] ---X------- │ └──▌ FIELD_ADDR byref <unknown class>:<unknown field>+ [000026] ----------- │ └──▌ LCL_VAR byref V00 this + [000047] -A--G------ └──▌ COMMA void + [000042] nA--G------ ├──▌ STOREIND ref + [000041] ----------- │ ├──▌ LCL_VAR byref V08 tmp4 + [000040] ----------- │ └──▌ LCL_VAR ref V07 tmp3 (last use)+ [000046] nA--G------ └──▌ STOREIND long + [000045] ----------- ├──▌ ADD byref + [000039] ----------- │ ├──▌ LCL_VAR byref V08 tmp4 + [000044] ----------- │ └──▌ CNS_INT long 8+ [000043] ----------- └──▌ LCL_FLD long V05 tmp1 [+8]

Specifically, in the base we notice that we have a promoted TYP_REF field that is being written to heap, which will involve a write barrier, in addition to the write barrier imposed for the block copy. So we write it back to stack first, to avoid this extra write barrier.
This has the side effect that we don't need to write it back after the write barrier call. It also means we don't need to save the target address to a local because we end up only using the address once.

In the diff we don't do this optimization anymore and instead just copy the promoted TYP_REF directly to the heap, incurring the write barrier. It doesn't really matter because overall we still only end up needing one write barrier.

We should be able to avoid the spilled local address in this case by teaching physical promotion to peel FIELD_ADDR nodes, though it comes with a bit of complexity to get the right null checking behavior. It would probably have quite decent diffs, though. Will open an issue for that.

@jakobbotsch
jakobbotsch merged commit 210a7a5 into dotnet:mainJul 4, 2023
@jakobbotsch
jakobbotsch deleted the physical-promotion-covering-segment branch July 4, 2023 16:02
@ghostghost locked as resolved and limited conversation to collaborators Aug 3, 2023
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@jakobbotsch@EgorBo
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' JIT: Handle primitive-sized remainders overlapping padding/promotions in physical promotion by jakobbotsch · Pull Request #88109 · dotnet/runtime · GitHub
Skip to content

JIT: Handle primitive-sized remainders overlapping padding/promotions in physical promotion - #88109

Merged
jakobbotsch merged 3 commits into
dotnet:mainfrom
jakobbotsch:physical-promotion-covering-segment
Jul 4, 2023
Merged

JIT: Handle primitive-sized remainders overlapping padding/promotions in physical promotion#88109
jakobbotsch merged 3 commits into
dotnet:mainfrom
jakobbotsch:physical-promotion-covering-segment

Conversation

@jakobbotsch

@jakobbotschjakobbotsch commented Jun 27, 2023

Copy link
Copy Markdown
Member

The remainder may be separated by a bit of padding or other promoted fields but still fit into a primitive; in this case it is still beneficial to copy it all as a primitive, instead of falling back to a full block copy.

Example.

privateS_s;voidFoo(){Ss=new();s.A=10;s.D=20;s.F=30;// A, D, F gets promoted_s=s;}privatestructS{publicbyteA;publicbyteB;publicbyteC;publicbyteD;publicbyteE;publicbyteF;}
 Processing block operation [000018] that involves replacements
dst+003 <- V04 (V01.[003..004)) (last use)
dst+005 <- V05 (V01.[005..006)) (last use)
Block op remainder: [001..003) [004..005)
- => Remainder strategy: retain a full block op+ => Remainder strategy: int at +001
 ; V00 this [V00,T01] ( 3, 3 ) ref -> rcx this class-hnd single-def
;* V01 loc0 [V01 ] ( 0, 0 ) struct ( 8) zero-ref do-not-enreg[SF] ld-addr-op
;# V02 OutArgs [V02 ] ( 1, 1 ) struct ( 0) [rsp+00H] do-not-enreg[XS] addr-exposed "OutgoingArgSpace"
;* V03 tmp1 [V03 ] ( 0, 0 ) ubyte -> zero-ref "V01.[000..001)"
;* V04 tmp2 [V04 ] ( 0, 0 ) ubyte -> zero-ref "V01.[003..004)"
;* V05 tmp3 [V05 ] ( 0, 0 ) ubyte -> zero-ref "V01.[005..006)"
; V06 tmp4 [V06,T00] ( 5, 10 ) byref -> rcx single-def "Spilling address for field-by-field copy"
;
; Lcl frame size = 0
G_M52879_IG01: ;; offset=0000H
;; size=0 bbWeight=1 PerfScore 0.00
G_M52879_IG02: ;; offset=0000H
add rcx, 8
xor eax, eax
- mov dword ptr [rcx], eax- mov dword ptr [rcx+02H], eax+ mov dword ptr [rcx+01H], eax
mov byte ptr [rcx], 10
mov byte ptr [rcx+03H], 20
mov byte ptr [rcx+05H], 30
- ;; size=22 bbWeight=1 PerfScore 5.50-G_M52879_IG03: ;; offset=0016H+ ;; size=20 bbWeight=1 PerfScore 4.50+G_M52879_IG03: ;; offset=0014H
ret ;; size=1 bbWeight=1 PerfScore 1.00
-; Total bytes of code 23, prolog size 0, PerfScore 8.80, instruction count 8, allocated bytes for code 23 (MethodHash=1da13170) for method Program:Foo():this (FullOpts)+; Total bytes of code 21, prolog size 0, PerfScore 7.60, instruction count 7, allocated bytes for code 21 (MethodHash=1da13170) for method Program:Foo():this (FullOpts)
; ============================================================

We have to be careful, however, since the covering segment can now contain promoted fields. If this happens we need to make sure we write the promoted field after the remainder.

@ghostghost added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Jun 27, 2023
@ghost

Copy link
Copy Markdown

Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.

Issue Details

The remainder may be separated by a bit of padding but still fit into a primitive; in this case it is still beneficial to copy it all as a primitive, instead of falling back to a full block copy.

Example.
Before:

Processing block operation [000737] that involves replacements
dst+000<-V101 (V47.[000..008)) (last use)
dst+008<-V102 (V47.[008..016)) (last use)
dst+016<-V103 (V47.[016..024)) (last use)
dst+024<-V104 (V47.[024..028)) (last use)
dst+028<-V105 (V47.[028..029)) (last use)
Block op remainder: [032..033) [036..040)
=>Remainderstrategy: retain a full block op

After:

Processing block operation [000737] that involves replacements
dst+000<-V101 (V47.[000..008)) (last use)
dst+008<-V102 (V47.[008..016)) (last use)
dst+016<-V103 (V47.[016..024)) (last use)
dst+024<-V104 (V47.[024..028)) (last use)
dst+028<-V105 (V47.[028..029)) (last use)
Block op remainder: [032..033) [036..040)
=>Remainderstrategy: long at +032

This leads to ASM diffs like the following:

 xor edx, edx
mov qword ptr [rsp+148H], rdx
- ;; size=20 bbWeight=0.33 PerfScore 1.57-G_M6338_IG24: ; bbWeight=0.33, nogc, extend- vmovdqu ymm0, ymmword ptr [rsp+128H]- vmovdqu ymmword ptr [rsp+D8H], ymm0- mov rdx, qword ptr [rsp+148H]- mov qword ptr [rsp+F8H], rdx- ;; size=34 bbWeight=0.33 PerfScore 2.32-G_M6338_IG25: ; bbWeight=0.33, extend
mov gword ptr [rsp+D8H], r12
- xor rdx, rdx- ; gcrRegs +[rdx]+ ;; size=28 bbWeight=0.33 PerfScore 1.90+G_M6338_IG24: ; bbWeight=0.33, gcrefRegs=1028 {rbx rbp r12}, byrefRegs=4000 {r14}, byref
mov gword ptr [rsp+E0H], rdx
- ;; size=18 bbWeight=0.33 PerfScore 0.75-G_M6338_IG26: ; bbWeight=0.33, gcrefRegs=1028 {rbx rbp r12}, byrefRegs=4000 {r14}, byref- ; gcrRegs -[rdx]+ ;; size=8 bbWeight=0.33 PerfScore 0.33+G_M6338_IG25: ; bbWeight=0.33, gcrefRegs=1028 {rbx rbp r12}, byrefRegs=4000 {r14}, byref
mov gword ptr [rsp+E8H], rdx
mov dword ptr [rsp+F0H], r9d
mov byte ptr [rsp+F4H], 0
+ ;; size=24 bbWeight=0.33 PerfScore 0.99+G_M6338_IG26: ; bbWeight=0.33, gcrefRegs=1028 {rbx rbp r12}, byrefRegs=4000 {r14}, byref+ mov qword ptr [rsp+F8H], rdx

We have to be careful, however, since the covering segment can now contain promoted fields. If this happens we need to make sure we write the promoted field after the remainder.

Author:jakobbotsch
Assignees:-
Labels:

area-CodeGen-coreclr

Milestone:-

…l promotion
The remainder may be separated by a bit of padding but still fit into a
primitive; in this case it is still beneficial to copy it all as a
primitive, instead of falling back to a full block copy.
Example.
Before:
```
Processing block operation [000737] that involves replacements
dst+000 <- V101 (V47.[000..008)) (last use)
dst+008 <- V102 (V47.[008..016)) (last use)
dst+016 <- V103 (V47.[016..024)) (last use)
dst+024 <- V104 (V47.[024..028)) (last use)
dst+028 <- V105 (V47.[028..029)) (last use)
Block op remainder: [032..033) [036..040)
=> Remainder strategy: retain a full block op
```
After:
```
Processing block operation [000737] that involves replacements
dst+000 <- V101 (V47.[000..008)) (last use)
dst+008 <- V102 (V47.[008..016)) (last use)
dst+016 <- V103 (V47.[016..024)) (last use)
dst+024 <- V104 (V47.[024..028)) (last use)
dst+028 <- V105 (V47.[028..029)) (last use)
Block op remainder: [032..033) [036..040)
=> Remainder strategy: long at +032
```
This leads to ASM diffs like the following:
```diff
xor edx, edx
mov qword ptr [rsp+148H], rdx
- ;; size=20 bbWeight=0.33 PerfScore 1.57
-G_M6338_IG24: ; bbWeight=0.33, nogc, extend
- vmovdqu ymm0, ymmword ptr [rsp+128H]
- vmovdqu ymmword ptr [rsp+D8H], ymm0
- mov rdx, qword ptr [rsp+148H]
- mov qword ptr [rsp+F8H], rdx
- ;; size=34 bbWeight=0.33 PerfScore 2.32
-G_M6338_IG25: ; bbWeight=0.33, extend
mov gword ptr [rsp+D8H], r12
- xor rdx, rdx
- ; gcrRegs +[rdx]
+ ;; size=28 bbWeight=0.33 PerfScore 1.90
+G_M6338_IG24: ; bbWeight=0.33, gcrefRegs=1028 {rbx rbp r12}, byrefRegs=4000 {r14}, byref
mov gword ptr [rsp+E0H], rdx
- ;; size=18 bbWeight=0.33 PerfScore 0.75
-G_M6338_IG26: ; bbWeight=0.33, gcrefRegs=1028 {rbx rbp r12}, byrefRegs=4000 {r14}, byref
- ; gcrRegs -[rdx]
+ ;; size=8 bbWeight=0.33 PerfScore 0.33
+G_M6338_IG25: ; bbWeight=0.33, gcrefRegs=1028 {rbx rbp r12}, byrefRegs=4000 {r14}, byref
mov gword ptr [rsp+E8H], rdx
mov dword ptr [rsp+F0H], r9d
mov byte ptr [rsp+F4H], 0
+ ;; size=24 bbWeight=0.33 PerfScore 0.99
+G_M6338_IG26: ; bbWeight=0.33, gcrefRegs=1028 {rbx rbp r12}, byrefRegs=4000 {r14}, byref
+ mov qword ptr [rsp+F8H], rdx
```
We have to be careful, however, since the covering segment can now
contain promoted fields. If this happens we need to make sure we write
the promoted field _after_ the remainder.
Unfortunately doing this requires quite a bit of refactoring. I have
extracted all common code for handling creation of derived accesses of
the destination/source into a common class called LocationAccess.
@jakobbotsch
jakobbotschforce-pushed the physical-promotion-covering-segment branch from 4ec5010 to 0c9b1c5CompareJune 30, 2023 22:07
@jakobbotsch

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-coreclr superpmi-diffs

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@jakobbotsch

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-coreclr superpmi-diffs

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@jakobbotsch
jakobbotsch marked this pull request as ready for review July 3, 2023 08:16
@jakobbotsch

Copy link
Copy Markdown
MemberAuthor

cc @dotnet/jit-contrib PTAL @EgorBo

Diffs. This is not just about padding, but also when the remainder is overlapping with promoted fields. A C# example of the difference:

privateS_s;voidFoo(){Ss=new();s.A=10;s.D=20;s.F=30;// A, D, F gets promoted_s=s;}privatestructS{publicbyteA;publicbyteB;publicbyteC;publicbyteD;publicbyteE;publicbyteF;}
 Processing block operation [000018] that involves replacements
dst+003 <- V04 (V01.[003..004)) (last use)
dst+005 <- V05 (V01.[005..006)) (last use)
Block op remainder: [001..003) [004..005)
- => Remainder strategy: retain a full block op+ => Remainder strategy: int at +001
 ; V00 this [V00,T01] ( 3, 3 ) ref -> rcx this class-hnd single-def
;* V01 loc0 [V01 ] ( 0, 0 ) struct ( 8) zero-ref do-not-enreg[SF] ld-addr-op
;# V02 OutArgs [V02 ] ( 1, 1 ) struct ( 0) [rsp+00H] do-not-enreg[XS] addr-exposed "OutgoingArgSpace"
;* V03 tmp1 [V03 ] ( 0, 0 ) ubyte -> zero-ref "V01.[000..001)"
;* V04 tmp2 [V04 ] ( 0, 0 ) ubyte -> zero-ref "V01.[003..004)"
;* V05 tmp3 [V05 ] ( 0, 0 ) ubyte -> zero-ref "V01.[005..006)"
; V06 tmp4 [V06,T00] ( 5, 10 ) byref -> rcx single-def "Spilling address for field-by-field copy"
;
; Lcl frame size = 0
G_M52879_IG01: ;; offset=0000H
;; size=0 bbWeight=1 PerfScore 0.00
G_M52879_IG02: ;; offset=0000H
add rcx, 8
xor eax, eax
- mov dword ptr [rcx], eax- mov dword ptr [rcx+02H], eax+ mov dword ptr [rcx+01H], eax
mov byte ptr [rcx], 10
mov byte ptr [rcx+03H], 20
mov byte ptr [rcx+05H], 30
- ;; size=22 bbWeight=1 PerfScore 5.50-G_M52879_IG03: ;; offset=0016H+ ;; size=20 bbWeight=1 PerfScore 4.50+G_M52879_IG03: ;; offset=0014H
ret ;; size=1 bbWeight=1 PerfScore 1.00
-; Total bytes of code 23, prolog size 0, PerfScore 8.80, instruction count 8, allocated bytes for code 23 (MethodHash=1da13170) for method Program:Foo():this (FullOpts)+; Total bytes of code 21, prolog size 0, PerfScore 7.60, instruction count 7, allocated bytes for code 21 (MethodHash=1da13170) for method Program:Foo():this (FullOpts)
; ============================================================

@jakobbotsch
jakobbotsch requested a review from EgorBoJuly 3, 2023 08:36
@jakobbotschjakobbotsch changed the title JIT: Handle primitive-sized remainders overlapping padding in physical promotionJIT: Handle primitive-sized remainders overlapping padding/promotions in physical promotionJul 3, 2023

@EgorBoEgorBo left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are regressions like this

@@ -13,46 +13,48 @@
; V02 arg1 [V02,T02] ( 3, 3 ) ref -> r8 class-hnd single-def
; V03 arg2 [V03,T03] ( 3, 3 ) struct ( 8) r9 single-def
;# V04 OutArgs [V04 ] ( 1, 1 ) struct ( 0) [rsp+00H] do-not-enreg[XS] addr-exposed "OutgoingArgSpace"
-; V05 tmp1 [V05,T01] ( 3, 6 ) struct (16) [rsp+00H] do-not-enreg[SF] must-init ld-addr-op "NewObj constructor temp"+; V05 tmp1 [V05,T04] ( 2, 4 ) struct (16) [rsp+08H] do-not-enreg[SF] must-init ld-addr-op "NewObj constructor temp"
;* V06 tmp2 [V06 ] ( 0, 0 ) long -> zero-ref "spilling helperCall"
;* V07 tmp3 [V07 ] ( 0, 0 ) ref -> zero-ref single-def "V05.[000..008)"
+; V08 tmp4 [V08,T01] ( 3, 6 ) byref -> rsi single-def "Spilling address for field-by-field copy"
;
-; Lcl frame size = 16+; Lcl frame size = 24
G_M36974_IG01: ; bbWeight=1, gcrefRegs=0000 {}, byrefRegs=0000 {}, byref, nogc <-- Prolog IG
- push rdi
push rsi
push rbx
- sub rsp, 16+ sub rsp, 24
xor eax, eax
- mov qword ptr [rsp], rax+ mov qword ptr [rsp+08H], rax
mov rbx, rcx
; byrRegs +[rbx]
- ;; size=16 bbWeight=1 PerfScore 4.75+ ;; size=16 bbWeight=1 PerfScore 3.75
G_M36974_IG02: ; bbWeight=1, gcrefRegs=0100 {r8}, byrefRegs=0008 {rbx}, byref
; gcrRegs +[r8]
- mov qword ptr [rsp+08H], r9- mov gword ptr [rsp], r8- lea rdi, bword ptr [rbx+08H]- ; byrRegs +[rdi]- lea rsi, bword ptr [rsp]+ mov qword ptr [rsp+10H], r9+ lea rsi, bword ptr [rbx+08H]
; byrRegs +[rsi]
- call CORINFO_HELP_ASSIGN_BYREF- ; gcrRegs -[r8]- movsq + mov rcx, rsi+ ; byrRegs +[rcx]+ mov rdx, r8+ ; gcrRegs +[rdx]+ call CORINFO_HELP_CHECKED_ASSIGN_REF+ ; gcrRegs -[rdx r8]+ ; byrRegs -[rcx]+ mov rax, qword ptr [rsp+10H]+ mov qword ptr [rsi+08H], rax
xor rax, rax
; gcrRegs +[rax]
mov gword ptr [rbx], rax
- ;; size=29 bbWeight=1 PerfScore 6.25+ ;; size=34 bbWeight=1 PerfScore 6.25
G_M36974_IG03: ; bbWeight=1, epilog, nogc, extend
- add rsp, 16+ add rsp, 24
pop rbx
pop rsi
- pop rdi
ret - ;; size=8 bbWeight=1 PerfScore 2.75+ ;; size=7 bbWeight=1 PerfScore 2.25-; Total bytes of code 53, prolog size 13, PerfScore 19.05, instruction count 20, allocated bytes for code 53 (MethodHash=ebd16f91) for method System.Linq.Parallel.HashLookupValueList`2[System.__Canon,System.Nullable`1[int]]:.ctor(System.__Canon,System.Nullable`1[int]):this (FullOpts)+; Total bytes of code 57, prolog size 13, PerfScore 17.95, instruction count 19, allocated bytes for code 57 (MethodHash=ebd16f91) for method System.Linq.Parallel.HashLookupValueList`2[System.__Canon,System.Nullable`1[int]]:.ctor(System.__Canon,System.Nullable`1[int]):this (FullOpts)
; ============================================================

expected?

@jakobbotsch

Copy link
Copy Markdown
MemberAuthor

Generally we do expect to replace CORINFO_HELP_ASSIGN_BYREF with CORINFO_HELP_CHECKED_ASSIGN_REF in the cases where we switch the remainder from a block copy (where the block has GC pointers) to a TYP_REF primitive copy.
That has the side effect that we no longer get the destination/source increments "for free" as part of that helper, so I suppose that can sometimes result in bigger code. But notice that in this case we get rid of a movsq which is a very expensive instruction, so perf wise I would expect this to be beneficial.

Now, this case is a bit peculiar because the fact that we no longer uses the full block copy for the remainder turns off an unrelated optimization:

@@ -1,20 +1,28 @@
STMT00001 ( 0x000[E-] ... ??? )
[000027] nA-XG------ ▌ STORE_BLK struct<System.Linq.Parallel.Pair`2, 16> (copy)
[000025] ---X------- ├──▌ FIELD_ADDR byref <unknown class>:<unknown field>
[000026] ----------- │ └──▌ LCL_VAR byref V00 this [000024] ----------- └──▌ LCL_VAR struct<System.Linq.Parallel.Pair`2, 16> V05 tmp1 (last use)
Processing block operation [000027] that involves replacements
dst+000 <- V07 (V05.[000..008)) (last use)
Block op remainder: [008..009) [012..016)
- => Remainder strategy: retain a full block op- Will write back V07 (V05.[000..008)) to avoid an additional write barrier- Skipping dst+000 <- V07 (V05.[000..008)); it is up-to-date in its struct local and will be handled as part of the remainder+ => Remainder strategy: long at +008++lvaGrabTemp returning 8 (V08 tmp4) called for Spilling address for field-by-field copy.++Local V05 should not be enregistered because: was accessed as a local field
New statement:
STMT00001 ( 0x000[E-] ... ??? )
- [000040] -A-XG------ ▌ COMMA void - [000039] UA--------- ├──▌ STORE_LCL_FLD ref V05 tmp1 [+0]- [000038] ----------- │ └──▌ LCL_VAR ref V07 tmp3 - [000027] nA-XG------ └──▌ STORE_BLK struct<System.Linq.Parallel.Pair`2, 16> (copy)- [000025] ---X------- ├──▌ FIELD_ADDR byref <unknown class>:<unknown field>- [000026] ----------- │ └──▌ LCL_VAR byref V00 this - [000024] ----------- └──▌ LCL_VAR struct<System.Linq.Parallel.Pair`2, 16> V05 tmp1 + [000048] -A-XG------ ▌ COMMA void + [000038] DA-X------- ├──▌ STORE_LCL_VAR byref V08 tmp4 + [000025] ---X------- │ └──▌ FIELD_ADDR byref <unknown class>:<unknown field>+ [000026] ----------- │ └──▌ LCL_VAR byref V00 this + [000047] -A--G------ └──▌ COMMA void + [000042] nA--G------ ├──▌ STOREIND ref + [000041] ----------- │ ├──▌ LCL_VAR byref V08 tmp4 + [000040] ----------- │ └──▌ LCL_VAR ref V07 tmp3 (last use)+ [000046] nA--G------ └──▌ STOREIND long + [000045] ----------- ├──▌ ADD byref + [000039] ----------- │ ├──▌ LCL_VAR byref V08 tmp4 + [000044] ----------- │ └──▌ CNS_INT long 8+ [000043] ----------- └──▌ LCL_FLD long V05 tmp1 [+8]

Specifically, in the base we notice that we have a promoted TYP_REF field that is being written to heap, which will involve a write barrier, in addition to the write barrier imposed for the block copy. So we write it back to stack first, to avoid this extra write barrier.
This has the side effect that we don't need to write it back after the write barrier call. It also means we don't need to save the target address to a local because we end up only using the address once.

In the diff we don't do this optimization anymore and instead just copy the promoted TYP_REF directly to the heap, incurring the write barrier. It doesn't really matter because overall we still only end up needing one write barrier.

We should be able to avoid the spilled local address in this case by teaching physical promotion to peel FIELD_ADDR nodes, though it comes with a bit of complexity to get the right null checking behavior. It would probably have quite decent diffs, though. Will open an issue for that.

@jakobbotsch
jakobbotsch merged commit 210a7a5 into dotnet:mainJul 4, 2023
@jakobbotsch
jakobbotsch deleted the physical-promotion-covering-segment branch July 4, 2023 16:02
@ghostghost locked as resolved and limited conversation to collaborators Aug 3, 2023
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@jakobbotsch@EgorBo
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' JIT: Handle primitive-sized remainders overlapping padding/promotions in physical promotion by jakobbotsch · Pull Request #88109 · dotnet/runtime · GitHub
Skip to content

JIT: Handle primitive-sized remainders overlapping padding/promotions in physical promotion - #88109

Merged
jakobbotsch merged 3 commits into
dotnet:mainfrom
jakobbotsch:physical-promotion-covering-segment
Jul 4, 2023
Merged

JIT: Handle primitive-sized remainders overlapping padding/promotions in physical promotion#88109
jakobbotsch merged 3 commits into
dotnet:mainfrom
jakobbotsch:physical-promotion-covering-segment

Conversation

@jakobbotsch

@jakobbotschjakobbotsch commented Jun 27, 2023

Copy link
Copy Markdown
Member

The remainder may be separated by a bit of padding or other promoted fields but still fit into a primitive; in this case it is still beneficial to copy it all as a primitive, instead of falling back to a full block copy.

Example.

privateS_s;voidFoo(){Ss=new();s.A=10;s.D=20;s.F=30;// A, D, F gets promoted_s=s;}privatestructS{publicbyteA;publicbyteB;publicbyteC;publicbyteD;publicbyteE;publicbyteF;}
 Processing block operation [000018] that involves replacements
dst+003 <- V04 (V01.[003..004)) (last use)
dst+005 <- V05 (V01.[005..006)) (last use)
Block op remainder: [001..003) [004..005)
- => Remainder strategy: retain a full block op+ => Remainder strategy: int at +001
 ; V00 this [V00,T01] ( 3, 3 ) ref -> rcx this class-hnd single-def
;* V01 loc0 [V01 ] ( 0, 0 ) struct ( 8) zero-ref do-not-enreg[SF] ld-addr-op
;# V02 OutArgs [V02 ] ( 1, 1 ) struct ( 0) [rsp+00H] do-not-enreg[XS] addr-exposed "OutgoingArgSpace"
;* V03 tmp1 [V03 ] ( 0, 0 ) ubyte -> zero-ref "V01.[000..001)"
;* V04 tmp2 [V04 ] ( 0, 0 ) ubyte -> zero-ref "V01.[003..004)"
;* V05 tmp3 [V05 ] ( 0, 0 ) ubyte -> zero-ref "V01.[005..006)"
; V06 tmp4 [V06,T00] ( 5, 10 ) byref -> rcx single-def "Spilling address for field-by-field copy"
;
; Lcl frame size = 0
G_M52879_IG01: ;; offset=0000H
;; size=0 bbWeight=1 PerfScore 0.00
G_M52879_IG02: ;; offset=0000H
add rcx, 8
xor eax, eax
- mov dword ptr [rcx], eax- mov dword ptr [rcx+02H], eax+ mov dword ptr [rcx+01H], eax
mov byte ptr [rcx], 10
mov byte ptr [rcx+03H], 20
mov byte ptr [rcx+05H], 30
- ;; size=22 bbWeight=1 PerfScore 5.50-G_M52879_IG03: ;; offset=0016H+ ;; size=20 bbWeight=1 PerfScore 4.50+G_M52879_IG03: ;; offset=0014H
ret ;; size=1 bbWeight=1 PerfScore 1.00
-; Total bytes of code 23, prolog size 0, PerfScore 8.80, instruction count 8, allocated bytes for code 23 (MethodHash=1da13170) for method Program:Foo():this (FullOpts)+; Total bytes of code 21, prolog size 0, PerfScore 7.60, instruction count 7, allocated bytes for code 21 (MethodHash=1da13170) for method Program:Foo():this (FullOpts)
; ============================================================

We have to be careful, however, since the covering segment can now contain promoted fields. If this happens we need to make sure we write the promoted field after the remainder.

@ghostghost added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Jun 27, 2023
@ghost

Copy link
Copy Markdown

Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.

Issue Details

The remainder may be separated by a bit of padding but still fit into a primitive; in this case it is still beneficial to copy it all as a primitive, instead of falling back to a full block copy.

Example.
Before:

Processing block operation [000737] that involves replacements
dst+000<-V101 (V47.[000..008)) (last use)
dst+008<-V102 (V47.[008..016)) (last use)
dst+016<-V103 (V47.[016..024)) (last use)
dst+024<-V104 (V47.[024..028)) (last use)
dst+028<-V105 (V47.[028..029)) (last use)
Block op remainder: [032..033) [036..040)
=>Remainderstrategy: retain a full block op

After:

Processing block operation [000737] that involves replacements
dst+000<-V101 (V47.[000..008)) (last use)
dst+008<-V102 (V47.[008..016)) (last use)
dst+016<-V103 (V47.[016..024)) (last use)
dst+024<-V104 (V47.[024..028)) (last use)
dst+028<-V105 (V47.[028..029)) (last use)
Block op remainder: [032..033) [036..040)
=>Remainderstrategy: long at +032

This leads to ASM diffs like the following:

 xor edx, edx
mov qword ptr [rsp+148H], rdx
- ;; size=20 bbWeight=0.33 PerfScore 1.57-G_M6338_IG24: ; bbWeight=0.33, nogc, extend- vmovdqu ymm0, ymmword ptr [rsp+128H]- vmovdqu ymmword ptr [rsp+D8H], ymm0- mov rdx, qword ptr [rsp+148H]- mov qword ptr [rsp+F8H], rdx- ;; size=34 bbWeight=0.33 PerfScore 2.32-G_M6338_IG25: ; bbWeight=0.33, extend
mov gword ptr [rsp+D8H], r12
- xor rdx, rdx- ; gcrRegs +[rdx]+ ;; size=28 bbWeight=0.33 PerfScore 1.90+G_M6338_IG24: ; bbWeight=0.33, gcrefRegs=1028 {rbx rbp r12}, byrefRegs=4000 {r14}, byref
mov gword ptr [rsp+E0H], rdx
- ;; size=18 bbWeight=0.33 PerfScore 0.75-G_M6338_IG26: ; bbWeight=0.33, gcrefRegs=1028 {rbx rbp r12}, byrefRegs=4000 {r14}, byref- ; gcrRegs -[rdx]+ ;; size=8 bbWeight=0.33 PerfScore 0.33+G_M6338_IG25: ; bbWeight=0.33, gcrefRegs=1028 {rbx rbp r12}, byrefRegs=4000 {r14}, byref
mov gword ptr [rsp+E8H], rdx
mov dword ptr [rsp+F0H], r9d
mov byte ptr [rsp+F4H], 0
+ ;; size=24 bbWeight=0.33 PerfScore 0.99+G_M6338_IG26: ; bbWeight=0.33, gcrefRegs=1028 {rbx rbp r12}, byrefRegs=4000 {r14}, byref+ mov qword ptr [rsp+F8H], rdx

We have to be careful, however, since the covering segment can now contain promoted fields. If this happens we need to make sure we write the promoted field after the remainder.

Author:jakobbotsch
Assignees:-
Labels:

area-CodeGen-coreclr

Milestone:-

…l promotion
The remainder may be separated by a bit of padding but still fit into a
primitive; in this case it is still beneficial to copy it all as a
primitive, instead of falling back to a full block copy.
Example.
Before:
```
Processing block operation [000737] that involves replacements
dst+000 <- V101 (V47.[000..008)) (last use)
dst+008 <- V102 (V47.[008..016)) (last use)
dst+016 <- V103 (V47.[016..024)) (last use)
dst+024 <- V104 (V47.[024..028)) (last use)
dst+028 <- V105 (V47.[028..029)) (last use)
Block op remainder: [032..033) [036..040)
=> Remainder strategy: retain a full block op
```
After:
```
Processing block operation [000737] that involves replacements
dst+000 <- V101 (V47.[000..008)) (last use)
dst+008 <- V102 (V47.[008..016)) (last use)
dst+016 <- V103 (V47.[016..024)) (last use)
dst+024 <- V104 (V47.[024..028)) (last use)
dst+028 <- V105 (V47.[028..029)) (last use)
Block op remainder: [032..033) [036..040)
=> Remainder strategy: long at +032
```
This leads to ASM diffs like the following:
```diff
xor edx, edx
mov qword ptr [rsp+148H], rdx
- ;; size=20 bbWeight=0.33 PerfScore 1.57
-G_M6338_IG24: ; bbWeight=0.33, nogc, extend
- vmovdqu ymm0, ymmword ptr [rsp+128H]
- vmovdqu ymmword ptr [rsp+D8H], ymm0
- mov rdx, qword ptr [rsp+148H]
- mov qword ptr [rsp+F8H], rdx
- ;; size=34 bbWeight=0.33 PerfScore 2.32
-G_M6338_IG25: ; bbWeight=0.33, extend
mov gword ptr [rsp+D8H], r12
- xor rdx, rdx
- ; gcrRegs +[rdx]
+ ;; size=28 bbWeight=0.33 PerfScore 1.90
+G_M6338_IG24: ; bbWeight=0.33, gcrefRegs=1028 {rbx rbp r12}, byrefRegs=4000 {r14}, byref
mov gword ptr [rsp+E0H], rdx
- ;; size=18 bbWeight=0.33 PerfScore 0.75
-G_M6338_IG26: ; bbWeight=0.33, gcrefRegs=1028 {rbx rbp r12}, byrefRegs=4000 {r14}, byref
- ; gcrRegs -[rdx]
+ ;; size=8 bbWeight=0.33 PerfScore 0.33
+G_M6338_IG25: ; bbWeight=0.33, gcrefRegs=1028 {rbx rbp r12}, byrefRegs=4000 {r14}, byref
mov gword ptr [rsp+E8H], rdx
mov dword ptr [rsp+F0H], r9d
mov byte ptr [rsp+F4H], 0
+ ;; size=24 bbWeight=0.33 PerfScore 0.99
+G_M6338_IG26: ; bbWeight=0.33, gcrefRegs=1028 {rbx rbp r12}, byrefRegs=4000 {r14}, byref
+ mov qword ptr [rsp+F8H], rdx
```
We have to be careful, however, since the covering segment can now
contain promoted fields. If this happens we need to make sure we write
the promoted field _after_ the remainder.
Unfortunately doing this requires quite a bit of refactoring. I have
extracted all common code for handling creation of derived accesses of
the destination/source into a common class called LocationAccess.
@jakobbotsch
jakobbotschforce-pushed the physical-promotion-covering-segment branch from 4ec5010 to 0c9b1c5CompareJune 30, 2023 22:07
@jakobbotsch

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-coreclr superpmi-diffs

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@jakobbotsch

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-coreclr superpmi-diffs

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@jakobbotsch
jakobbotsch marked this pull request as ready for review July 3, 2023 08:16
@jakobbotsch

Copy link
Copy Markdown
MemberAuthor

cc @dotnet/jit-contrib PTAL @EgorBo

Diffs. This is not just about padding, but also when the remainder is overlapping with promoted fields. A C# example of the difference:

privateS_s;voidFoo(){Ss=new();s.A=10;s.D=20;s.F=30;// A, D, F gets promoted_s=s;}privatestructS{publicbyteA;publicbyteB;publicbyteC;publicbyteD;publicbyteE;publicbyteF;}
 Processing block operation [000018] that involves replacements
dst+003 <- V04 (V01.[003..004)) (last use)
dst+005 <- V05 (V01.[005..006)) (last use)
Block op remainder: [001..003) [004..005)
- => Remainder strategy: retain a full block op+ => Remainder strategy: int at +001
 ; V00 this [V00,T01] ( 3, 3 ) ref -> rcx this class-hnd single-def
;* V01 loc0 [V01 ] ( 0, 0 ) struct ( 8) zero-ref do-not-enreg[SF] ld-addr-op
;# V02 OutArgs [V02 ] ( 1, 1 ) struct ( 0) [rsp+00H] do-not-enreg[XS] addr-exposed "OutgoingArgSpace"
;* V03 tmp1 [V03 ] ( 0, 0 ) ubyte -> zero-ref "V01.[000..001)"
;* V04 tmp2 [V04 ] ( 0, 0 ) ubyte -> zero-ref "V01.[003..004)"
;* V05 tmp3 [V05 ] ( 0, 0 ) ubyte -> zero-ref "V01.[005..006)"
; V06 tmp4 [V06,T00] ( 5, 10 ) byref -> rcx single-def "Spilling address for field-by-field copy"
;
; Lcl frame size = 0
G_M52879_IG01: ;; offset=0000H
;; size=0 bbWeight=1 PerfScore 0.00
G_M52879_IG02: ;; offset=0000H
add rcx, 8
xor eax, eax
- mov dword ptr [rcx], eax- mov dword ptr [rcx+02H], eax+ mov dword ptr [rcx+01H], eax
mov byte ptr [rcx], 10
mov byte ptr [rcx+03H], 20
mov byte ptr [rcx+05H], 30
- ;; size=22 bbWeight=1 PerfScore 5.50-G_M52879_IG03: ;; offset=0016H+ ;; size=20 bbWeight=1 PerfScore 4.50+G_M52879_IG03: ;; offset=0014H
ret ;; size=1 bbWeight=1 PerfScore 1.00
-; Total bytes of code 23, prolog size 0, PerfScore 8.80, instruction count 8, allocated bytes for code 23 (MethodHash=1da13170) for method Program:Foo():this (FullOpts)+; Total bytes of code 21, prolog size 0, PerfScore 7.60, instruction count 7, allocated bytes for code 21 (MethodHash=1da13170) for method Program:Foo():this (FullOpts)
; ============================================================

@jakobbotsch
jakobbotsch requested a review from EgorBoJuly 3, 2023 08:36
@jakobbotschjakobbotsch changed the title JIT: Handle primitive-sized remainders overlapping padding in physical promotionJIT: Handle primitive-sized remainders overlapping padding/promotions in physical promotionJul 3, 2023

@EgorBoEgorBo left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are regressions like this

@@ -13,46 +13,48 @@
; V02 arg1 [V02,T02] ( 3, 3 ) ref -> r8 class-hnd single-def
; V03 arg2 [V03,T03] ( 3, 3 ) struct ( 8) r9 single-def
;# V04 OutArgs [V04 ] ( 1, 1 ) struct ( 0) [rsp+00H] do-not-enreg[XS] addr-exposed "OutgoingArgSpace"
-; V05 tmp1 [V05,T01] ( 3, 6 ) struct (16) [rsp+00H] do-not-enreg[SF] must-init ld-addr-op "NewObj constructor temp"+; V05 tmp1 [V05,T04] ( 2, 4 ) struct (16) [rsp+08H] do-not-enreg[SF] must-init ld-addr-op "NewObj constructor temp"
;* V06 tmp2 [V06 ] ( 0, 0 ) long -> zero-ref "spilling helperCall"
;* V07 tmp3 [V07 ] ( 0, 0 ) ref -> zero-ref single-def "V05.[000..008)"
+; V08 tmp4 [V08,T01] ( 3, 6 ) byref -> rsi single-def "Spilling address for field-by-field copy"
;
-; Lcl frame size = 16+; Lcl frame size = 24
G_M36974_IG01: ; bbWeight=1, gcrefRegs=0000 {}, byrefRegs=0000 {}, byref, nogc <-- Prolog IG
- push rdi
push rsi
push rbx
- sub rsp, 16+ sub rsp, 24
xor eax, eax
- mov qword ptr [rsp], rax+ mov qword ptr [rsp+08H], rax
mov rbx, rcx
; byrRegs +[rbx]
- ;; size=16 bbWeight=1 PerfScore 4.75+ ;; size=16 bbWeight=1 PerfScore 3.75
G_M36974_IG02: ; bbWeight=1, gcrefRegs=0100 {r8}, byrefRegs=0008 {rbx}, byref
; gcrRegs +[r8]
- mov qword ptr [rsp+08H], r9- mov gword ptr [rsp], r8- lea rdi, bword ptr [rbx+08H]- ; byrRegs +[rdi]- lea rsi, bword ptr [rsp]+ mov qword ptr [rsp+10H], r9+ lea rsi, bword ptr [rbx+08H]
; byrRegs +[rsi]
- call CORINFO_HELP_ASSIGN_BYREF- ; gcrRegs -[r8]- movsq + mov rcx, rsi+ ; byrRegs +[rcx]+ mov rdx, r8+ ; gcrRegs +[rdx]+ call CORINFO_HELP_CHECKED_ASSIGN_REF+ ; gcrRegs -[rdx r8]+ ; byrRegs -[rcx]+ mov rax, qword ptr [rsp+10H]+ mov qword ptr [rsi+08H], rax
xor rax, rax
; gcrRegs +[rax]
mov gword ptr [rbx], rax
- ;; size=29 bbWeight=1 PerfScore 6.25+ ;; size=34 bbWeight=1 PerfScore 6.25
G_M36974_IG03: ; bbWeight=1, epilog, nogc, extend
- add rsp, 16+ add rsp, 24
pop rbx
pop rsi
- pop rdi
ret - ;; size=8 bbWeight=1 PerfScore 2.75+ ;; size=7 bbWeight=1 PerfScore 2.25-; Total bytes of code 53, prolog size 13, PerfScore 19.05, instruction count 20, allocated bytes for code 53 (MethodHash=ebd16f91) for method System.Linq.Parallel.HashLookupValueList`2[System.__Canon,System.Nullable`1[int]]:.ctor(System.__Canon,System.Nullable`1[int]):this (FullOpts)+; Total bytes of code 57, prolog size 13, PerfScore 17.95, instruction count 19, allocated bytes for code 57 (MethodHash=ebd16f91) for method System.Linq.Parallel.HashLookupValueList`2[System.__Canon,System.Nullable`1[int]]:.ctor(System.__Canon,System.Nullable`1[int]):this (FullOpts)
; ============================================================

expected?

@jakobbotsch

Copy link
Copy Markdown
MemberAuthor

Generally we do expect to replace CORINFO_HELP_ASSIGN_BYREF with CORINFO_HELP_CHECKED_ASSIGN_REF in the cases where we switch the remainder from a block copy (where the block has GC pointers) to a TYP_REF primitive copy.
That has the side effect that we no longer get the destination/source increments "for free" as part of that helper, so I suppose that can sometimes result in bigger code. But notice that in this case we get rid of a movsq which is a very expensive instruction, so perf wise I would expect this to be beneficial.

Now, this case is a bit peculiar because the fact that we no longer uses the full block copy for the remainder turns off an unrelated optimization:

@@ -1,20 +1,28 @@
STMT00001 ( 0x000[E-] ... ??? )
[000027] nA-XG------ ▌ STORE_BLK struct<System.Linq.Parallel.Pair`2, 16> (copy)
[000025] ---X------- ├──▌ FIELD_ADDR byref <unknown class>:<unknown field>
[000026] ----------- │ └──▌ LCL_VAR byref V00 this [000024] ----------- └──▌ LCL_VAR struct<System.Linq.Parallel.Pair`2, 16> V05 tmp1 (last use)
Processing block operation [000027] that involves replacements
dst+000 <- V07 (V05.[000..008)) (last use)
Block op remainder: [008..009) [012..016)
- => Remainder strategy: retain a full block op- Will write back V07 (V05.[000..008)) to avoid an additional write barrier- Skipping dst+000 <- V07 (V05.[000..008)); it is up-to-date in its struct local and will be handled as part of the remainder+ => Remainder strategy: long at +008++lvaGrabTemp returning 8 (V08 tmp4) called for Spilling address for field-by-field copy.++Local V05 should not be enregistered because: was accessed as a local field
New statement:
STMT00001 ( 0x000[E-] ... ??? )
- [000040] -A-XG------ ▌ COMMA void - [000039] UA--------- ├──▌ STORE_LCL_FLD ref V05 tmp1 [+0]- [000038] ----------- │ └──▌ LCL_VAR ref V07 tmp3 - [000027] nA-XG------ └──▌ STORE_BLK struct<System.Linq.Parallel.Pair`2, 16> (copy)- [000025] ---X------- ├──▌ FIELD_ADDR byref <unknown class>:<unknown field>- [000026] ----------- │ └──▌ LCL_VAR byref V00 this - [000024] ----------- └──▌ LCL_VAR struct<System.Linq.Parallel.Pair`2, 16> V05 tmp1 + [000048] -A-XG------ ▌ COMMA void + [000038] DA-X------- ├──▌ STORE_LCL_VAR byref V08 tmp4 + [000025] ---X------- │ └──▌ FIELD_ADDR byref <unknown class>:<unknown field>+ [000026] ----------- │ └──▌ LCL_VAR byref V00 this + [000047] -A--G------ └──▌ COMMA void + [000042] nA--G------ ├──▌ STOREIND ref + [000041] ----------- │ ├──▌ LCL_VAR byref V08 tmp4 + [000040] ----------- │ └──▌ LCL_VAR ref V07 tmp3 (last use)+ [000046] nA--G------ └──▌ STOREIND long + [000045] ----------- ├──▌ ADD byref + [000039] ----------- │ ├──▌ LCL_VAR byref V08 tmp4 + [000044] ----------- │ └──▌ CNS_INT long 8+ [000043] ----------- └──▌ LCL_FLD long V05 tmp1 [+8]

Specifically, in the base we notice that we have a promoted TYP_REF field that is being written to heap, which will involve a write barrier, in addition to the write barrier imposed for the block copy. So we write it back to stack first, to avoid this extra write barrier.
This has the side effect that we don't need to write it back after the write barrier call. It also means we don't need to save the target address to a local because we end up only using the address once.

In the diff we don't do this optimization anymore and instead just copy the promoted TYP_REF directly to the heap, incurring the write barrier. It doesn't really matter because overall we still only end up needing one write barrier.

We should be able to avoid the spilled local address in this case by teaching physical promotion to peel FIELD_ADDR nodes, though it comes with a bit of complexity to get the right null checking behavior. It would probably have quite decent diffs, though. Will open an issue for that.

@jakobbotsch
jakobbotsch merged commit 210a7a5 into dotnet:mainJul 4, 2023
@jakobbotsch
jakobbotsch deleted the physical-promotion-covering-segment branch July 4, 2023 16:02
@ghostghost locked as resolved and limited conversation to collaborators Aug 3, 2023
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@jakobbotsch@EgorBo
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' JIT: Handle primitive-sized remainders overlapping padding/promotions in physical promotion by jakobbotsch · Pull Request #88109 · dotnet/runtime · GitHub
Skip to content

JIT: Handle primitive-sized remainders overlapping padding/promotions in physical promotion - #88109

Merged
jakobbotsch merged 3 commits into
dotnet:mainfrom
jakobbotsch:physical-promotion-covering-segment
Jul 4, 2023
Merged

JIT: Handle primitive-sized remainders overlapping padding/promotions in physical promotion#88109
jakobbotsch merged 3 commits into
dotnet:mainfrom
jakobbotsch:physical-promotion-covering-segment

Conversation

@jakobbotsch

@jakobbotschjakobbotsch commented Jun 27, 2023

Copy link
Copy Markdown
Member

The remainder may be separated by a bit of padding or other promoted fields but still fit into a primitive; in this case it is still beneficial to copy it all as a primitive, instead of falling back to a full block copy.

Example.

privateS_s;voidFoo(){Ss=new();s.A=10;s.D=20;s.F=30;// A, D, F gets promoted_s=s;}privatestructS{publicbyteA;publicbyteB;publicbyteC;publicbyteD;publicbyteE;publicbyteF;}
 Processing block operation [000018] that involves replacements
dst+003 <- V04 (V01.[003..004)) (last use)
dst+005 <- V05 (V01.[005..006)) (last use)
Block op remainder: [001..003) [004..005)
- => Remainder strategy: retain a full block op+ => Remainder strategy: int at +001
 ; V00 this [V00,T01] ( 3, 3 ) ref -> rcx this class-hnd single-def
;* V01 loc0 [V01 ] ( 0, 0 ) struct ( 8) zero-ref do-not-enreg[SF] ld-addr-op
;# V02 OutArgs [V02 ] ( 1, 1 ) struct ( 0) [rsp+00H] do-not-enreg[XS] addr-exposed "OutgoingArgSpace"
;* V03 tmp1 [V03 ] ( 0, 0 ) ubyte -> zero-ref "V01.[000..001)"
;* V04 tmp2 [V04 ] ( 0, 0 ) ubyte -> zero-ref "V01.[003..004)"
;* V05 tmp3 [V05 ] ( 0, 0 ) ubyte -> zero-ref "V01.[005..006)"
; V06 tmp4 [V06,T00] ( 5, 10 ) byref -> rcx single-def "Spilling address for field-by-field copy"
;
; Lcl frame size = 0
G_M52879_IG01: ;; offset=0000H
;; size=0 bbWeight=1 PerfScore 0.00
G_M52879_IG02: ;; offset=0000H
add rcx, 8
xor eax, eax
- mov dword ptr [rcx], eax- mov dword ptr [rcx+02H], eax+ mov dword ptr [rcx+01H], eax
mov byte ptr [rcx], 10
mov byte ptr [rcx+03H], 20
mov byte ptr [rcx+05H], 30
- ;; size=22 bbWeight=1 PerfScore 5.50-G_M52879_IG03: ;; offset=0016H+ ;; size=20 bbWeight=1 PerfScore 4.50+G_M52879_IG03: ;; offset=0014H
ret ;; size=1 bbWeight=1 PerfScore 1.00
-; Total bytes of code 23, prolog size 0, PerfScore 8.80, instruction count 8, allocated bytes for code 23 (MethodHash=1da13170) for method Program:Foo():this (FullOpts)+; Total bytes of code 21, prolog size 0, PerfScore 7.60, instruction count 7, allocated bytes for code 21 (MethodHash=1da13170) for method Program:Foo():this (FullOpts)
; ============================================================

We have to be careful, however, since the covering segment can now contain promoted fields. If this happens we need to make sure we write the promoted field after the remainder.

@ghostghost added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Jun 27, 2023
@ghost

Copy link
Copy Markdown

Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.

Issue Details

The remainder may be separated by a bit of padding but still fit into a primitive; in this case it is still beneficial to copy it all as a primitive, instead of falling back to a full block copy.

Example.
Before:

Processing block operation [000737] that involves replacements
dst+000<-V101 (V47.[000..008)) (last use)
dst+008<-V102 (V47.[008..016)) (last use)
dst+016<-V103 (V47.[016..024)) (last use)
dst+024<-V104 (V47.[024..028)) (last use)
dst+028<-V105 (V47.[028..029)) (last use)
Block op remainder: [032..033) [036..040)
=>Remainderstrategy: retain a full block op

After:

Processing block operation [000737] that involves replacements
dst+000<-V101 (V47.[000..008)) (last use)
dst+008<-V102 (V47.[008..016)) (last use)
dst+016<-V103 (V47.[016..024)) (last use)
dst+024<-V104 (V47.[024..028)) (last use)
dst+028<-V105 (V47.[028..029)) (last use)
Block op remainder: [032..033) [036..040)
=>Remainderstrategy: long at +032

This leads to ASM diffs like the following:

 xor edx, edx
mov qword ptr [rsp+148H], rdx
- ;; size=20 bbWeight=0.33 PerfScore 1.57-G_M6338_IG24: ; bbWeight=0.33, nogc, extend- vmovdqu ymm0, ymmword ptr [rsp+128H]- vmovdqu ymmword ptr [rsp+D8H], ymm0- mov rdx, qword ptr [rsp+148H]- mov qword ptr [rsp+F8H], rdx- ;; size=34 bbWeight=0.33 PerfScore 2.32-G_M6338_IG25: ; bbWeight=0.33, extend
mov gword ptr [rsp+D8H], r12
- xor rdx, rdx- ; gcrRegs +[rdx]+ ;; size=28 bbWeight=0.33 PerfScore 1.90+G_M6338_IG24: ; bbWeight=0.33, gcrefRegs=1028 {rbx rbp r12}, byrefRegs=4000 {r14}, byref
mov gword ptr [rsp+E0H], rdx
- ;; size=18 bbWeight=0.33 PerfScore 0.75-G_M6338_IG26: ; bbWeight=0.33, gcrefRegs=1028 {rbx rbp r12}, byrefRegs=4000 {r14}, byref- ; gcrRegs -[rdx]+ ;; size=8 bbWeight=0.33 PerfScore 0.33+G_M6338_IG25: ; bbWeight=0.33, gcrefRegs=1028 {rbx rbp r12}, byrefRegs=4000 {r14}, byref
mov gword ptr [rsp+E8H], rdx
mov dword ptr [rsp+F0H], r9d
mov byte ptr [rsp+F4H], 0
+ ;; size=24 bbWeight=0.33 PerfScore 0.99+G_M6338_IG26: ; bbWeight=0.33, gcrefRegs=1028 {rbx rbp r12}, byrefRegs=4000 {r14}, byref+ mov qword ptr [rsp+F8H], rdx

We have to be careful, however, since the covering segment can now contain promoted fields. If this happens we need to make sure we write the promoted field after the remainder.

Author:jakobbotsch
Assignees:-
Labels:

area-CodeGen-coreclr

Milestone:-

…l promotion
The remainder may be separated by a bit of padding but still fit into a
primitive; in this case it is still beneficial to copy it all as a
primitive, instead of falling back to a full block copy.
Example.
Before:
```
Processing block operation [000737] that involves replacements
dst+000 <- V101 (V47.[000..008)) (last use)
dst+008 <- V102 (V47.[008..016)) (last use)
dst+016 <- V103 (V47.[016..024)) (last use)
dst+024 <- V104 (V47.[024..028)) (last use)
dst+028 <- V105 (V47.[028..029)) (last use)
Block op remainder: [032..033) [036..040)
=> Remainder strategy: retain a full block op
```
After:
```
Processing block operation [000737] that involves replacements
dst+000 <- V101 (V47.[000..008)) (last use)
dst+008 <- V102 (V47.[008..016)) (last use)
dst+016 <- V103 (V47.[016..024)) (last use)
dst+024 <- V104 (V47.[024..028)) (last use)
dst+028 <- V105 (V47.[028..029)) (last use)
Block op remainder: [032..033) [036..040)
=> Remainder strategy: long at +032
```
This leads to ASM diffs like the following:
```diff
xor edx, edx
mov qword ptr [rsp+148H], rdx
- ;; size=20 bbWeight=0.33 PerfScore 1.57
-G_M6338_IG24: ; bbWeight=0.33, nogc, extend
- vmovdqu ymm0, ymmword ptr [rsp+128H]
- vmovdqu ymmword ptr [rsp+D8H], ymm0
- mov rdx, qword ptr [rsp+148H]
- mov qword ptr [rsp+F8H], rdx
- ;; size=34 bbWeight=0.33 PerfScore 2.32
-G_M6338_IG25: ; bbWeight=0.33, extend
mov gword ptr [rsp+D8H], r12
- xor rdx, rdx
- ; gcrRegs +[rdx]
+ ;; size=28 bbWeight=0.33 PerfScore 1.90
+G_M6338_IG24: ; bbWeight=0.33, gcrefRegs=1028 {rbx rbp r12}, byrefRegs=4000 {r14}, byref
mov gword ptr [rsp+E0H], rdx
- ;; size=18 bbWeight=0.33 PerfScore 0.75
-G_M6338_IG26: ; bbWeight=0.33, gcrefRegs=1028 {rbx rbp r12}, byrefRegs=4000 {r14}, byref
- ; gcrRegs -[rdx]
+ ;; size=8 bbWeight=0.33 PerfScore 0.33
+G_M6338_IG25: ; bbWeight=0.33, gcrefRegs=1028 {rbx rbp r12}, byrefRegs=4000 {r14}, byref
mov gword ptr [rsp+E8H], rdx
mov dword ptr [rsp+F0H], r9d
mov byte ptr [rsp+F4H], 0
+ ;; size=24 bbWeight=0.33 PerfScore 0.99
+G_M6338_IG26: ; bbWeight=0.33, gcrefRegs=1028 {rbx rbp r12}, byrefRegs=4000 {r14}, byref
+ mov qword ptr [rsp+F8H], rdx
```
We have to be careful, however, since the covering segment can now
contain promoted fields. If this happens we need to make sure we write
the promoted field _after_ the remainder.
Unfortunately doing this requires quite a bit of refactoring. I have
extracted all common code for handling creation of derived accesses of
the destination/source into a common class called LocationAccess.
@jakobbotsch
jakobbotschforce-pushed the physical-promotion-covering-segment branch from 4ec5010 to 0c9b1c5CompareJune 30, 2023 22:07
@jakobbotsch

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-coreclr superpmi-diffs

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@jakobbotsch

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-coreclr superpmi-diffs

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@jakobbotsch
jakobbotsch marked this pull request as ready for review July 3, 2023 08:16
@jakobbotsch

Copy link
Copy Markdown
MemberAuthor

cc @dotnet/jit-contrib PTAL @EgorBo

Diffs. This is not just about padding, but also when the remainder is overlapping with promoted fields. A C# example of the difference:

privateS_s;voidFoo(){Ss=new();s.A=10;s.D=20;s.F=30;// A, D, F gets promoted_s=s;}privatestructS{publicbyteA;publicbyteB;publicbyteC;publicbyteD;publicbyteE;publicbyteF;}
 Processing block operation [000018] that involves replacements
dst+003 <- V04 (V01.[003..004)) (last use)
dst+005 <- V05 (V01.[005..006)) (last use)
Block op remainder: [001..003) [004..005)
- => Remainder strategy: retain a full block op+ => Remainder strategy: int at +001
 ; V00 this [V00,T01] ( 3, 3 ) ref -> rcx this class-hnd single-def
;* V01 loc0 [V01 ] ( 0, 0 ) struct ( 8) zero-ref do-not-enreg[SF] ld-addr-op
;# V02 OutArgs [V02 ] ( 1, 1 ) struct ( 0) [rsp+00H] do-not-enreg[XS] addr-exposed "OutgoingArgSpace"
;* V03 tmp1 [V03 ] ( 0, 0 ) ubyte -> zero-ref "V01.[000..001)"
;* V04 tmp2 [V04 ] ( 0, 0 ) ubyte -> zero-ref "V01.[003..004)"
;* V05 tmp3 [V05 ] ( 0, 0 ) ubyte -> zero-ref "V01.[005..006)"
; V06 tmp4 [V06,T00] ( 5, 10 ) byref -> rcx single-def "Spilling address for field-by-field copy"
;
; Lcl frame size = 0
G_M52879_IG01: ;; offset=0000H
;; size=0 bbWeight=1 PerfScore 0.00
G_M52879_IG02: ;; offset=0000H
add rcx, 8
xor eax, eax
- mov dword ptr [rcx], eax- mov dword ptr [rcx+02H], eax+ mov dword ptr [rcx+01H], eax
mov byte ptr [rcx], 10
mov byte ptr [rcx+03H], 20
mov byte ptr [rcx+05H], 30
- ;; size=22 bbWeight=1 PerfScore 5.50-G_M52879_IG03: ;; offset=0016H+ ;; size=20 bbWeight=1 PerfScore 4.50+G_M52879_IG03: ;; offset=0014H
ret ;; size=1 bbWeight=1 PerfScore 1.00
-; Total bytes of code 23, prolog size 0, PerfScore 8.80, instruction count 8, allocated bytes for code 23 (MethodHash=1da13170) for method Program:Foo():this (FullOpts)+; Total bytes of code 21, prolog size 0, PerfScore 7.60, instruction count 7, allocated bytes for code 21 (MethodHash=1da13170) for method Program:Foo():this (FullOpts)
; ============================================================

@jakobbotsch
jakobbotsch requested a review from EgorBoJuly 3, 2023 08:36
@jakobbotschjakobbotsch changed the title JIT: Handle primitive-sized remainders overlapping padding in physical promotionJIT: Handle primitive-sized remainders overlapping padding/promotions in physical promotionJul 3, 2023

@EgorBoEgorBo left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are regressions like this

@@ -13,46 +13,48 @@
; V02 arg1 [V02,T02] ( 3, 3 ) ref -> r8 class-hnd single-def
; V03 arg2 [V03,T03] ( 3, 3 ) struct ( 8) r9 single-def
;# V04 OutArgs [V04 ] ( 1, 1 ) struct ( 0) [rsp+00H] do-not-enreg[XS] addr-exposed "OutgoingArgSpace"
-; V05 tmp1 [V05,T01] ( 3, 6 ) struct (16) [rsp+00H] do-not-enreg[SF] must-init ld-addr-op "NewObj constructor temp"+; V05 tmp1 [V05,T04] ( 2, 4 ) struct (16) [rsp+08H] do-not-enreg[SF] must-init ld-addr-op "NewObj constructor temp"
;* V06 tmp2 [V06 ] ( 0, 0 ) long -> zero-ref "spilling helperCall"
;* V07 tmp3 [V07 ] ( 0, 0 ) ref -> zero-ref single-def "V05.[000..008)"
+; V08 tmp4 [V08,T01] ( 3, 6 ) byref -> rsi single-def "Spilling address for field-by-field copy"
;
-; Lcl frame size = 16+; Lcl frame size = 24
G_M36974_IG01: ; bbWeight=1, gcrefRegs=0000 {}, byrefRegs=0000 {}, byref, nogc <-- Prolog IG
- push rdi
push rsi
push rbx
- sub rsp, 16+ sub rsp, 24
xor eax, eax
- mov qword ptr [rsp], rax+ mov qword ptr [rsp+08H], rax
mov rbx, rcx
; byrRegs +[rbx]
- ;; size=16 bbWeight=1 PerfScore 4.75+ ;; size=16 bbWeight=1 PerfScore 3.75
G_M36974_IG02: ; bbWeight=1, gcrefRegs=0100 {r8}, byrefRegs=0008 {rbx}, byref
; gcrRegs +[r8]
- mov qword ptr [rsp+08H], r9- mov gword ptr [rsp], r8- lea rdi, bword ptr [rbx+08H]- ; byrRegs +[rdi]- lea rsi, bword ptr [rsp]+ mov qword ptr [rsp+10H], r9+ lea rsi, bword ptr [rbx+08H]
; byrRegs +[rsi]
- call CORINFO_HELP_ASSIGN_BYREF- ; gcrRegs -[r8]- movsq + mov rcx, rsi+ ; byrRegs +[rcx]+ mov rdx, r8+ ; gcrRegs +[rdx]+ call CORINFO_HELP_CHECKED_ASSIGN_REF+ ; gcrRegs -[rdx r8]+ ; byrRegs -[rcx]+ mov rax, qword ptr [rsp+10H]+ mov qword ptr [rsi+08H], rax
xor rax, rax
; gcrRegs +[rax]
mov gword ptr [rbx], rax
- ;; size=29 bbWeight=1 PerfScore 6.25+ ;; size=34 bbWeight=1 PerfScore 6.25
G_M36974_IG03: ; bbWeight=1, epilog, nogc, extend
- add rsp, 16+ add rsp, 24
pop rbx
pop rsi
- pop rdi
ret - ;; size=8 bbWeight=1 PerfScore 2.75+ ;; size=7 bbWeight=1 PerfScore 2.25-; Total bytes of code 53, prolog size 13, PerfScore 19.05, instruction count 20, allocated bytes for code 53 (MethodHash=ebd16f91) for method System.Linq.Parallel.HashLookupValueList`2[System.__Canon,System.Nullable`1[int]]:.ctor(System.__Canon,System.Nullable`1[int]):this (FullOpts)+; Total bytes of code 57, prolog size 13, PerfScore 17.95, instruction count 19, allocated bytes for code 57 (MethodHash=ebd16f91) for method System.Linq.Parallel.HashLookupValueList`2[System.__Canon,System.Nullable`1[int]]:.ctor(System.__Canon,System.Nullable`1[int]):this (FullOpts)
; ============================================================

expected?

@jakobbotsch

Copy link
Copy Markdown
MemberAuthor

Generally we do expect to replace CORINFO_HELP_ASSIGN_BYREF with CORINFO_HELP_CHECKED_ASSIGN_REF in the cases where we switch the remainder from a block copy (where the block has GC pointers) to a TYP_REF primitive copy.
That has the side effect that we no longer get the destination/source increments "for free" as part of that helper, so I suppose that can sometimes result in bigger code. But notice that in this case we get rid of a movsq which is a very expensive instruction, so perf wise I would expect this to be beneficial.

Now, this case is a bit peculiar because the fact that we no longer uses the full block copy for the remainder turns off an unrelated optimization:

@@ -1,20 +1,28 @@
STMT00001 ( 0x000[E-] ... ??? )
[000027] nA-XG------ ▌ STORE_BLK struct<System.Linq.Parallel.Pair`2, 16> (copy)
[000025] ---X------- ├──▌ FIELD_ADDR byref <unknown class>:<unknown field>
[000026] ----------- │ └──▌ LCL_VAR byref V00 this [000024] ----------- └──▌ LCL_VAR struct<System.Linq.Parallel.Pair`2, 16> V05 tmp1 (last use)
Processing block operation [000027] that involves replacements
dst+000 <- V07 (V05.[000..008)) (last use)
Block op remainder: [008..009) [012..016)
- => Remainder strategy: retain a full block op- Will write back V07 (V05.[000..008)) to avoid an additional write barrier- Skipping dst+000 <- V07 (V05.[000..008)); it is up-to-date in its struct local and will be handled as part of the remainder+ => Remainder strategy: long at +008++lvaGrabTemp returning 8 (V08 tmp4) called for Spilling address for field-by-field copy.++Local V05 should not be enregistered because: was accessed as a local field
New statement:
STMT00001 ( 0x000[E-] ... ??? )
- [000040] -A-XG------ ▌ COMMA void - [000039] UA--------- ├──▌ STORE_LCL_FLD ref V05 tmp1 [+0]- [000038] ----------- │ └──▌ LCL_VAR ref V07 tmp3 - [000027] nA-XG------ └──▌ STORE_BLK struct<System.Linq.Parallel.Pair`2, 16> (copy)- [000025] ---X------- ├──▌ FIELD_ADDR byref <unknown class>:<unknown field>- [000026] ----------- │ └──▌ LCL_VAR byref V00 this - [000024] ----------- └──▌ LCL_VAR struct<System.Linq.Parallel.Pair`2, 16> V05 tmp1 + [000048] -A-XG------ ▌ COMMA void + [000038] DA-X------- ├──▌ STORE_LCL_VAR byref V08 tmp4 + [000025] ---X------- │ └──▌ FIELD_ADDR byref <unknown class>:<unknown field>+ [000026] ----------- │ └──▌ LCL_VAR byref V00 this + [000047] -A--G------ └──▌ COMMA void + [000042] nA--G------ ├──▌ STOREIND ref + [000041] ----------- │ ├──▌ LCL_VAR byref V08 tmp4 + [000040] ----------- │ └──▌ LCL_VAR ref V07 tmp3 (last use)+ [000046] nA--G------ └──▌ STOREIND long + [000045] ----------- ├──▌ ADD byref + [000039] ----------- │ ├──▌ LCL_VAR byref V08 tmp4 + [000044] ----------- │ └──▌ CNS_INT long 8+ [000043] ----------- └──▌ LCL_FLD long V05 tmp1 [+8]

Specifically, in the base we notice that we have a promoted TYP_REF field that is being written to heap, which will involve a write barrier, in addition to the write barrier imposed for the block copy. So we write it back to stack first, to avoid this extra write barrier.
This has the side effect that we don't need to write it back after the write barrier call. It also means we don't need to save the target address to a local because we end up only using the address once.

In the diff we don't do this optimization anymore and instead just copy the promoted TYP_REF directly to the heap, incurring the write barrier. It doesn't really matter because overall we still only end up needing one write barrier.

We should be able to avoid the spilled local address in this case by teaching physical promotion to peel FIELD_ADDR nodes, though it comes with a bit of complexity to get the right null checking behavior. It would probably have quite decent diffs, though. Will open an issue for that.

@jakobbotsch
jakobbotsch merged commit 210a7a5 into dotnet:mainJul 4, 2023
@jakobbotsch
jakobbotsch deleted the physical-promotion-covering-segment branch July 4, 2023 16:02
@ghostghost locked as resolved and limited conversation to collaborators Aug 3, 2023
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@jakobbotsch@EgorBo
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' JIT: Handle primitive-sized remainders overlapping padding/promotions in physical promotion by jakobbotsch · Pull Request #88109 · dotnet/runtime · GitHub
Skip to content

JIT: Handle primitive-sized remainders overlapping padding/promotions in physical promotion - #88109

Merged
jakobbotsch merged 3 commits into
dotnet:mainfrom
jakobbotsch:physical-promotion-covering-segment
Jul 4, 2023
Merged

JIT: Handle primitive-sized remainders overlapping padding/promotions in physical promotion#88109
jakobbotsch merged 3 commits into
dotnet:mainfrom
jakobbotsch:physical-promotion-covering-segment

Conversation

@jakobbotsch

@jakobbotschjakobbotsch commented Jun 27, 2023

Copy link
Copy Markdown
Member

The remainder may be separated by a bit of padding or other promoted fields but still fit into a primitive; in this case it is still beneficial to copy it all as a primitive, instead of falling back to a full block copy.

Example.

privateS_s;voidFoo(){Ss=new();s.A=10;s.D=20;s.F=30;// A, D, F gets promoted_s=s;}privatestructS{publicbyteA;publicbyteB;publicbyteC;publicbyteD;publicbyteE;publicbyteF;}
 Processing block operation [000018] that involves replacements
dst+003 <- V04 (V01.[003..004)) (last use)
dst+005 <- V05 (V01.[005..006)) (last use)
Block op remainder: [001..003) [004..005)
- => Remainder strategy: retain a full block op+ => Remainder strategy: int at +001
 ; V00 this [V00,T01] ( 3, 3 ) ref -> rcx this class-hnd single-def
;* V01 loc0 [V01 ] ( 0, 0 ) struct ( 8) zero-ref do-not-enreg[SF] ld-addr-op
;# V02 OutArgs [V02 ] ( 1, 1 ) struct ( 0) [rsp+00H] do-not-enreg[XS] addr-exposed "OutgoingArgSpace"
;* V03 tmp1 [V03 ] ( 0, 0 ) ubyte -> zero-ref "V01.[000..001)"
;* V04 tmp2 [V04 ] ( 0, 0 ) ubyte -> zero-ref "V01.[003..004)"
;* V05 tmp3 [V05 ] ( 0, 0 ) ubyte -> zero-ref "V01.[005..006)"
; V06 tmp4 [V06,T00] ( 5, 10 ) byref -> rcx single-def "Spilling address for field-by-field copy"
;
; Lcl frame size = 0
G_M52879_IG01: ;; offset=0000H
;; size=0 bbWeight=1 PerfScore 0.00
G_M52879_IG02: ;; offset=0000H
add rcx, 8
xor eax, eax
- mov dword ptr [rcx], eax- mov dword ptr [rcx+02H], eax+ mov dword ptr [rcx+01H], eax
mov byte ptr [rcx], 10
mov byte ptr [rcx+03H], 20
mov byte ptr [rcx+05H], 30
- ;; size=22 bbWeight=1 PerfScore 5.50-G_M52879_IG03: ;; offset=0016H+ ;; size=20 bbWeight=1 PerfScore 4.50+G_M52879_IG03: ;; offset=0014H
ret ;; size=1 bbWeight=1 PerfScore 1.00
-; Total bytes of code 23, prolog size 0, PerfScore 8.80, instruction count 8, allocated bytes for code 23 (MethodHash=1da13170) for method Program:Foo():this (FullOpts)+; Total bytes of code 21, prolog size 0, PerfScore 7.60, instruction count 7, allocated bytes for code 21 (MethodHash=1da13170) for method Program:Foo():this (FullOpts)
; ============================================================

We have to be careful, however, since the covering segment can now contain promoted fields. If this happens we need to make sure we write the promoted field after the remainder.

@ghostghost added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Jun 27, 2023
@ghost

Copy link
Copy Markdown

Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.

Issue Details

The remainder may be separated by a bit of padding but still fit into a primitive; in this case it is still beneficial to copy it all as a primitive, instead of falling back to a full block copy.

Example.
Before:

Processing block operation [000737] that involves replacements
dst+000<-V101 (V47.[000..008)) (last use)
dst+008<-V102 (V47.[008..016)) (last use)
dst+016<-V103 (V47.[016..024)) (last use)
dst+024<-V104 (V47.[024..028)) (last use)
dst+028<-V105 (V47.[028..029)) (last use)
Block op remainder: [032..033) [036..040)
=>Remainderstrategy: retain a full block op

After:

Processing block operation [000737] that involves replacements
dst+000<-V101 (V47.[000..008)) (last use)
dst+008<-V102 (V47.[008..016)) (last use)
dst+016<-V103 (V47.[016..024)) (last use)
dst+024<-V104 (V47.[024..028)) (last use)
dst+028<-V105 (V47.[028..029)) (last use)
Block op remainder: [032..033) [036..040)
=>Remainderstrategy: long at +032

This leads to ASM diffs like the following:

 xor edx, edx
mov qword ptr [rsp+148H], rdx
- ;; size=20 bbWeight=0.33 PerfScore 1.57-G_M6338_IG24: ; bbWeight=0.33, nogc, extend- vmovdqu ymm0, ymmword ptr [rsp+128H]- vmovdqu ymmword ptr [rsp+D8H], ymm0- mov rdx, qword ptr [rsp+148H]- mov qword ptr [rsp+F8H], rdx- ;; size=34 bbWeight=0.33 PerfScore 2.32-G_M6338_IG25: ; bbWeight=0.33, extend
mov gword ptr [rsp+D8H], r12
- xor rdx, rdx- ; gcrRegs +[rdx]+ ;; size=28 bbWeight=0.33 PerfScore 1.90+G_M6338_IG24: ; bbWeight=0.33, gcrefRegs=1028 {rbx rbp r12}, byrefRegs=4000 {r14}, byref
mov gword ptr [rsp+E0H], rdx
- ;; size=18 bbWeight=0.33 PerfScore 0.75-G_M6338_IG26: ; bbWeight=0.33, gcrefRegs=1028 {rbx rbp r12}, byrefRegs=4000 {r14}, byref- ; gcrRegs -[rdx]+ ;; size=8 bbWeight=0.33 PerfScore 0.33+G_M6338_IG25: ; bbWeight=0.33, gcrefRegs=1028 {rbx rbp r12}, byrefRegs=4000 {r14}, byref
mov gword ptr [rsp+E8H], rdx
mov dword ptr [rsp+F0H], r9d
mov byte ptr [rsp+F4H], 0
+ ;; size=24 bbWeight=0.33 PerfScore 0.99+G_M6338_IG26: ; bbWeight=0.33, gcrefRegs=1028 {rbx rbp r12}, byrefRegs=4000 {r14}, byref+ mov qword ptr [rsp+F8H], rdx

We have to be careful, however, since the covering segment can now contain promoted fields. If this happens we need to make sure we write the promoted field after the remainder.

Author:jakobbotsch
Assignees:-
Labels:

area-CodeGen-coreclr

Milestone:-

…l promotion
The remainder may be separated by a bit of padding but still fit into a
primitive; in this case it is still beneficial to copy it all as a
primitive, instead of falling back to a full block copy.
Example.
Before:
```
Processing block operation [000737] that involves replacements
dst+000 <- V101 (V47.[000..008)) (last use)
dst+008 <- V102 (V47.[008..016)) (last use)
dst+016 <- V103 (V47.[016..024)) (last use)
dst+024 <- V104 (V47.[024..028)) (last use)
dst+028 <- V105 (V47.[028..029)) (last use)
Block op remainder: [032..033) [036..040)
=> Remainder strategy: retain a full block op
```
After:
```
Processing block operation [000737] that involves replacements
dst+000 <- V101 (V47.[000..008)) (last use)
dst+008 <- V102 (V47.[008..016)) (last use)
dst+016 <- V103 (V47.[016..024)) (last use)
dst+024 <- V104 (V47.[024..028)) (last use)
dst+028 <- V105 (V47.[028..029)) (last use)
Block op remainder: [032..033) [036..040)
=> Remainder strategy: long at +032
```
This leads to ASM diffs like the following:
```diff
xor edx, edx
mov qword ptr [rsp+148H], rdx
- ;; size=20 bbWeight=0.33 PerfScore 1.57
-G_M6338_IG24: ; bbWeight=0.33, nogc, extend
- vmovdqu ymm0, ymmword ptr [rsp+128H]
- vmovdqu ymmword ptr [rsp+D8H], ymm0
- mov rdx, qword ptr [rsp+148H]
- mov qword ptr [rsp+F8H], rdx
- ;; size=34 bbWeight=0.33 PerfScore 2.32
-G_M6338_IG25: ; bbWeight=0.33, extend
mov gword ptr [rsp+D8H], r12
- xor rdx, rdx
- ; gcrRegs +[rdx]
+ ;; size=28 bbWeight=0.33 PerfScore 1.90
+G_M6338_IG24: ; bbWeight=0.33, gcrefRegs=1028 {rbx rbp r12}, byrefRegs=4000 {r14}, byref
mov gword ptr [rsp+E0H], rdx
- ;; size=18 bbWeight=0.33 PerfScore 0.75
-G_M6338_IG26: ; bbWeight=0.33, gcrefRegs=1028 {rbx rbp r12}, byrefRegs=4000 {r14}, byref
- ; gcrRegs -[rdx]
+ ;; size=8 bbWeight=0.33 PerfScore 0.33
+G_M6338_IG25: ; bbWeight=0.33, gcrefRegs=1028 {rbx rbp r12}, byrefRegs=4000 {r14}, byref
mov gword ptr [rsp+E8H], rdx
mov dword ptr [rsp+F0H], r9d
mov byte ptr [rsp+F4H], 0
+ ;; size=24 bbWeight=0.33 PerfScore 0.99
+G_M6338_IG26: ; bbWeight=0.33, gcrefRegs=1028 {rbx rbp r12}, byrefRegs=4000 {r14}, byref
+ mov qword ptr [rsp+F8H], rdx
```
We have to be careful, however, since the covering segment can now
contain promoted fields. If this happens we need to make sure we write
the promoted field _after_ the remainder.
Unfortunately doing this requires quite a bit of refactoring. I have
extracted all common code for handling creation of derived accesses of
the destination/source into a common class called LocationAccess.
@jakobbotsch
jakobbotschforce-pushed the physical-promotion-covering-segment branch from 4ec5010 to 0c9b1c5CompareJune 30, 2023 22:07
@jakobbotsch

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-coreclr superpmi-diffs

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@jakobbotsch

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-coreclr superpmi-diffs

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@jakobbotsch
jakobbotsch marked this pull request as ready for review July 3, 2023 08:16
@jakobbotsch

Copy link
Copy Markdown
MemberAuthor

cc @dotnet/jit-contrib PTAL @EgorBo

Diffs. This is not just about padding, but also when the remainder is overlapping with promoted fields. A C# example of the difference:

privateS_s;voidFoo(){Ss=new();s.A=10;s.D=20;s.F=30;// A, D, F gets promoted_s=s;}privatestructS{publicbyteA;publicbyteB;publicbyteC;publicbyteD;publicbyteE;publicbyteF;}
 Processing block operation [000018] that involves replacements
dst+003 <- V04 (V01.[003..004)) (last use)
dst+005 <- V05 (V01.[005..006)) (last use)
Block op remainder: [001..003) [004..005)
- => Remainder strategy: retain a full block op+ => Remainder strategy: int at +001
 ; V00 this [V00,T01] ( 3, 3 ) ref -> rcx this class-hnd single-def
;* V01 loc0 [V01 ] ( 0, 0 ) struct ( 8) zero-ref do-not-enreg[SF] ld-addr-op
;# V02 OutArgs [V02 ] ( 1, 1 ) struct ( 0) [rsp+00H] do-not-enreg[XS] addr-exposed "OutgoingArgSpace"
;* V03 tmp1 [V03 ] ( 0, 0 ) ubyte -> zero-ref "V01.[000..001)"
;* V04 tmp2 [V04 ] ( 0, 0 ) ubyte -> zero-ref "V01.[003..004)"
;* V05 tmp3 [V05 ] ( 0, 0 ) ubyte -> zero-ref "V01.[005..006)"
; V06 tmp4 [V06,T00] ( 5, 10 ) byref -> rcx single-def "Spilling address for field-by-field copy"
;
; Lcl frame size = 0
G_M52879_IG01: ;; offset=0000H
;; size=0 bbWeight=1 PerfScore 0.00
G_M52879_IG02: ;; offset=0000H
add rcx, 8
xor eax, eax
- mov dword ptr [rcx], eax- mov dword ptr [rcx+02H], eax+ mov dword ptr [rcx+01H], eax
mov byte ptr [rcx], 10
mov byte ptr [rcx+03H], 20
mov byte ptr [rcx+05H], 30
- ;; size=22 bbWeight=1 PerfScore 5.50-G_M52879_IG03: ;; offset=0016H+ ;; size=20 bbWeight=1 PerfScore 4.50+G_M52879_IG03: ;; offset=0014H
ret ;; size=1 bbWeight=1 PerfScore 1.00
-; Total bytes of code 23, prolog size 0, PerfScore 8.80, instruction count 8, allocated bytes for code 23 (MethodHash=1da13170) for method Program:Foo():this (FullOpts)+; Total bytes of code 21, prolog size 0, PerfScore 7.60, instruction count 7, allocated bytes for code 21 (MethodHash=1da13170) for method Program:Foo():this (FullOpts)
; ============================================================

@jakobbotsch
jakobbotsch requested a review from EgorBoJuly 3, 2023 08:36
@jakobbotschjakobbotsch changed the title JIT: Handle primitive-sized remainders overlapping padding in physical promotionJIT: Handle primitive-sized remainders overlapping padding/promotions in physical promotionJul 3, 2023

@EgorBoEgorBo left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are regressions like this

@@ -13,46 +13,48 @@
; V02 arg1 [V02,T02] ( 3, 3 ) ref -> r8 class-hnd single-def
; V03 arg2 [V03,T03] ( 3, 3 ) struct ( 8) r9 single-def
;# V04 OutArgs [V04 ] ( 1, 1 ) struct ( 0) [rsp+00H] do-not-enreg[XS] addr-exposed "OutgoingArgSpace"
-; V05 tmp1 [V05,T01] ( 3, 6 ) struct (16) [rsp+00H] do-not-enreg[SF] must-init ld-addr-op "NewObj constructor temp"+; V05 tmp1 [V05,T04] ( 2, 4 ) struct (16) [rsp+08H] do-not-enreg[SF] must-init ld-addr-op "NewObj constructor temp"
;* V06 tmp2 [V06 ] ( 0, 0 ) long -> zero-ref "spilling helperCall"
;* V07 tmp3 [V07 ] ( 0, 0 ) ref -> zero-ref single-def "V05.[000..008)"
+; V08 tmp4 [V08,T01] ( 3, 6 ) byref -> rsi single-def "Spilling address for field-by-field copy"
;
-; Lcl frame size = 16+; Lcl frame size = 24
G_M36974_IG01: ; bbWeight=1, gcrefRegs=0000 {}, byrefRegs=0000 {}, byref, nogc <-- Prolog IG
- push rdi
push rsi
push rbx
- sub rsp, 16+ sub rsp, 24
xor eax, eax
- mov qword ptr [rsp], rax+ mov qword ptr [rsp+08H], rax
mov rbx, rcx
; byrRegs +[rbx]
- ;; size=16 bbWeight=1 PerfScore 4.75+ ;; size=16 bbWeight=1 PerfScore 3.75
G_M36974_IG02: ; bbWeight=1, gcrefRegs=0100 {r8}, byrefRegs=0008 {rbx}, byref
; gcrRegs +[r8]
- mov qword ptr [rsp+08H], r9- mov gword ptr [rsp], r8- lea rdi, bword ptr [rbx+08H]- ; byrRegs +[rdi]- lea rsi, bword ptr [rsp]+ mov qword ptr [rsp+10H], r9+ lea rsi, bword ptr [rbx+08H]
; byrRegs +[rsi]
- call CORINFO_HELP_ASSIGN_BYREF- ; gcrRegs -[r8]- movsq + mov rcx, rsi+ ; byrRegs +[rcx]+ mov rdx, r8+ ; gcrRegs +[rdx]+ call CORINFO_HELP_CHECKED_ASSIGN_REF+ ; gcrRegs -[rdx r8]+ ; byrRegs -[rcx]+ mov rax, qword ptr [rsp+10H]+ mov qword ptr [rsi+08H], rax
xor rax, rax
; gcrRegs +[rax]
mov gword ptr [rbx], rax
- ;; size=29 bbWeight=1 PerfScore 6.25+ ;; size=34 bbWeight=1 PerfScore 6.25
G_M36974_IG03: ; bbWeight=1, epilog, nogc, extend
- add rsp, 16+ add rsp, 24
pop rbx
pop rsi
- pop rdi
ret - ;; size=8 bbWeight=1 PerfScore 2.75+ ;; size=7 bbWeight=1 PerfScore 2.25-; Total bytes of code 53, prolog size 13, PerfScore 19.05, instruction count 20, allocated bytes for code 53 (MethodHash=ebd16f91) for method System.Linq.Parallel.HashLookupValueList`2[System.__Canon,System.Nullable`1[int]]:.ctor(System.__Canon,System.Nullable`1[int]):this (FullOpts)+; Total bytes of code 57, prolog size 13, PerfScore 17.95, instruction count 19, allocated bytes for code 57 (MethodHash=ebd16f91) for method System.Linq.Parallel.HashLookupValueList`2[System.__Canon,System.Nullable`1[int]]:.ctor(System.__Canon,System.Nullable`1[int]):this (FullOpts)
; ============================================================

expected?

@jakobbotsch

Copy link
Copy Markdown
MemberAuthor

Generally we do expect to replace CORINFO_HELP_ASSIGN_BYREF with CORINFO_HELP_CHECKED_ASSIGN_REF in the cases where we switch the remainder from a block copy (where the block has GC pointers) to a TYP_REF primitive copy.
That has the side effect that we no longer get the destination/source increments "for free" as part of that helper, so I suppose that can sometimes result in bigger code. But notice that in this case we get rid of a movsq which is a very expensive instruction, so perf wise I would expect this to be beneficial.

Now, this case is a bit peculiar because the fact that we no longer uses the full block copy for the remainder turns off an unrelated optimization:

@@ -1,20 +1,28 @@
STMT00001 ( 0x000[E-] ... ??? )
[000027] nA-XG------ ▌ STORE_BLK struct<System.Linq.Parallel.Pair`2, 16> (copy)
[000025] ---X------- ├──▌ FIELD_ADDR byref <unknown class>:<unknown field>
[000026] ----------- │ └──▌ LCL_VAR byref V00 this [000024] ----------- └──▌ LCL_VAR struct<System.Linq.Parallel.Pair`2, 16> V05 tmp1 (last use)
Processing block operation [000027] that involves replacements
dst+000 <- V07 (V05.[000..008)) (last use)
Block op remainder: [008..009) [012..016)
- => Remainder strategy: retain a full block op- Will write back V07 (V05.[000..008)) to avoid an additional write barrier- Skipping dst+000 <- V07 (V05.[000..008)); it is up-to-date in its struct local and will be handled as part of the remainder+ => Remainder strategy: long at +008++lvaGrabTemp returning 8 (V08 tmp4) called for Spilling address for field-by-field copy.++Local V05 should not be enregistered because: was accessed as a local field
New statement:
STMT00001 ( 0x000[E-] ... ??? )
- [000040] -A-XG------ ▌ COMMA void - [000039] UA--------- ├──▌ STORE_LCL_FLD ref V05 tmp1 [+0]- [000038] ----------- │ └──▌ LCL_VAR ref V07 tmp3 - [000027] nA-XG------ └──▌ STORE_BLK struct<System.Linq.Parallel.Pair`2, 16> (copy)- [000025] ---X------- ├──▌ FIELD_ADDR byref <unknown class>:<unknown field>- [000026] ----------- │ └──▌ LCL_VAR byref V00 this - [000024] ----------- └──▌ LCL_VAR struct<System.Linq.Parallel.Pair`2, 16> V05 tmp1 + [000048] -A-XG------ ▌ COMMA void + [000038] DA-X------- ├──▌ STORE_LCL_VAR byref V08 tmp4 + [000025] ---X------- │ └──▌ FIELD_ADDR byref <unknown class>:<unknown field>+ [000026] ----------- │ └──▌ LCL_VAR byref V00 this + [000047] -A--G------ └──▌ COMMA void + [000042] nA--G------ ├──▌ STOREIND ref + [000041] ----------- │ ├──▌ LCL_VAR byref V08 tmp4 + [000040] ----------- │ └──▌ LCL_VAR ref V07 tmp3 (last use)+ [000046] nA--G------ └──▌ STOREIND long + [000045] ----------- ├──▌ ADD byref + [000039] ----------- │ ├──▌ LCL_VAR byref V08 tmp4 + [000044] ----------- │ └──▌ CNS_INT long 8+ [000043] ----------- └──▌ LCL_FLD long V05 tmp1 [+8]

Specifically, in the base we notice that we have a promoted TYP_REF field that is being written to heap, which will involve a write barrier, in addition to the write barrier imposed for the block copy. So we write it back to stack first, to avoid this extra write barrier.
This has the side effect that we don't need to write it back after the write barrier call. It also means we don't need to save the target address to a local because we end up only using the address once.

In the diff we don't do this optimization anymore and instead just copy the promoted TYP_REF directly to the heap, incurring the write barrier. It doesn't really matter because overall we still only end up needing one write barrier.

We should be able to avoid the spilled local address in this case by teaching physical promotion to peel FIELD_ADDR nodes, though it comes with a bit of complexity to get the right null checking behavior. It would probably have quite decent diffs, though. Will open an issue for that.

@jakobbotsch
jakobbotsch merged commit 210a7a5 into dotnet:mainJul 4, 2023
@jakobbotsch
jakobbotsch deleted the physical-promotion-covering-segment branch July 4, 2023 16:02
@ghostghost locked as resolved and limited conversation to collaborators Aug 3, 2023
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@jakobbotsch@EgorBo
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' JIT: Handle primitive-sized remainders overlapping padding/promotions in physical promotion by jakobbotsch · Pull Request #88109 · dotnet/runtime · GitHub
Skip to content

JIT: Handle primitive-sized remainders overlapping padding/promotions in physical promotion - #88109

Merged
jakobbotsch merged 3 commits into
dotnet:mainfrom
jakobbotsch:physical-promotion-covering-segment
Jul 4, 2023
Merged

JIT: Handle primitive-sized remainders overlapping padding/promotions in physical promotion#88109
jakobbotsch merged 3 commits into
dotnet:mainfrom
jakobbotsch:physical-promotion-covering-segment

Conversation

@jakobbotsch

@jakobbotschjakobbotsch commented Jun 27, 2023

Copy link
Copy Markdown
Member

The remainder may be separated by a bit of padding or other promoted fields but still fit into a primitive; in this case it is still beneficial to copy it all as a primitive, instead of falling back to a full block copy.

Example.

privateS_s;voidFoo(){Ss=new();s.A=10;s.D=20;s.F=30;// A, D, F gets promoted_s=s;}privatestructS{publicbyteA;publicbyteB;publicbyteC;publicbyteD;publicbyteE;publicbyteF;}
 Processing block operation [000018] that involves replacements
dst+003 <- V04 (V01.[003..004)) (last use)
dst+005 <- V05 (V01.[005..006)) (last use)
Block op remainder: [001..003) [004..005)
- => Remainder strategy: retain a full block op+ => Remainder strategy: int at +001
 ; V00 this [V00,T01] ( 3, 3 ) ref -> rcx this class-hnd single-def
;* V01 loc0 [V01 ] ( 0, 0 ) struct ( 8) zero-ref do-not-enreg[SF] ld-addr-op
;# V02 OutArgs [V02 ] ( 1, 1 ) struct ( 0) [rsp+00H] do-not-enreg[XS] addr-exposed "OutgoingArgSpace"
;* V03 tmp1 [V03 ] ( 0, 0 ) ubyte -> zero-ref "V01.[000..001)"
;* V04 tmp2 [V04 ] ( 0, 0 ) ubyte -> zero-ref "V01.[003..004)"
;* V05 tmp3 [V05 ] ( 0, 0 ) ubyte -> zero-ref "V01.[005..006)"
; V06 tmp4 [V06,T00] ( 5, 10 ) byref -> rcx single-def "Spilling address for field-by-field copy"
;
; Lcl frame size = 0
G_M52879_IG01: ;; offset=0000H
;; size=0 bbWeight=1 PerfScore 0.00
G_M52879_IG02: ;; offset=0000H
add rcx, 8
xor eax, eax
- mov dword ptr [rcx], eax- mov dword ptr [rcx+02H], eax+ mov dword ptr [rcx+01H], eax
mov byte ptr [rcx], 10
mov byte ptr [rcx+03H], 20
mov byte ptr [rcx+05H], 30
- ;; size=22 bbWeight=1 PerfScore 5.50-G_M52879_IG03: ;; offset=0016H+ ;; size=20 bbWeight=1 PerfScore 4.50+G_M52879_IG03: ;; offset=0014H
ret ;; size=1 bbWeight=1 PerfScore 1.00
-; Total bytes of code 23, prolog size 0, PerfScore 8.80, instruction count 8, allocated bytes for code 23 (MethodHash=1da13170) for method Program:Foo():this (FullOpts)+; Total bytes of code 21, prolog size 0, PerfScore 7.60, instruction count 7, allocated bytes for code 21 (MethodHash=1da13170) for method Program:Foo():this (FullOpts)
; ============================================================

We have to be careful, however, since the covering segment can now contain promoted fields. If this happens we need to make sure we write the promoted field after the remainder.

@ghostghost added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Jun 27, 2023
@ghost

Copy link
Copy Markdown

Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.

Issue Details

The remainder may be separated by a bit of padding but still fit into a primitive; in this case it is still beneficial to copy it all as a primitive, instead of falling back to a full block copy.

Example.
Before:

Processing block operation [000737] that involves replacements
dst+000<-V101 (V47.[000..008)) (last use)
dst+008<-V102 (V47.[008..016)) (last use)
dst+016<-V103 (V47.[016..024)) (last use)
dst+024<-V104 (V47.[024..028)) (last use)
dst+028<-V105 (V47.[028..029)) (last use)
Block op remainder: [032..033) [036..040)
=>Remainderstrategy: retain a full block op

After:

Processing block operation [000737] that involves replacements
dst+000<-V101 (V47.[000..008)) (last use)
dst+008<-V102 (V47.[008..016)) (last use)
dst+016<-V103 (V47.[016..024)) (last use)
dst+024<-V104 (V47.[024..028)) (last use)
dst+028<-V105 (V47.[028..029)) (last use)
Block op remainder: [032..033) [036..040)
=>Remainderstrategy: long at +032

This leads to ASM diffs like the following:

 xor edx, edx
mov qword ptr [rsp+148H], rdx
- ;; size=20 bbWeight=0.33 PerfScore 1.57-G_M6338_IG24: ; bbWeight=0.33, nogc, extend- vmovdqu ymm0, ymmword ptr [rsp+128H]- vmovdqu ymmword ptr [rsp+D8H], ymm0- mov rdx, qword ptr [rsp+148H]- mov qword ptr [rsp+F8H], rdx- ;; size=34 bbWeight=0.33 PerfScore 2.32-G_M6338_IG25: ; bbWeight=0.33, extend
mov gword ptr [rsp+D8H], r12
- xor rdx, rdx- ; gcrRegs +[rdx]+ ;; size=28 bbWeight=0.33 PerfScore 1.90+G_M6338_IG24: ; bbWeight=0.33, gcrefRegs=1028 {rbx rbp r12}, byrefRegs=4000 {r14}, byref
mov gword ptr [rsp+E0H], rdx
- ;; size=18 bbWeight=0.33 PerfScore 0.75-G_M6338_IG26: ; bbWeight=0.33, gcrefRegs=1028 {rbx rbp r12}, byrefRegs=4000 {r14}, byref- ; gcrRegs -[rdx]+ ;; size=8 bbWeight=0.33 PerfScore 0.33+G_M6338_IG25: ; bbWeight=0.33, gcrefRegs=1028 {rbx rbp r12}, byrefRegs=4000 {r14}, byref
mov gword ptr [rsp+E8H], rdx
mov dword ptr [rsp+F0H], r9d
mov byte ptr [rsp+F4H], 0
+ ;; size=24 bbWeight=0.33 PerfScore 0.99+G_M6338_IG26: ; bbWeight=0.33, gcrefRegs=1028 {rbx rbp r12}, byrefRegs=4000 {r14}, byref+ mov qword ptr [rsp+F8H], rdx

We have to be careful, however, since the covering segment can now contain promoted fields. If this happens we need to make sure we write the promoted field after the remainder.

Author:jakobbotsch
Assignees:-
Labels:

area-CodeGen-coreclr

Milestone:-

…l promotion
The remainder may be separated by a bit of padding but still fit into a
primitive; in this case it is still beneficial to copy it all as a
primitive, instead of falling back to a full block copy.
Example.
Before:
```
Processing block operation [000737] that involves replacements
dst+000 <- V101 (V47.[000..008)) (last use)
dst+008 <- V102 (V47.[008..016)) (last use)
dst+016 <- V103 (V47.[016..024)) (last use)
dst+024 <- V104 (V47.[024..028)) (last use)
dst+028 <- V105 (V47.[028..029)) (last use)
Block op remainder: [032..033) [036..040)
=> Remainder strategy: retain a full block op
```
After:
```
Processing block operation [000737] that involves replacements
dst+000 <- V101 (V47.[000..008)) (last use)
dst+008 <- V102 (V47.[008..016)) (last use)
dst+016 <- V103 (V47.[016..024)) (last use)
dst+024 <- V104 (V47.[024..028)) (last use)
dst+028 <- V105 (V47.[028..029)) (last use)
Block op remainder: [032..033) [036..040)
=> Remainder strategy: long at +032
```
This leads to ASM diffs like the following:
```diff
xor edx, edx
mov qword ptr [rsp+148H], rdx
- ;; size=20 bbWeight=0.33 PerfScore 1.57
-G_M6338_IG24: ; bbWeight=0.33, nogc, extend
- vmovdqu ymm0, ymmword ptr [rsp+128H]
- vmovdqu ymmword ptr [rsp+D8H], ymm0
- mov rdx, qword ptr [rsp+148H]
- mov qword ptr [rsp+F8H], rdx
- ;; size=34 bbWeight=0.33 PerfScore 2.32
-G_M6338_IG25: ; bbWeight=0.33, extend
mov gword ptr [rsp+D8H], r12
- xor rdx, rdx
- ; gcrRegs +[rdx]
+ ;; size=28 bbWeight=0.33 PerfScore 1.90
+G_M6338_IG24: ; bbWeight=0.33, gcrefRegs=1028 {rbx rbp r12}, byrefRegs=4000 {r14}, byref
mov gword ptr [rsp+E0H], rdx
- ;; size=18 bbWeight=0.33 PerfScore 0.75
-G_M6338_IG26: ; bbWeight=0.33, gcrefRegs=1028 {rbx rbp r12}, byrefRegs=4000 {r14}, byref
- ; gcrRegs -[rdx]
+ ;; size=8 bbWeight=0.33 PerfScore 0.33
+G_M6338_IG25: ; bbWeight=0.33, gcrefRegs=1028 {rbx rbp r12}, byrefRegs=4000 {r14}, byref
mov gword ptr [rsp+E8H], rdx
mov dword ptr [rsp+F0H], r9d
mov byte ptr [rsp+F4H], 0
+ ;; size=24 bbWeight=0.33 PerfScore 0.99
+G_M6338_IG26: ; bbWeight=0.33, gcrefRegs=1028 {rbx rbp r12}, byrefRegs=4000 {r14}, byref
+ mov qword ptr [rsp+F8H], rdx
```
We have to be careful, however, since the covering segment can now
contain promoted fields. If this happens we need to make sure we write
the promoted field _after_ the remainder.
Unfortunately doing this requires quite a bit of refactoring. I have
extracted all common code for handling creation of derived accesses of
the destination/source into a common class called LocationAccess.
@jakobbotsch
jakobbotschforce-pushed the physical-promotion-covering-segment branch from 4ec5010 to 0c9b1c5CompareJune 30, 2023 22:07
@jakobbotsch

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-coreclr superpmi-diffs

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@jakobbotsch

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-coreclr superpmi-diffs

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@jakobbotsch
jakobbotsch marked this pull request as ready for review July 3, 2023 08:16
@jakobbotsch

Copy link
Copy Markdown
MemberAuthor

cc @dotnet/jit-contrib PTAL @EgorBo

Diffs. This is not just about padding, but also when the remainder is overlapping with promoted fields. A C# example of the difference:

privateS_s;voidFoo(){Ss=new();s.A=10;s.D=20;s.F=30;// A, D, F gets promoted_s=s;}privatestructS{publicbyteA;publicbyteB;publicbyteC;publicbyteD;publicbyteE;publicbyteF;}
 Processing block operation [000018] that involves replacements
dst+003 <- V04 (V01.[003..004)) (last use)
dst+005 <- V05 (V01.[005..006)) (last use)
Block op remainder: [001..003) [004..005)
- => Remainder strategy: retain a full block op+ => Remainder strategy: int at +001
 ; V00 this [V00,T01] ( 3, 3 ) ref -> rcx this class-hnd single-def
;* V01 loc0 [V01 ] ( 0, 0 ) struct ( 8) zero-ref do-not-enreg[SF] ld-addr-op
;# V02 OutArgs [V02 ] ( 1, 1 ) struct ( 0) [rsp+00H] do-not-enreg[XS] addr-exposed "OutgoingArgSpace"
;* V03 tmp1 [V03 ] ( 0, 0 ) ubyte -> zero-ref "V01.[000..001)"
;* V04 tmp2 [V04 ] ( 0, 0 ) ubyte -> zero-ref "V01.[003..004)"
;* V05 tmp3 [V05 ] ( 0, 0 ) ubyte -> zero-ref "V01.[005..006)"
; V06 tmp4 [V06,T00] ( 5, 10 ) byref -> rcx single-def "Spilling address for field-by-field copy"
;
; Lcl frame size = 0
G_M52879_IG01: ;; offset=0000H
;; size=0 bbWeight=1 PerfScore 0.00
G_M52879_IG02: ;; offset=0000H
add rcx, 8
xor eax, eax
- mov dword ptr [rcx], eax- mov dword ptr [rcx+02H], eax+ mov dword ptr [rcx+01H], eax
mov byte ptr [rcx], 10
mov byte ptr [rcx+03H], 20
mov byte ptr [rcx+05H], 30
- ;; size=22 bbWeight=1 PerfScore 5.50-G_M52879_IG03: ;; offset=0016H+ ;; size=20 bbWeight=1 PerfScore 4.50+G_M52879_IG03: ;; offset=0014H
ret ;; size=1 bbWeight=1 PerfScore 1.00
-; Total bytes of code 23, prolog size 0, PerfScore 8.80, instruction count 8, allocated bytes for code 23 (MethodHash=1da13170) for method Program:Foo():this (FullOpts)+; Total bytes of code 21, prolog size 0, PerfScore 7.60, instruction count 7, allocated bytes for code 21 (MethodHash=1da13170) for method Program:Foo():this (FullOpts)
; ============================================================

@jakobbotsch
jakobbotsch requested a review from EgorBoJuly 3, 2023 08:36
@jakobbotschjakobbotsch changed the title JIT: Handle primitive-sized remainders overlapping padding in physical promotionJIT: Handle primitive-sized remainders overlapping padding/promotions in physical promotionJul 3, 2023

@EgorBoEgorBo left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are regressions like this

@@ -13,46 +13,48 @@
; V02 arg1 [V02,T02] ( 3, 3 ) ref -> r8 class-hnd single-def
; V03 arg2 [V03,T03] ( 3, 3 ) struct ( 8) r9 single-def
;# V04 OutArgs [V04 ] ( 1, 1 ) struct ( 0) [rsp+00H] do-not-enreg[XS] addr-exposed "OutgoingArgSpace"
-; V05 tmp1 [V05,T01] ( 3, 6 ) struct (16) [rsp+00H] do-not-enreg[SF] must-init ld-addr-op "NewObj constructor temp"+; V05 tmp1 [V05,T04] ( 2, 4 ) struct (16) [rsp+08H] do-not-enreg[SF] must-init ld-addr-op "NewObj constructor temp"
;* V06 tmp2 [V06 ] ( 0, 0 ) long -> zero-ref "spilling helperCall"
;* V07 tmp3 [V07 ] ( 0, 0 ) ref -> zero-ref single-def "V05.[000..008)"
+; V08 tmp4 [V08,T01] ( 3, 6 ) byref -> rsi single-def "Spilling address for field-by-field copy"
;
-; Lcl frame size = 16+; Lcl frame size = 24
G_M36974_IG01: ; bbWeight=1, gcrefRegs=0000 {}, byrefRegs=0000 {}, byref, nogc <-- Prolog IG
- push rdi
push rsi
push rbx
- sub rsp, 16+ sub rsp, 24
xor eax, eax
- mov qword ptr [rsp], rax+ mov qword ptr [rsp+08H], rax
mov rbx, rcx
; byrRegs +[rbx]
- ;; size=16 bbWeight=1 PerfScore 4.75+ ;; size=16 bbWeight=1 PerfScore 3.75
G_M36974_IG02: ; bbWeight=1, gcrefRegs=0100 {r8}, byrefRegs=0008 {rbx}, byref
; gcrRegs +[r8]
- mov qword ptr [rsp+08H], r9- mov gword ptr [rsp], r8- lea rdi, bword ptr [rbx+08H]- ; byrRegs +[rdi]- lea rsi, bword ptr [rsp]+ mov qword ptr [rsp+10H], r9+ lea rsi, bword ptr [rbx+08H]
; byrRegs +[rsi]
- call CORINFO_HELP_ASSIGN_BYREF- ; gcrRegs -[r8]- movsq + mov rcx, rsi+ ; byrRegs +[rcx]+ mov rdx, r8+ ; gcrRegs +[rdx]+ call CORINFO_HELP_CHECKED_ASSIGN_REF+ ; gcrRegs -[rdx r8]+ ; byrRegs -[rcx]+ mov rax, qword ptr [rsp+10H]+ mov qword ptr [rsi+08H], rax
xor rax, rax
; gcrRegs +[rax]
mov gword ptr [rbx], rax
- ;; size=29 bbWeight=1 PerfScore 6.25+ ;; size=34 bbWeight=1 PerfScore 6.25
G_M36974_IG03: ; bbWeight=1, epilog, nogc, extend
- add rsp, 16+ add rsp, 24
pop rbx
pop rsi
- pop rdi
ret - ;; size=8 bbWeight=1 PerfScore 2.75+ ;; size=7 bbWeight=1 PerfScore 2.25-; Total bytes of code 53, prolog size 13, PerfScore 19.05, instruction count 20, allocated bytes for code 53 (MethodHash=ebd16f91) for method System.Linq.Parallel.HashLookupValueList`2[System.__Canon,System.Nullable`1[int]]:.ctor(System.__Canon,System.Nullable`1[int]):this (FullOpts)+; Total bytes of code 57, prolog size 13, PerfScore 17.95, instruction count 19, allocated bytes for code 57 (MethodHash=ebd16f91) for method System.Linq.Parallel.HashLookupValueList`2[System.__Canon,System.Nullable`1[int]]:.ctor(System.__Canon,System.Nullable`1[int]):this (FullOpts)
; ============================================================

expected?

@jakobbotsch

Copy link
Copy Markdown
MemberAuthor

Generally we do expect to replace CORINFO_HELP_ASSIGN_BYREF with CORINFO_HELP_CHECKED_ASSIGN_REF in the cases where we switch the remainder from a block copy (where the block has GC pointers) to a TYP_REF primitive copy.
That has the side effect that we no longer get the destination/source increments "for free" as part of that helper, so I suppose that can sometimes result in bigger code. But notice that in this case we get rid of a movsq which is a very expensive instruction, so perf wise I would expect this to be beneficial.

Now, this case is a bit peculiar because the fact that we no longer uses the full block copy for the remainder turns off an unrelated optimization:

@@ -1,20 +1,28 @@
STMT00001 ( 0x000[E-] ... ??? )
[000027] nA-XG------ ▌ STORE_BLK struct<System.Linq.Parallel.Pair`2, 16> (copy)
[000025] ---X------- ├──▌ FIELD_ADDR byref <unknown class>:<unknown field>
[000026] ----------- │ └──▌ LCL_VAR byref V00 this [000024] ----------- └──▌ LCL_VAR struct<System.Linq.Parallel.Pair`2, 16> V05 tmp1 (last use)
Processing block operation [000027] that involves replacements
dst+000 <- V07 (V05.[000..008)) (last use)
Block op remainder: [008..009) [012..016)
- => Remainder strategy: retain a full block op- Will write back V07 (V05.[000..008)) to avoid an additional write barrier- Skipping dst+000 <- V07 (V05.[000..008)); it is up-to-date in its struct local and will be handled as part of the remainder+ => Remainder strategy: long at +008++lvaGrabTemp returning 8 (V08 tmp4) called for Spilling address for field-by-field copy.++Local V05 should not be enregistered because: was accessed as a local field
New statement:
STMT00001 ( 0x000[E-] ... ??? )
- [000040] -A-XG------ ▌ COMMA void - [000039] UA--------- ├──▌ STORE_LCL_FLD ref V05 tmp1 [+0]- [000038] ----------- │ └──▌ LCL_VAR ref V07 tmp3 - [000027] nA-XG------ └──▌ STORE_BLK struct<System.Linq.Parallel.Pair`2, 16> (copy)- [000025] ---X------- ├──▌ FIELD_ADDR byref <unknown class>:<unknown field>- [000026] ----------- │ └──▌ LCL_VAR byref V00 this - [000024] ----------- └──▌ LCL_VAR struct<System.Linq.Parallel.Pair`2, 16> V05 tmp1 + [000048] -A-XG------ ▌ COMMA void + [000038] DA-X------- ├──▌ STORE_LCL_VAR byref V08 tmp4 + [000025] ---X------- │ └──▌ FIELD_ADDR byref <unknown class>:<unknown field>+ [000026] ----------- │ └──▌ LCL_VAR byref V00 this + [000047] -A--G------ └──▌ COMMA void + [000042] nA--G------ ├──▌ STOREIND ref + [000041] ----------- │ ├──▌ LCL_VAR byref V08 tmp4 + [000040] ----------- │ └──▌ LCL_VAR ref V07 tmp3 (last use)+ [000046] nA--G------ └──▌ STOREIND long + [000045] ----------- ├──▌ ADD byref + [000039] ----------- │ ├──▌ LCL_VAR byref V08 tmp4 + [000044] ----------- │ └──▌ CNS_INT long 8+ [000043] ----------- └──▌ LCL_FLD long V05 tmp1 [+8]

Specifically, in the base we notice that we have a promoted TYP_REF field that is being written to heap, which will involve a write barrier, in addition to the write barrier imposed for the block copy. So we write it back to stack first, to avoid this extra write barrier.
This has the side effect that we don't need to write it back after the write barrier call. It also means we don't need to save the target address to a local because we end up only using the address once.

In the diff we don't do this optimization anymore and instead just copy the promoted TYP_REF directly to the heap, incurring the write barrier. It doesn't really matter because overall we still only end up needing one write barrier.

We should be able to avoid the spilled local address in this case by teaching physical promotion to peel FIELD_ADDR nodes, though it comes with a bit of complexity to get the right null checking behavior. It would probably have quite decent diffs, though. Will open an issue for that.

@jakobbotsch
jakobbotsch merged commit 210a7a5 into dotnet:mainJul 4, 2023
@jakobbotsch
jakobbotsch deleted the physical-promotion-covering-segment branch July 4, 2023 16:02
@ghostghost locked as resolved and limited conversation to collaborators Aug 3, 2023
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@jakobbotsch@EgorBo
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); JIT: Handle primitive-sized remainders overlapping padding/promotions in physical promotion by jakobbotsch · Pull Request #88109 · dotnet/runtime · GitHub
Skip to content

JIT: Handle primitive-sized remainders overlapping padding/promotions in physical promotion - #88109

Merged
jakobbotsch merged 3 commits into
dotnet:mainfrom
jakobbotsch:physical-promotion-covering-segment
Jul 4, 2023
Merged

JIT: Handle primitive-sized remainders overlapping padding/promotions in physical promotion#88109
jakobbotsch merged 3 commits into
dotnet:mainfrom
jakobbotsch:physical-promotion-covering-segment

Conversation

@jakobbotsch

@jakobbotschjakobbotsch commented Jun 27, 2023

Copy link
Copy Markdown
Member

The remainder may be separated by a bit of padding or other promoted fields but still fit into a primitive; in this case it is still beneficial to copy it all as a primitive, instead of falling back to a full block copy.

Example.

privateS_s;voidFoo(){Ss=new();s.A=10;s.D=20;s.F=30;// A, D, F gets promoted_s=s;}privatestructS{publicbyteA;publicbyteB;publicbyteC;publicbyteD;publicbyteE;publicbyteF;}
 Processing block operation [000018] that involves replacements
dst+003 <- V04 (V01.[003..004)) (last use)
dst+005 <- V05 (V01.[005..006)) (last use)
Block op remainder: [001..003) [004..005)
- => Remainder strategy: retain a full block op+ => Remainder strategy: int at +001
 ; V00 this [V00,T01] ( 3, 3 ) ref -> rcx this class-hnd single-def
;* V01 loc0 [V01 ] ( 0, 0 ) struct ( 8) zero-ref do-not-enreg[SF] ld-addr-op
;# V02 OutArgs [V02 ] ( 1, 1 ) struct ( 0) [rsp+00H] do-not-enreg[XS] addr-exposed "OutgoingArgSpace"
;* V03 tmp1 [V03 ] ( 0, 0 ) ubyte -> zero-ref "V01.[000..001)"
;* V04 tmp2 [V04 ] ( 0, 0 ) ubyte -> zero-ref "V01.[003..004)"
;* V05 tmp3 [V05 ] ( 0, 0 ) ubyte -> zero-ref "V01.[005..006)"
; V06 tmp4 [V06,T00] ( 5, 10 ) byref -> rcx single-def "Spilling address for field-by-field copy"
;
; Lcl frame size = 0
G_M52879_IG01: ;; offset=0000H
;; size=0 bbWeight=1 PerfScore 0.00
G_M52879_IG02: ;; offset=0000H
add rcx, 8
xor eax, eax
- mov dword ptr [rcx], eax- mov dword ptr [rcx+02H], eax+ mov dword ptr [rcx+01H], eax
mov byte ptr [rcx], 10
mov byte ptr [rcx+03H], 20
mov byte ptr [rcx+05H], 30
- ;; size=22 bbWeight=1 PerfScore 5.50-G_M52879_IG03: ;; offset=0016H+ ;; size=20 bbWeight=1 PerfScore 4.50+G_M52879_IG03: ;; offset=0014H
ret ;; size=1 bbWeight=1 PerfScore 1.00
-; Total bytes of code 23, prolog size 0, PerfScore 8.80, instruction count 8, allocated bytes for code 23 (MethodHash=1da13170) for method Program:Foo():this (FullOpts)+; Total bytes of code 21, prolog size 0, PerfScore 7.60, instruction count 7, allocated bytes for code 21 (MethodHash=1da13170) for method Program:Foo():this (FullOpts)
; ============================================================

We have to be careful, however, since the covering segment can now contain promoted fields. If this happens we need to make sure we write the promoted field after the remainder.

@ghostghost added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Jun 27, 2023
@ghost

Copy link
Copy Markdown

Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.

Issue Details

The remainder may be separated by a bit of padding but still fit into a primitive; in this case it is still beneficial to copy it all as a primitive, instead of falling back to a full block copy.

Example.
Before:

Processing block operation [000737] that involves replacements
dst+000<-V101 (V47.[000..008)) (last use)
dst+008<-V102 (V47.[008..016)) (last use)
dst+016<-V103 (V47.[016..024)) (last use)
dst+024<-V104 (V47.[024..028)) (last use)
dst+028<-V105 (V47.[028..029)) (last use)
Block op remainder: [032..033) [036..040)
=>Remainderstrategy: retain a full block op

After:

Processing block operation [000737] that involves replacements
dst+000<-V101 (V47.[000..008)) (last use)
dst+008<-V102 (V47.[008..016)) (last use)
dst+016<-V103 (V47.[016..024)) (last use)
dst+024<-V104 (V47.[024..028)) (last use)
dst+028<-V105 (V47.[028..029)) (last use)
Block op remainder: [032..033) [036..040)
=>Remainderstrategy: long at +032

This leads to ASM diffs like the following:

 xor edx, edx
mov qword ptr [rsp+148H], rdx
- ;; size=20 bbWeight=0.33 PerfScore 1.57-G_M6338_IG24: ; bbWeight=0.33, nogc, extend- vmovdqu ymm0, ymmword ptr [rsp+128H]- vmovdqu ymmword ptr [rsp+D8H], ymm0- mov rdx, qword ptr [rsp+148H]- mov qword ptr [rsp+F8H], rdx- ;; size=34 bbWeight=0.33 PerfScore 2.32-G_M6338_IG25: ; bbWeight=0.33, extend
mov gword ptr [rsp+D8H], r12
- xor rdx, rdx- ; gcrRegs +[rdx]+ ;; size=28 bbWeight=0.33 PerfScore 1.90+G_M6338_IG24: ; bbWeight=0.33, gcrefRegs=1028 {rbx rbp r12}, byrefRegs=4000 {r14}, byref
mov gword ptr [rsp+E0H], rdx
- ;; size=18 bbWeight=0.33 PerfScore 0.75-G_M6338_IG26: ; bbWeight=0.33, gcrefRegs=1028 {rbx rbp r12}, byrefRegs=4000 {r14}, byref- ; gcrRegs -[rdx]+ ;; size=8 bbWeight=0.33 PerfScore 0.33+G_M6338_IG25: ; bbWeight=0.33, gcrefRegs=1028 {rbx rbp r12}, byrefRegs=4000 {r14}, byref
mov gword ptr [rsp+E8H], rdx
mov dword ptr [rsp+F0H], r9d
mov byte ptr [rsp+F4H], 0
+ ;; size=24 bbWeight=0.33 PerfScore 0.99+G_M6338_IG26: ; bbWeight=0.33, gcrefRegs=1028 {rbx rbp r12}, byrefRegs=4000 {r14}, byref+ mov qword ptr [rsp+F8H], rdx

We have to be careful, however, since the covering segment can now contain promoted fields. If this happens we need to make sure we write the promoted field after the remainder.

Author:jakobbotsch
Assignees:-
Labels:

area-CodeGen-coreclr

Milestone:-

…l promotion
The remainder may be separated by a bit of padding but still fit into a
primitive; in this case it is still beneficial to copy it all as a
primitive, instead of falling back to a full block copy.
Example.
Before:
```
Processing block operation [000737] that involves replacements
dst+000 <- V101 (V47.[000..008)) (last use)
dst+008 <- V102 (V47.[008..016)) (last use)
dst+016 <- V103 (V47.[016..024)) (last use)
dst+024 <- V104 (V47.[024..028)) (last use)
dst+028 <- V105 (V47.[028..029)) (last use)
Block op remainder: [032..033) [036..040)
=> Remainder strategy: retain a full block op
```
After:
```
Processing block operation [000737] that involves replacements
dst+000 <- V101 (V47.[000..008)) (last use)
dst+008 <- V102 (V47.[008..016)) (last use)
dst+016 <- V103 (V47.[016..024)) (last use)
dst+024 <- V104 (V47.[024..028)) (last use)
dst+028 <- V105 (V47.[028..029)) (last use)
Block op remainder: [032..033) [036..040)
=> Remainder strategy: long at +032
```
This leads to ASM diffs like the following:
```diff
xor edx, edx
mov qword ptr [rsp+148H], rdx
- ;; size=20 bbWeight=0.33 PerfScore 1.57
-G_M6338_IG24: ; bbWeight=0.33, nogc, extend
- vmovdqu ymm0, ymmword ptr [rsp+128H]
- vmovdqu ymmword ptr [rsp+D8H], ymm0
- mov rdx, qword ptr [rsp+148H]
- mov qword ptr [rsp+F8H], rdx
- ;; size=34 bbWeight=0.33 PerfScore 2.32
-G_M6338_IG25: ; bbWeight=0.33, extend
mov gword ptr [rsp+D8H], r12
- xor rdx, rdx
- ; gcrRegs +[rdx]
+ ;; size=28 bbWeight=0.33 PerfScore 1.90
+G_M6338_IG24: ; bbWeight=0.33, gcrefRegs=1028 {rbx rbp r12}, byrefRegs=4000 {r14}, byref
mov gword ptr [rsp+E0H], rdx
- ;; size=18 bbWeight=0.33 PerfScore 0.75
-G_M6338_IG26: ; bbWeight=0.33, gcrefRegs=1028 {rbx rbp r12}, byrefRegs=4000 {r14}, byref
- ; gcrRegs -[rdx]
+ ;; size=8 bbWeight=0.33 PerfScore 0.33
+G_M6338_IG25: ; bbWeight=0.33, gcrefRegs=1028 {rbx rbp r12}, byrefRegs=4000 {r14}, byref
mov gword ptr [rsp+E8H], rdx
mov dword ptr [rsp+F0H], r9d
mov byte ptr [rsp+F4H], 0
+ ;; size=24 bbWeight=0.33 PerfScore 0.99
+G_M6338_IG26: ; bbWeight=0.33, gcrefRegs=1028 {rbx rbp r12}, byrefRegs=4000 {r14}, byref
+ mov qword ptr [rsp+F8H], rdx
```
We have to be careful, however, since the covering segment can now
contain promoted fields. If this happens we need to make sure we write
the promoted field _after_ the remainder.
Unfortunately doing this requires quite a bit of refactoring. I have
extracted all common code for handling creation of derived accesses of
the destination/source into a common class called LocationAccess.
@jakobbotsch
jakobbotschforce-pushed the physical-promotion-covering-segment branch from 4ec5010 to 0c9b1c5CompareJune 30, 2023 22:07
@jakobbotsch

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-coreclr superpmi-diffs

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@jakobbotsch

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-coreclr superpmi-diffs

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@jakobbotsch
jakobbotsch marked this pull request as ready for review July 3, 2023 08:16
@jakobbotsch

Copy link
Copy Markdown
MemberAuthor

cc @dotnet/jit-contrib PTAL @EgorBo

Diffs. This is not just about padding, but also when the remainder is overlapping with promoted fields. A C# example of the difference:

privateS_s;voidFoo(){Ss=new();s.A=10;s.D=20;s.F=30;// A, D, F gets promoted_s=s;}privatestructS{publicbyteA;publicbyteB;publicbyteC;publicbyteD;publicbyteE;publicbyteF;}
 Processing block operation [000018] that involves replacements
dst+003 <- V04 (V01.[003..004)) (last use)
dst+005 <- V05 (V01.[005..006)) (last use)
Block op remainder: [001..003) [004..005)
- => Remainder strategy: retain a full block op+ => Remainder strategy: int at +001
 ; V00 this [V00,T01] ( 3, 3 ) ref -> rcx this class-hnd single-def
;* V01 loc0 [V01 ] ( 0, 0 ) struct ( 8) zero-ref do-not-enreg[SF] ld-addr-op
;# V02 OutArgs [V02 ] ( 1, 1 ) struct ( 0) [rsp+00H] do-not-enreg[XS] addr-exposed "OutgoingArgSpace"
;* V03 tmp1 [V03 ] ( 0, 0 ) ubyte -> zero-ref "V01.[000..001)"
;* V04 tmp2 [V04 ] ( 0, 0 ) ubyte -> zero-ref "V01.[003..004)"
;* V05 tmp3 [V05 ] ( 0, 0 ) ubyte -> zero-ref "V01.[005..006)"
; V06 tmp4 [V06,T00] ( 5, 10 ) byref -> rcx single-def "Spilling address for field-by-field copy"
;
; Lcl frame size = 0
G_M52879_IG01: ;; offset=0000H
;; size=0 bbWeight=1 PerfScore 0.00
G_M52879_IG02: ;; offset=0000H
add rcx, 8
xor eax, eax
- mov dword ptr [rcx], eax- mov dword ptr [rcx+02H], eax+ mov dword ptr [rcx+01H], eax
mov byte ptr [rcx], 10
mov byte ptr [rcx+03H], 20
mov byte ptr [rcx+05H], 30
- ;; size=22 bbWeight=1 PerfScore 5.50-G_M52879_IG03: ;; offset=0016H+ ;; size=20 bbWeight=1 PerfScore 4.50+G_M52879_IG03: ;; offset=0014H
ret ;; size=1 bbWeight=1 PerfScore 1.00
-; Total bytes of code 23, prolog size 0, PerfScore 8.80, instruction count 8, allocated bytes for code 23 (MethodHash=1da13170) for method Program:Foo():this (FullOpts)+; Total bytes of code 21, prolog size 0, PerfScore 7.60, instruction count 7, allocated bytes for code 21 (MethodHash=1da13170) for method Program:Foo():this (FullOpts)
; ============================================================

@jakobbotsch
jakobbotsch requested a review from EgorBoJuly 3, 2023 08:36
@jakobbotschjakobbotsch changed the title JIT: Handle primitive-sized remainders overlapping padding in physical promotionJIT: Handle primitive-sized remainders overlapping padding/promotions in physical promotionJul 3, 2023

@EgorBoEgorBo left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are regressions like this

@@ -13,46 +13,48 @@
; V02 arg1 [V02,T02] ( 3, 3 ) ref -> r8 class-hnd single-def
; V03 arg2 [V03,T03] ( 3, 3 ) struct ( 8) r9 single-def
;# V04 OutArgs [V04 ] ( 1, 1 ) struct ( 0) [rsp+00H] do-not-enreg[XS] addr-exposed "OutgoingArgSpace"
-; V05 tmp1 [V05,T01] ( 3, 6 ) struct (16) [rsp+00H] do-not-enreg[SF] must-init ld-addr-op "NewObj constructor temp"+; V05 tmp1 [V05,T04] ( 2, 4 ) struct (16) [rsp+08H] do-not-enreg[SF] must-init ld-addr-op "NewObj constructor temp"
;* V06 tmp2 [V06 ] ( 0, 0 ) long -> zero-ref "spilling helperCall"
;* V07 tmp3 [V07 ] ( 0, 0 ) ref -> zero-ref single-def "V05.[000..008)"
+; V08 tmp4 [V08,T01] ( 3, 6 ) byref -> rsi single-def "Spilling address for field-by-field copy"
;
-; Lcl frame size = 16+; Lcl frame size = 24
G_M36974_IG01: ; bbWeight=1, gcrefRegs=0000 {}, byrefRegs=0000 {}, byref, nogc <-- Prolog IG
- push rdi
push rsi
push rbx
- sub rsp, 16+ sub rsp, 24
xor eax, eax
- mov qword ptr [rsp], rax+ mov qword ptr [rsp+08H], rax
mov rbx, rcx
; byrRegs +[rbx]
- ;; size=16 bbWeight=1 PerfScore 4.75+ ;; size=16 bbWeight=1 PerfScore 3.75
G_M36974_IG02: ; bbWeight=1, gcrefRegs=0100 {r8}, byrefRegs=0008 {rbx}, byref
; gcrRegs +[r8]
- mov qword ptr [rsp+08H], r9- mov gword ptr [rsp], r8- lea rdi, bword ptr [rbx+08H]- ; byrRegs +[rdi]- lea rsi, bword ptr [rsp]+ mov qword ptr [rsp+10H], r9+ lea rsi, bword ptr [rbx+08H]
; byrRegs +[rsi]
- call CORINFO_HELP_ASSIGN_BYREF- ; gcrRegs -[r8]- movsq + mov rcx, rsi+ ; byrRegs +[rcx]+ mov rdx, r8+ ; gcrRegs +[rdx]+ call CORINFO_HELP_CHECKED_ASSIGN_REF+ ; gcrRegs -[rdx r8]+ ; byrRegs -[rcx]+ mov rax, qword ptr [rsp+10H]+ mov qword ptr [rsi+08H], rax
xor rax, rax
; gcrRegs +[rax]
mov gword ptr [rbx], rax
- ;; size=29 bbWeight=1 PerfScore 6.25+ ;; size=34 bbWeight=1 PerfScore 6.25
G_M36974_IG03: ; bbWeight=1, epilog, nogc, extend
- add rsp, 16+ add rsp, 24
pop rbx
pop rsi
- pop rdi
ret - ;; size=8 bbWeight=1 PerfScore 2.75+ ;; size=7 bbWeight=1 PerfScore 2.25-; Total bytes of code 53, prolog size 13, PerfScore 19.05, instruction count 20, allocated bytes for code 53 (MethodHash=ebd16f91) for method System.Linq.Parallel.HashLookupValueList`2[System.__Canon,System.Nullable`1[int]]:.ctor(System.__Canon,System.Nullable`1[int]):this (FullOpts)+; Total bytes of code 57, prolog size 13, PerfScore 17.95, instruction count 19, allocated bytes for code 57 (MethodHash=ebd16f91) for method System.Linq.Parallel.HashLookupValueList`2[System.__Canon,System.Nullable`1[int]]:.ctor(System.__Canon,System.Nullable`1[int]):this (FullOpts)
; ============================================================

expected?

@jakobbotsch

Copy link
Copy Markdown
MemberAuthor

Generally we do expect to replace CORINFO_HELP_ASSIGN_BYREF with CORINFO_HELP_CHECKED_ASSIGN_REF in the cases where we switch the remainder from a block copy (where the block has GC pointers) to a TYP_REF primitive copy.
That has the side effect that we no longer get the destination/source increments "for free" as part of that helper, so I suppose that can sometimes result in bigger code. But notice that in this case we get rid of a movsq which is a very expensive instruction, so perf wise I would expect this to be beneficial.

Now, this case is a bit peculiar because the fact that we no longer uses the full block copy for the remainder turns off an unrelated optimization:

@@ -1,20 +1,28 @@
STMT00001 ( 0x000[E-] ... ??? )
[000027] nA-XG------ ▌ STORE_BLK struct<System.Linq.Parallel.Pair`2, 16> (copy)
[000025] ---X------- ├──▌ FIELD_ADDR byref <unknown class>:<unknown field>
[000026] ----------- │ └──▌ LCL_VAR byref V00 this [000024] ----------- └──▌ LCL_VAR struct<System.Linq.Parallel.Pair`2, 16> V05 tmp1 (last use)
Processing block operation [000027] that involves replacements
dst+000 <- V07 (V05.[000..008)) (last use)
Block op remainder: [008..009) [012..016)
- => Remainder strategy: retain a full block op- Will write back V07 (V05.[000..008)) to avoid an additional write barrier- Skipping dst+000 <- V07 (V05.[000..008)); it is up-to-date in its struct local and will be handled as part of the remainder+ => Remainder strategy: long at +008++lvaGrabTemp returning 8 (V08 tmp4) called for Spilling address for field-by-field copy.++Local V05 should not be enregistered because: was accessed as a local field
New statement:
STMT00001 ( 0x000[E-] ... ??? )
- [000040] -A-XG------ ▌ COMMA void - [000039] UA--------- ├──▌ STORE_LCL_FLD ref V05 tmp1 [+0]- [000038] ----------- │ └──▌ LCL_VAR ref V07 tmp3 - [000027] nA-XG------ └──▌ STORE_BLK struct<System.Linq.Parallel.Pair`2, 16> (copy)- [000025] ---X------- ├──▌ FIELD_ADDR byref <unknown class>:<unknown field>- [000026] ----------- │ └──▌ LCL_VAR byref V00 this - [000024] ----------- └──▌ LCL_VAR struct<System.Linq.Parallel.Pair`2, 16> V05 tmp1 + [000048] -A-XG------ ▌ COMMA void + [000038] DA-X------- ├──▌ STORE_LCL_VAR byref V08 tmp4 + [000025] ---X------- │ └──▌ FIELD_ADDR byref <unknown class>:<unknown field>+ [000026] ----------- │ └──▌ LCL_VAR byref V00 this + [000047] -A--G------ └──▌ COMMA void + [000042] nA--G------ ├──▌ STOREIND ref + [000041] ----------- │ ├──▌ LCL_VAR byref V08 tmp4 + [000040] ----------- │ └──▌ LCL_VAR ref V07 tmp3 (last use)+ [000046] nA--G------ └──▌ STOREIND long + [000045] ----------- ├──▌ ADD byref + [000039] ----------- │ ├──▌ LCL_VAR byref V08 tmp4 + [000044] ----------- │ └──▌ CNS_INT long 8+ [000043] ----------- └──▌ LCL_FLD long V05 tmp1 [+8]

Specifically, in the base we notice that we have a promoted TYP_REF field that is being written to heap, which will involve a write barrier, in addition to the write barrier imposed for the block copy. So we write it back to stack first, to avoid this extra write barrier.
This has the side effect that we don't need to write it back after the write barrier call. It also means we don't need to save the target address to a local because we end up only using the address once.

In the diff we don't do this optimization anymore and instead just copy the promoted TYP_REF directly to the heap, incurring the write barrier. It doesn't really matter because overall we still only end up needing one write barrier.

We should be able to avoid the spilled local address in this case by teaching physical promotion to peel FIELD_ADDR nodes, though it comes with a bit of complexity to get the right null checking behavior. It would probably have quite decent diffs, though. Will open an issue for that.

@jakobbotsch
jakobbotsch merged commit 210a7a5 into dotnet:mainJul 4, 2023
@jakobbotsch
jakobbotsch deleted the physical-promotion-covering-segment branch July 4, 2023 16:02
@ghostghost locked as resolved and limited conversation to collaborators Aug 3, 2023
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@jakobbotsch@EgorBo