Improve SpanHelpers.ClearWithReferences for arm64 - #93346

Closed
EgorBo wants to merge 6 commits into
dotnet:mainfrom
EgorBo:opt-clear-with-ref
Closed

Improve SpanHelpers.ClearWithReferences for arm64#93346
EgorBo wants to merge 6 commits into
dotnet:mainfrom
EgorBo:opt-clear-with-ref

Conversation

@EgorBo

@EgorBoEgorBo commented Oct 11, 2023

Copy link
Copy Markdown
Member

Follow up to #93214, now for Span<GC>.Clear();

I noticed that the existing implementation is slightly sub-optimal on arm: it's relying on complex addressing modes + is not aligned to 16 bytes. Turns out, 16-byte alignment makes a lot of sense even if we don't use SIMD - stp (two 8-byte stores) benefits from it as well.

Standalone benchmark (for simpler tests)

Apple M2 Max (also tested on Ampere):

| Method | len | Mean | Ratio |
|----------- |----- |-----------:|------:|-
| Clear_Base | 1 | 1.199 ns | 1.00 |
| Clear_PR | 1 | 1.186 ns | 0.99 |
| | | | |
| Clear_Base | 5 | 1.215 ns | 1.00 |
| Clear_PR | 5 | 1.215 ns | 1.00 |
| | | | |
| Clear_Base | 8 | 1.617 ns | 1.00 |
| Clear_PR | 8 | 1.269 ns | 0.78 |
| | | | |
| Clear_Base | 16 | 2.405 ns | 1.00 |
| Clear_PR | 16 | 2.061 ns | 0.86 |
| | | | |
| Clear_Base | 64 | 8.727 ns | 1.00 |
| Clear_PR | 64 | 5.415 ns | 0.62 |
| | | | |
| Clear_Base | 512 | 70.081 ns | 1.00 |
| Clear_PR | 512 | 35.482 ns | 0.51 |
| | | | |
| Clear_Base | 1024 | 140.080 ns | 1.00 |
| Clear_PR | 1024 | 72.254 ns | 0.52 |
| | | | |
| Clear_Base | 4096 | 559.551 ns | 1.00 |
| Clear_PR | 4096 | 333.439 ns | 0.60 |

Ryzen 7950X:

| Method | len | Mean | Ratio |
|----------- |----- |------------:|------:|-
| Clear_Base | 1 | 0.3854 ns | 1.00 |
| Clear_PR | 1 | 0.3798 ns | 0.99 |
| | | | |
| Clear_Base | 5 | 0.5310 ns | 1.00 |
| Clear_PR | 5 | 0.3907 ns | 0.74 |
| | | | |
| Clear_Base | 8 | 0.8995 ns | 1.00 |
| Clear_PR | 8 | 0.5264 ns | 0.59 |
| | | | |
| Clear_Base | 16 | 1.4398 ns | 1.00 |
| Clear_PR | 16 | 1.2039 ns | 0.84 |
| | | | |
| Clear_Base | 64 | 5.1603 ns | 1.00 |
| Clear_PR | 64 | 5.1023 ns | 0.99 |
| | | | |
| Clear_Base | 512 | 46.0601 ns | 1.00 |
| Clear_PR | 512 | 45.9100 ns | 1.00 |
| | | | |
| Clear_Base | 1024 | 94.9471 ns | 1.00 |
| Clear_PR | 1024 | 94.7812 ns | 1.00 |
| | | | |
| Clear_Base | 4096 | 380.4915 ns | 1.00 |
| Clear_PR | 4096 | 375.3246 ns | 0.99 |

For X64, it slightly improves small inputs. It seems like if we pin the input and use SIMD we can see >2X improvement for large inputs but I was not sure about doing pinning (from GC's point of view, atomicity is fine) here and left as is, cc @jkotas

@ghostghost assigned EgorBoOct 11, 2023
@ghost

Copy link
Copy Markdown

Tagging subscribers to this area: @dotnet/area-system-memory
See info in area-owners.md if you want to be subscribed.

Issue Details

Follow up to #93214, now for Span<GC>.Clear();

I noticed that the existing implementation is slightly sub-optimal on arm: it's relying on complex addressing modes + is not aligned to 16 bytes. Turns out, 16-byte alignment makes a lot of sense even if we don't use SIMD - stp (two 8-byte stores) benefits from it as well.

Standalone benchmark (for simpler tests)

Apple M2 Max (also tested on Ampere):

| Method | len | Mean | Ratio |
|----------- |----- |-----------:|------:|-
| Clear_Base | 1 | 1.199 ns | 1.00 |
| Clear_PR | 1 | 1.186 ns | 0.99 |
| | | | |
| Clear_Base | 5 | 1.215 ns | 1.00 |
| Clear_PR | 5 | 1.215 ns | 1.00 |
| | | | |
| Clear_Base | 8 | 1.617 ns | 1.00 |
| Clear_PR | 8 | 1.269 ns | 0.78 |
| | | | |
| Clear_Base | 16 | 2.405 ns | 1.00 |
| Clear_PR | 16 | 2.061 ns | 0.86 |
| | | | |
| Clear_Base | 64 | 8.727 ns | 1.00 |
| Clear_PR | 64 | 5.415 ns | 0.62 |
| | | | |
| Clear_Base | 512 | 70.081 ns | 1.00 |
| Clear_PR | 512 | 35.482 ns | 0.51 |
| | | | |
| Clear_Base | 1024 | 140.080 ns | 1.00 |
| Clear_PR | 1024 | 72.254 ns | 0.52 |
| | | | |
| Clear_Base | 4096 | 559.551 ns | 1.00 |
| Clear_PR | 4096 | 333.439 ns | 0.60 |

Ryzen 7950X:

| Method | len | Mean | Ratio |
|----------- |----- |------------:|------:|-
| Clear_Base | 1 | 0.3854 ns | 1.00 |
| Clear_PR | 1 | 0.3798 ns | 0.99 |
| | | | |
| Clear_Base | 5 | 0.5310 ns | 1.00 |
| Clear_PR | 5 | 0.3907 ns | 0.74 |
| | | | |
| Clear_Base | 8 | 0.8995 ns | 1.00 |
| Clear_PR | 8 | 0.5264 ns | 0.59 |
| | | | |
| Clear_Base | 16 | 1.4398 ns | 1.00 |
| Clear_PR | 16 | 1.2039 ns | 0.84 |
| | | | |
| Clear_Base | 64 | 5.1603 ns | 1.00 |
| Clear_PR | 64 | 5.1023 ns | 0.99 |
| | | | |
| Clear_Base | 512 | 46.0601 ns | 1.00 |
| Clear_PR | 512 | 45.9100 ns | 1.00 |
| | | | |
| Clear_Base | 1024 | 94.9471 ns | 1.00 |
| Clear_PR | 1024 | 94.7812 ns | 1.00 |
| | | | |
| Clear_Base | 4096 | 380.4915 ns | 1.00 |
| Clear_PR | 4096 | 375.3246 ns | 0.99 |

For X64, it slightly improves small inputs. It seems like if we pin the input and use SIMD we can see >2X improvement for large inputs but I was not sure about doing pinning (from GC's point of view, atomicity is fine) here and left as is, cc @jkotas

Author:EgorBo
Assignees:EgorBo
Labels:

area-System.Memory

Milestone:-

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.

This works great for microbenchmarks, but it works poorly for real work loads where the jump target is poorly predicated. We used to have switch table like this in managed memcopy originally, but it was later removed.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

This works great for microbenchmarks, but it works poorly for real work loads where the jump target is poorly predicated. We used to have switch table like this in managed memcopy originally, but it was later removed.

Should I revert to the previous shape? I was mostly interested in aligning to 16 bytes (it's the reason I am seeing nice results on arm64) and slightly different addressing mode in the main loop, to make it foldable into stp

@EgorBoEgorBoOct 11, 2023

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Btw, presumably, PGO can re-shuffle the switch (or convert to if-else) based on profile. However, in the real world it's likely has a mixed profile.

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.

Should I revert to the previous shape?

Yes, unless you can prove that the switch with jump table is better for real workloads.

For reference, dotnet/coreclr#9786 is the PR that got rid of the switch from memory copy. It went through extensive validation.

@tannergoodingtannergoodingOct 13, 2023

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.

This works great for microbenchmarks, but it works poorly for real work loads where the jump target is poorly predicated. We used to have switch table like this in managed memcopy originally, but it was later removed.

@jkotas, I'm not sure what you mean here, could you elaborate?

The hardware optimization manuals and underlying native memcpy algorithms across all 3 major implementations (MSVC, GCC, Clang; for x64 and Arm64) use/recommend a jump table explicitly for this reason. Because an unconditional and unpredictable branch, is better than a tight tree of conditional and still unpredictable branches.

The premise is that a fallthrough jump table allows a tight, unrolled version of handling for the trailing elements to exist. This then requires 1 unpredicted branch to decide which of the immediately following and equidistant switch cases needs to be jumped to. These cases are often already in the instruction/decode cache due to being in the same or following 32-byte code window.

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.

dotnet/coreclr#9786 was done by Intel engineers to address bottlenecks observed in real workloads. Yes, it goes contrary to the guidance you have quoted.

Possible explanation: Conditional jumps and indirect jumps use two different predictors. The relative performance of conditional jumps vs. indirect jump depends on relative pressure on each predictor. .NET code tends to have more indirect jumps than an average code. It means that the indirect jump predictor sees more pressure in .NET code and it is more profitable to depend on indirect jump predictor in .NET memcpy implementation.

@EgorBo

EgorBo commented Oct 11, 2023

Copy link
Copy Markdown
MemberAuthor

Codegen on arm64:

New codegen:
; Method Prog:ClearWithReferences2(byref,ulong) (FullOpts)G_M64195_IG01: ;; offset=0x0000 stp fp, lr,[sp, #-0x10]!mov fp,sp ;; size=8 bbWeight=8 PerfScore 12.00G_M64195_IG02: ;; offset=0x0008mov x2, x1cmp x2, #8 bhi G_M64195_IG12 ;; size=12 bbWeight=8 PerfScore 16.00G_M64195_IG03: ;; offset=0x0014cmp w2, #8 bhi G_M64195_IG12mov w1, w2 adr x2,[@RWD00] ldr w2,[x2, x1,LSL #2] adr x3,[G_M64195_IG02]add x2, x2, x3 br x2 ;; size=32 bbWeight=4 PerfScore 30.00G_M64195_IG04: ;; offset=0x0034str xzr,[x0] b G_M64195_IG15align[0 bytes for IG13]align[0 bytes]align[0 bytes]align[0 bytes] ;; size=8 bbWeight=0.50 PerfScore 1.00G_M64195_IG05: ;; offset=0x003C stp xzr, xzr,[x0] b G_M64195_IG15 ;; size=8 bbWeight=0.50 PerfScore 1.00G_M64195_IG06: ;; offset=0x0044 stp xzr, xzr,[x0]str xzr,[x0, #0x10] b G_M64195_IG15 ;; size=12 bbWeight=0.50 PerfScore 1.50G_M64195_IG07: ;; offset=0x0050 stp xzr, xzr,[x0] stp xzr, xzr,[x0, #0x10] b G_M64195_IG15 ;; size=12 bbWeight=0.50 PerfScore 1.50G_M64195_IG08: ;; offset=0x005C stp xzr, xzr,[x0] stp xzr, xzr,[x0, #0x10]str xzr,[x0, #0x20] b G_M64195_IG15 ;; size=16 bbWeight=0.50 PerfScore 2.00G_M64195_IG09: ;; offset=0x006C stp xzr, xzr,[x0] stp xzr, xzr,[x0, #0x10] stp xzr, xzr,[x0, #0x20] b G_M64195_IG15 ;; size=16 bbWeight=0.50 PerfScore 2.00G_M64195_IG10: ;; offset=0x007C stp xzr, xzr,[x0] stp xzr, xzr,[x0, #0x10] stp xzr, xzr,[x0, #0x20]str xzr,[x0, #0x30] b G_M64195_IG15 ;; size=20 bbWeight=0.50 PerfScore 2.50G_M64195_IG11: ;; offset=0x0090 stp xzr, xzr,[x0] stp xzr, xzr,[x0, #0x10] stp xzr, xzr,[x0, #0x20] stp xzr, xzr,[x0, #0x30] b G_M64195_IG15 ;; size=20 bbWeight=0.50 PerfScore 2.50G_M64195_IG12: ;; offset=0x00A4 lsr x2, x1, #3 ;; size=4 bbWeight=4 PerfScore 4.00G_M64195_IG13: ;; offset=0x00A8 stp xzr, xzr,[x0] stp xzr, xzr,[x0, #0x10] stp xzr, xzr,[x0, #0x20] stp xzr, xzr,[x0, #0x30]add x0, x0, #64sub x2, x2, #1 cbnz x2, G_M64195_IG13 ;; size=28 bbWeight=32 PerfScore 192.00G_M64195_IG14: ;; offset=0x00C4and x1, x1, #7 b G_M64195_IG02 ;; size=8 bbWeight=4 PerfScore 6.00G_M64195_IG15: ;; offset=0x00CC ldp fp, lr,[sp], #0x10ret lr ;; size=8 bbWeight=1 PerfScore 2.00RWD00 dd	000000C4h ; case G_M64195_IG15dd0000002Ch ; case G_M64195_IG04dd00000034h ; case G_M64195_IG05dd0000003Ch ; case G_M64195_IG06dd00000048h ; case G_M64195_IG07dd00000054h ; case G_M64195_IG08dd00000064h ; case G_M64195_IG09dd00000074h ; case G_M64195_IG10dd00000088h ; case G_M64195_IG11; Total bytes of code: 212
Previous codegen:
; Method Prog:ClearWithReferences(byref,ulong) (FullOpts)G_M45361_IG01: ;; offset=0x0000 stp fp, lr,[sp, #-0x10]!mov fp,sp ;; size=8 bbWeight=1 PerfScore 1.50G_M45361_IG02: ;; offset=0x0008cmp x1, #8 blo G_M45361_IG04align[0 bytes for IG03]align[0 bytes]align[0 bytes]align[0 bytes] ;; size=8 bbWeight=1 PerfScore 1.50G_M45361_IG03: ;; offset=0x0010lsl x2, x1, #3add x3, x0, x2str xzr,[x3, #-0x08]add x3, x0, x2str xzr,[x3, #-0x10]add x3, x0, x2str xzr,[x3, #-0x18]add x3, x0, x2str xzr,[x3, #-0x20]add x3, x0, x2str xzr,[x3, #-0x28]add x3, x0, x2str xzr,[x3, #-0x30]add x3, x0, x2str xzr,[x3, #-0x38]add x2, x0, x2str xzr,[x2, #-0x40]sub x1, x1, #8cmp x1, #8 bhs G_M45361_IG03 ;; size=80 bbWeight=4 PerfScore 60.00G_M45361_IG04: ;; offset=0x0060cmp x1, #4 bhs G_M45361_IG07 ;; size=8 bbWeight=1 PerfScore 1.50G_M45361_IG05: ;; offset=0x0068cmp x1, #2 bhs G_M45361_IG08 cbnz x1, G_M45361_IG09 ;; size=12 bbWeight=0.50 PerfScore 1.25G_M45361_IG06: ;; offset=0x0074 ldp fp, lr,[sp], #0x10ret lr ;; size=8 bbWeight=0.50 PerfScore 1.00G_M45361_IG07: ;; offset=0x007C stp xzr, xzr,[x0, #0x10]lsl x2, x1, #3add x3, x0, x2str xzr,[x3, #-0x18]add x2, x0, x2str xzr,[x2, #-0x10] ;; size=24 bbWeight=0.50 PerfScore 2.50G_M45361_IG08: ;; offset=0x0094str xzr,[x0, #0x08]lsl x2, x1, #3add x1, x0, x2str xzr,[x1, #-0x08] ;; size=16 bbWeight=0.50 PerfScore 1.75G_M45361_IG09: ;; offset=0x00A4str xzr,[x0] ;; size=4 bbWeight=0.50 PerfScore 0.50G_M45361_IG10: ;; offset=0x00A8 ldp fp, lr,[sp], #0x10ret lr ;; size=8 bbWeight=0.50 PerfScore 1.00; Total bytes of code: 176

I can tune JIT to fold

stp xzr, xzr, [x0]
stp xzr, xzr, [x0, #..]

to SIMD in the newly added LowerIndirCoelsceStore, but it seems to be fast as is.

@EgorBo
EgorBo marked this pull request as draft October 11, 2023 22:59
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.cs Outdated
@EgorBo

EgorBo commented Oct 13, 2023

Copy link
Copy Markdown
MemberAuthor

I have mixed results with this change, I'll wait for fixes for #93382 (needed for the remainder) and #76067 (comment) (needed for the main loop) and try again after

@ghostghost locked as resolved and limited conversation to collaborators Nov 12, 2023
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@EgorBo@jkotas@tannergooding
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n 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;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Improve SpanHelpers.ClearWithReferences for arm64 - #93346

Closed
EgorBo wants to merge 6 commits into
dotnet:mainfrom
EgorBo:opt-clear-with-ref
Closed

Improve SpanHelpers.ClearWithReferences for arm64#93346
EgorBo wants to merge 6 commits into
dotnet:mainfrom
EgorBo:opt-clear-with-ref

Conversation

@EgorBo

@EgorBoEgorBo commented Oct 11, 2023

Copy link
Copy Markdown
Member

Follow up to #93214, now for Span<GC>.Clear();

I noticed that the existing implementation is slightly sub-optimal on arm: it's relying on complex addressing modes + is not aligned to 16 bytes. Turns out, 16-byte alignment makes a lot of sense even if we don't use SIMD - stp (two 8-byte stores) benefits from it as well.

Standalone benchmark (for simpler tests)

Apple M2 Max (also tested on Ampere):

| Method | len | Mean | Ratio |
|----------- |----- |-----------:|------:|-
| Clear_Base | 1 | 1.199 ns | 1.00 |
| Clear_PR | 1 | 1.186 ns | 0.99 |
| | | | |
| Clear_Base | 5 | 1.215 ns | 1.00 |
| Clear_PR | 5 | 1.215 ns | 1.00 |
| | | | |
| Clear_Base | 8 | 1.617 ns | 1.00 |
| Clear_PR | 8 | 1.269 ns | 0.78 |
| | | | |
| Clear_Base | 16 | 2.405 ns | 1.00 |
| Clear_PR | 16 | 2.061 ns | 0.86 |
| | | | |
| Clear_Base | 64 | 8.727 ns | 1.00 |
| Clear_PR | 64 | 5.415 ns | 0.62 |
| | | | |
| Clear_Base | 512 | 70.081 ns | 1.00 |
| Clear_PR | 512 | 35.482 ns | 0.51 |
| | | | |
| Clear_Base | 1024 | 140.080 ns | 1.00 |
| Clear_PR | 1024 | 72.254 ns | 0.52 |
| | | | |
| Clear_Base | 4096 | 559.551 ns | 1.00 |
| Clear_PR | 4096 | 333.439 ns | 0.60 |

Ryzen 7950X:

| Method | len | Mean | Ratio |
|----------- |----- |------------:|------:|-
| Clear_Base | 1 | 0.3854 ns | 1.00 |
| Clear_PR | 1 | 0.3798 ns | 0.99 |
| | | | |
| Clear_Base | 5 | 0.5310 ns | 1.00 |
| Clear_PR | 5 | 0.3907 ns | 0.74 |
| | | | |
| Clear_Base | 8 | 0.8995 ns | 1.00 |
| Clear_PR | 8 | 0.5264 ns | 0.59 |
| | | | |
| Clear_Base | 16 | 1.4398 ns | 1.00 |
| Clear_PR | 16 | 1.2039 ns | 0.84 |
| | | | |
| Clear_Base | 64 | 5.1603 ns | 1.00 |
| Clear_PR | 64 | 5.1023 ns | 0.99 |
| | | | |
| Clear_Base | 512 | 46.0601 ns | 1.00 |
| Clear_PR | 512 | 45.9100 ns | 1.00 |
| | | | |
| Clear_Base | 1024 | 94.9471 ns | 1.00 |
| Clear_PR | 1024 | 94.7812 ns | 1.00 |
| | | | |
| Clear_Base | 4096 | 380.4915 ns | 1.00 |
| Clear_PR | 4096 | 375.3246 ns | 0.99 |

For X64, it slightly improves small inputs. It seems like if we pin the input and use SIMD we can see >2X improvement for large inputs but I was not sure about doing pinning (from GC's point of view, atomicity is fine) here and left as is, cc @jkotas

@ghostghost assigned EgorBoOct 11, 2023
@ghost

Copy link
Copy Markdown

Tagging subscribers to this area: @dotnet/area-system-memory
See info in area-owners.md if you want to be subscribed.

Issue Details

Follow up to #93214, now for Span<GC>.Clear();

I noticed that the existing implementation is slightly sub-optimal on arm: it's relying on complex addressing modes + is not aligned to 16 bytes. Turns out, 16-byte alignment makes a lot of sense even if we don't use SIMD - stp (two 8-byte stores) benefits from it as well.

Standalone benchmark (for simpler tests)

Apple M2 Max (also tested on Ampere):

| Method | len | Mean | Ratio |
|----------- |----- |-----------:|------:|-
| Clear_Base | 1 | 1.199 ns | 1.00 |
| Clear_PR | 1 | 1.186 ns | 0.99 |
| | | | |
| Clear_Base | 5 | 1.215 ns | 1.00 |
| Clear_PR | 5 | 1.215 ns | 1.00 |
| | | | |
| Clear_Base | 8 | 1.617 ns | 1.00 |
| Clear_PR | 8 | 1.269 ns | 0.78 |
| | | | |
| Clear_Base | 16 | 2.405 ns | 1.00 |
| Clear_PR | 16 | 2.061 ns | 0.86 |
| | | | |
| Clear_Base | 64 | 8.727 ns | 1.00 |
| Clear_PR | 64 | 5.415 ns | 0.62 |
| | | | |
| Clear_Base | 512 | 70.081 ns | 1.00 |
| Clear_PR | 512 | 35.482 ns | 0.51 |
| | | | |
| Clear_Base | 1024 | 140.080 ns | 1.00 |
| Clear_PR | 1024 | 72.254 ns | 0.52 |
| | | | |
| Clear_Base | 4096 | 559.551 ns | 1.00 |
| Clear_PR | 4096 | 333.439 ns | 0.60 |

Ryzen 7950X:

| Method | len | Mean | Ratio |
|----------- |----- |------------:|------:|-
| Clear_Base | 1 | 0.3854 ns | 1.00 |
| Clear_PR | 1 | 0.3798 ns | 0.99 |
| | | | |
| Clear_Base | 5 | 0.5310 ns | 1.00 |
| Clear_PR | 5 | 0.3907 ns | 0.74 |
| | | | |
| Clear_Base | 8 | 0.8995 ns | 1.00 |
| Clear_PR | 8 | 0.5264 ns | 0.59 |
| | | | |
| Clear_Base | 16 | 1.4398 ns | 1.00 |
| Clear_PR | 16 | 1.2039 ns | 0.84 |
| | | | |
| Clear_Base | 64 | 5.1603 ns | 1.00 |
| Clear_PR | 64 | 5.1023 ns | 0.99 |
| | | | |
| Clear_Base | 512 | 46.0601 ns | 1.00 |
| Clear_PR | 512 | 45.9100 ns | 1.00 |
| | | | |
| Clear_Base | 1024 | 94.9471 ns | 1.00 |
| Clear_PR | 1024 | 94.7812 ns | 1.00 |
| | | | |
| Clear_Base | 4096 | 380.4915 ns | 1.00 |
| Clear_PR | 4096 | 375.3246 ns | 0.99 |

For X64, it slightly improves small inputs. It seems like if we pin the input and use SIMD we can see >2X improvement for large inputs but I was not sure about doing pinning (from GC's point of view, atomicity is fine) here and left as is, cc @jkotas

Author:EgorBo
Assignees:EgorBo
Labels:

area-System.Memory

Milestone:-

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.

This works great for microbenchmarks, but it works poorly for real work loads where the jump target is poorly predicated. We used to have switch table like this in managed memcopy originally, but it was later removed.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

This works great for microbenchmarks, but it works poorly for real work loads where the jump target is poorly predicated. We used to have switch table like this in managed memcopy originally, but it was later removed.

Should I revert to the previous shape? I was mostly interested in aligning to 16 bytes (it's the reason I am seeing nice results on arm64) and slightly different addressing mode in the main loop, to make it foldable into stp

@EgorBoEgorBoOct 11, 2023

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Btw, presumably, PGO can re-shuffle the switch (or convert to if-else) based on profile. However, in the real world it's likely has a mixed profile.

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.

Should I revert to the previous shape?

Yes, unless you can prove that the switch with jump table is better for real workloads.

For reference, dotnet/coreclr#9786 is the PR that got rid of the switch from memory copy. It went through extensive validation.

@tannergoodingtannergoodingOct 13, 2023

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.

This works great for microbenchmarks, but it works poorly for real work loads where the jump target is poorly predicated. We used to have switch table like this in managed memcopy originally, but it was later removed.

@jkotas, I'm not sure what you mean here, could you elaborate?

The hardware optimization manuals and underlying native memcpy algorithms across all 3 major implementations (MSVC, GCC, Clang; for x64 and Arm64) use/recommend a jump table explicitly for this reason. Because an unconditional and unpredictable branch, is better than a tight tree of conditional and still unpredictable branches.

The premise is that a fallthrough jump table allows a tight, unrolled version of handling for the trailing elements to exist. This then requires 1 unpredicted branch to decide which of the immediately following and equidistant switch cases needs to be jumped to. These cases are often already in the instruction/decode cache due to being in the same or following 32-byte code window.

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.

dotnet/coreclr#9786 was done by Intel engineers to address bottlenecks observed in real workloads. Yes, it goes contrary to the guidance you have quoted.

Possible explanation: Conditional jumps and indirect jumps use two different predictors. The relative performance of conditional jumps vs. indirect jump depends on relative pressure on each predictor. .NET code tends to have more indirect jumps than an average code. It means that the indirect jump predictor sees more pressure in .NET code and it is more profitable to depend on indirect jump predictor in .NET memcpy implementation.

@EgorBo

EgorBo commented Oct 11, 2023

Copy link
Copy Markdown
MemberAuthor

Codegen on arm64:

New codegen:
; Method Prog:ClearWithReferences2(byref,ulong) (FullOpts)G_M64195_IG01: ;; offset=0x0000 stp fp, lr,[sp, #-0x10]!mov fp,sp ;; size=8 bbWeight=8 PerfScore 12.00G_M64195_IG02: ;; offset=0x0008mov x2, x1cmp x2, #8 bhi G_M64195_IG12 ;; size=12 bbWeight=8 PerfScore 16.00G_M64195_IG03: ;; offset=0x0014cmp w2, #8 bhi G_M64195_IG12mov w1, w2 adr x2,[@RWD00] ldr w2,[x2, x1,LSL #2] adr x3,[G_M64195_IG02]add x2, x2, x3 br x2 ;; size=32 bbWeight=4 PerfScore 30.00G_M64195_IG04: ;; offset=0x0034str xzr,[x0] b G_M64195_IG15align[0 bytes for IG13]align[0 bytes]align[0 bytes]align[0 bytes] ;; size=8 bbWeight=0.50 PerfScore 1.00G_M64195_IG05: ;; offset=0x003C stp xzr, xzr,[x0] b G_M64195_IG15 ;; size=8 bbWeight=0.50 PerfScore 1.00G_M64195_IG06: ;; offset=0x0044 stp xzr, xzr,[x0]str xzr,[x0, #0x10] b G_M64195_IG15 ;; size=12 bbWeight=0.50 PerfScore 1.50G_M64195_IG07: ;; offset=0x0050 stp xzr, xzr,[x0] stp xzr, xzr,[x0, #0x10] b G_M64195_IG15 ;; size=12 bbWeight=0.50 PerfScore 1.50G_M64195_IG08: ;; offset=0x005C stp xzr, xzr,[x0] stp xzr, xzr,[x0, #0x10]str xzr,[x0, #0x20] b G_M64195_IG15 ;; size=16 bbWeight=0.50 PerfScore 2.00G_M64195_IG09: ;; offset=0x006C stp xzr, xzr,[x0] stp xzr, xzr,[x0, #0x10] stp xzr, xzr,[x0, #0x20] b G_M64195_IG15 ;; size=16 bbWeight=0.50 PerfScore 2.00G_M64195_IG10: ;; offset=0x007C stp xzr, xzr,[x0] stp xzr, xzr,[x0, #0x10] stp xzr, xzr,[x0, #0x20]str xzr,[x0, #0x30] b G_M64195_IG15 ;; size=20 bbWeight=0.50 PerfScore 2.50G_M64195_IG11: ;; offset=0x0090 stp xzr, xzr,[x0] stp xzr, xzr,[x0, #0x10] stp xzr, xzr,[x0, #0x20] stp xzr, xzr,[x0, #0x30] b G_M64195_IG15 ;; size=20 bbWeight=0.50 PerfScore 2.50G_M64195_IG12: ;; offset=0x00A4 lsr x2, x1, #3 ;; size=4 bbWeight=4 PerfScore 4.00G_M64195_IG13: ;; offset=0x00A8 stp xzr, xzr,[x0] stp xzr, xzr,[x0, #0x10] stp xzr, xzr,[x0, #0x20] stp xzr, xzr,[x0, #0x30]add x0, x0, #64sub x2, x2, #1 cbnz x2, G_M64195_IG13 ;; size=28 bbWeight=32 PerfScore 192.00G_M64195_IG14: ;; offset=0x00C4and x1, x1, #7 b G_M64195_IG02 ;; size=8 bbWeight=4 PerfScore 6.00G_M64195_IG15: ;; offset=0x00CC ldp fp, lr,[sp], #0x10ret lr ;; size=8 bbWeight=1 PerfScore 2.00RWD00 dd	000000C4h ; case G_M64195_IG15dd0000002Ch ; case G_M64195_IG04dd00000034h ; case G_M64195_IG05dd0000003Ch ; case G_M64195_IG06dd00000048h ; case G_M64195_IG07dd00000054h ; case G_M64195_IG08dd00000064h ; case G_M64195_IG09dd00000074h ; case G_M64195_IG10dd00000088h ; case G_M64195_IG11; Total bytes of code: 212
Previous codegen:
; Method Prog:ClearWithReferences(byref,ulong) (FullOpts)G_M45361_IG01: ;; offset=0x0000 stp fp, lr,[sp, #-0x10]!mov fp,sp ;; size=8 bbWeight=1 PerfScore 1.50G_M45361_IG02: ;; offset=0x0008cmp x1, #8 blo G_M45361_IG04align[0 bytes for IG03]align[0 bytes]align[0 bytes]align[0 bytes] ;; size=8 bbWeight=1 PerfScore 1.50G_M45361_IG03: ;; offset=0x0010lsl x2, x1, #3add x3, x0, x2str xzr,[x3, #-0x08]add x3, x0, x2str xzr,[x3, #-0x10]add x3, x0, x2str xzr,[x3, #-0x18]add x3, x0, x2str xzr,[x3, #-0x20]add x3, x0, x2str xzr,[x3, #-0x28]add x3, x0, x2str xzr,[x3, #-0x30]add x3, x0, x2str xzr,[x3, #-0x38]add x2, x0, x2str xzr,[x2, #-0x40]sub x1, x1, #8cmp x1, #8 bhs G_M45361_IG03 ;; size=80 bbWeight=4 PerfScore 60.00G_M45361_IG04: ;; offset=0x0060cmp x1, #4 bhs G_M45361_IG07 ;; size=8 bbWeight=1 PerfScore 1.50G_M45361_IG05: ;; offset=0x0068cmp x1, #2 bhs G_M45361_IG08 cbnz x1, G_M45361_IG09 ;; size=12 bbWeight=0.50 PerfScore 1.25G_M45361_IG06: ;; offset=0x0074 ldp fp, lr,[sp], #0x10ret lr ;; size=8 bbWeight=0.50 PerfScore 1.00G_M45361_IG07: ;; offset=0x007C stp xzr, xzr,[x0, #0x10]lsl x2, x1, #3add x3, x0, x2str xzr,[x3, #-0x18]add x2, x0, x2str xzr,[x2, #-0x10] ;; size=24 bbWeight=0.50 PerfScore 2.50G_M45361_IG08: ;; offset=0x0094str xzr,[x0, #0x08]lsl x2, x1, #3add x1, x0, x2str xzr,[x1, #-0x08] ;; size=16 bbWeight=0.50 PerfScore 1.75G_M45361_IG09: ;; offset=0x00A4str xzr,[x0] ;; size=4 bbWeight=0.50 PerfScore 0.50G_M45361_IG10: ;; offset=0x00A8 ldp fp, lr,[sp], #0x10ret lr ;; size=8 bbWeight=0.50 PerfScore 1.00; Total bytes of code: 176

I can tune JIT to fold

stp xzr, xzr, [x0]
stp xzr, xzr, [x0, #..]

to SIMD in the newly added LowerIndirCoelsceStore, but it seems to be fast as is.

@EgorBo
EgorBo marked this pull request as draft October 11, 2023 22:59
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.cs Outdated
@EgorBo

EgorBo commented Oct 13, 2023

Copy link
Copy Markdown
MemberAuthor

I have mixed results with this change, I'll wait for fixes for #93382 (needed for the remainder) and #76067 (comment) (needed for the main loop) and try again after

@ghostghost locked as resolved and limited conversation to collaborators Nov 12, 2023
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@EgorBo@jkotas@tannergooding
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Improve SpanHelpers.ClearWithReferences for arm64 - #93346

Closed
EgorBo wants to merge 6 commits into
dotnet:mainfrom
EgorBo:opt-clear-with-ref
Closed

Improve SpanHelpers.ClearWithReferences for arm64#93346
EgorBo wants to merge 6 commits into
dotnet:mainfrom
EgorBo:opt-clear-with-ref

Conversation

@EgorBo

@EgorBoEgorBo commented Oct 11, 2023

Copy link
Copy Markdown
Member

Follow up to #93214, now for Span<GC>.Clear();

I noticed that the existing implementation is slightly sub-optimal on arm: it's relying on complex addressing modes + is not aligned to 16 bytes. Turns out, 16-byte alignment makes a lot of sense even if we don't use SIMD - stp (two 8-byte stores) benefits from it as well.

Standalone benchmark (for simpler tests)

Apple M2 Max (also tested on Ampere):

| Method | len | Mean | Ratio |
|----------- |----- |-----------:|------:|-
| Clear_Base | 1 | 1.199 ns | 1.00 |
| Clear_PR | 1 | 1.186 ns | 0.99 |
| | | | |
| Clear_Base | 5 | 1.215 ns | 1.00 |
| Clear_PR | 5 | 1.215 ns | 1.00 |
| | | | |
| Clear_Base | 8 | 1.617 ns | 1.00 |
| Clear_PR | 8 | 1.269 ns | 0.78 |
| | | | |
| Clear_Base | 16 | 2.405 ns | 1.00 |
| Clear_PR | 16 | 2.061 ns | 0.86 |
| | | | |
| Clear_Base | 64 | 8.727 ns | 1.00 |
| Clear_PR | 64 | 5.415 ns | 0.62 |
| | | | |
| Clear_Base | 512 | 70.081 ns | 1.00 |
| Clear_PR | 512 | 35.482 ns | 0.51 |
| | | | |
| Clear_Base | 1024 | 140.080 ns | 1.00 |
| Clear_PR | 1024 | 72.254 ns | 0.52 |
| | | | |
| Clear_Base | 4096 | 559.551 ns | 1.00 |
| Clear_PR | 4096 | 333.439 ns | 0.60 |

Ryzen 7950X:

| Method | len | Mean | Ratio |
|----------- |----- |------------:|------:|-
| Clear_Base | 1 | 0.3854 ns | 1.00 |
| Clear_PR | 1 | 0.3798 ns | 0.99 |
| | | | |
| Clear_Base | 5 | 0.5310 ns | 1.00 |
| Clear_PR | 5 | 0.3907 ns | 0.74 |
| | | | |
| Clear_Base | 8 | 0.8995 ns | 1.00 |
| Clear_PR | 8 | 0.5264 ns | 0.59 |
| | | | |
| Clear_Base | 16 | 1.4398 ns | 1.00 |
| Clear_PR | 16 | 1.2039 ns | 0.84 |
| | | | |
| Clear_Base | 64 | 5.1603 ns | 1.00 |
| Clear_PR | 64 | 5.1023 ns | 0.99 |
| | | | |
| Clear_Base | 512 | 46.0601 ns | 1.00 |
| Clear_PR | 512 | 45.9100 ns | 1.00 |
| | | | |
| Clear_Base | 1024 | 94.9471 ns | 1.00 |
| Clear_PR | 1024 | 94.7812 ns | 1.00 |
| | | | |
| Clear_Base | 4096 | 380.4915 ns | 1.00 |
| Clear_PR | 4096 | 375.3246 ns | 0.99 |

For X64, it slightly improves small inputs. It seems like if we pin the input and use SIMD we can see >2X improvement for large inputs but I was not sure about doing pinning (from GC's point of view, atomicity is fine) here and left as is, cc @jkotas

@ghostghost assigned EgorBoOct 11, 2023
@ghost

Copy link
Copy Markdown

Tagging subscribers to this area: @dotnet/area-system-memory
See info in area-owners.md if you want to be subscribed.

Issue Details

Follow up to #93214, now for Span<GC>.Clear();

I noticed that the existing implementation is slightly sub-optimal on arm: it's relying on complex addressing modes + is not aligned to 16 bytes. Turns out, 16-byte alignment makes a lot of sense even if we don't use SIMD - stp (two 8-byte stores) benefits from it as well.

Standalone benchmark (for simpler tests)

Apple M2 Max (also tested on Ampere):

| Method | len | Mean | Ratio |
|----------- |----- |-----------:|------:|-
| Clear_Base | 1 | 1.199 ns | 1.00 |
| Clear_PR | 1 | 1.186 ns | 0.99 |
| | | | |
| Clear_Base | 5 | 1.215 ns | 1.00 |
| Clear_PR | 5 | 1.215 ns | 1.00 |
| | | | |
| Clear_Base | 8 | 1.617 ns | 1.00 |
| Clear_PR | 8 | 1.269 ns | 0.78 |
| | | | |
| Clear_Base | 16 | 2.405 ns | 1.00 |
| Clear_PR | 16 | 2.061 ns | 0.86 |
| | | | |
| Clear_Base | 64 | 8.727 ns | 1.00 |
| Clear_PR | 64 | 5.415 ns | 0.62 |
| | | | |
| Clear_Base | 512 | 70.081 ns | 1.00 |
| Clear_PR | 512 | 35.482 ns | 0.51 |
| | | | |
| Clear_Base | 1024 | 140.080 ns | 1.00 |
| Clear_PR | 1024 | 72.254 ns | 0.52 |
| | | | |
| Clear_Base | 4096 | 559.551 ns | 1.00 |
| Clear_PR | 4096 | 333.439 ns | 0.60 |

Ryzen 7950X:

| Method | len | Mean | Ratio |
|----------- |----- |------------:|------:|-
| Clear_Base | 1 | 0.3854 ns | 1.00 |
| Clear_PR | 1 | 0.3798 ns | 0.99 |
| | | | |
| Clear_Base | 5 | 0.5310 ns | 1.00 |
| Clear_PR | 5 | 0.3907 ns | 0.74 |
| | | | |
| Clear_Base | 8 | 0.8995 ns | 1.00 |
| Clear_PR | 8 | 0.5264 ns | 0.59 |
| | | | |
| Clear_Base | 16 | 1.4398 ns | 1.00 |
| Clear_PR | 16 | 1.2039 ns | 0.84 |
| | | | |
| Clear_Base | 64 | 5.1603 ns | 1.00 |
| Clear_PR | 64 | 5.1023 ns | 0.99 |
| | | | |
| Clear_Base | 512 | 46.0601 ns | 1.00 |
| Clear_PR | 512 | 45.9100 ns | 1.00 |
| | | | |
| Clear_Base | 1024 | 94.9471 ns | 1.00 |
| Clear_PR | 1024 | 94.7812 ns | 1.00 |
| | | | |
| Clear_Base | 4096 | 380.4915 ns | 1.00 |
| Clear_PR | 4096 | 375.3246 ns | 0.99 |

For X64, it slightly improves small inputs. It seems like if we pin the input and use SIMD we can see >2X improvement for large inputs but I was not sure about doing pinning (from GC's point of view, atomicity is fine) here and left as is, cc @jkotas

Author:EgorBo
Assignees:EgorBo
Labels:

area-System.Memory

Milestone:-

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.

This works great for microbenchmarks, but it works poorly for real work loads where the jump target is poorly predicated. We used to have switch table like this in managed memcopy originally, but it was later removed.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

This works great for microbenchmarks, but it works poorly for real work loads where the jump target is poorly predicated. We used to have switch table like this in managed memcopy originally, but it was later removed.

Should I revert to the previous shape? I was mostly interested in aligning to 16 bytes (it's the reason I am seeing nice results on arm64) and slightly different addressing mode in the main loop, to make it foldable into stp

@EgorBoEgorBoOct 11, 2023

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Btw, presumably, PGO can re-shuffle the switch (or convert to if-else) based on profile. However, in the real world it's likely has a mixed profile.

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.

Should I revert to the previous shape?

Yes, unless you can prove that the switch with jump table is better for real workloads.

For reference, dotnet/coreclr#9786 is the PR that got rid of the switch from memory copy. It went through extensive validation.

@tannergoodingtannergoodingOct 13, 2023

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.

This works great for microbenchmarks, but it works poorly for real work loads where the jump target is poorly predicated. We used to have switch table like this in managed memcopy originally, but it was later removed.

@jkotas, I'm not sure what you mean here, could you elaborate?

The hardware optimization manuals and underlying native memcpy algorithms across all 3 major implementations (MSVC, GCC, Clang; for x64 and Arm64) use/recommend a jump table explicitly for this reason. Because an unconditional and unpredictable branch, is better than a tight tree of conditional and still unpredictable branches.

The premise is that a fallthrough jump table allows a tight, unrolled version of handling for the trailing elements to exist. This then requires 1 unpredicted branch to decide which of the immediately following and equidistant switch cases needs to be jumped to. These cases are often already in the instruction/decode cache due to being in the same or following 32-byte code window.

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.

dotnet/coreclr#9786 was done by Intel engineers to address bottlenecks observed in real workloads. Yes, it goes contrary to the guidance you have quoted.

Possible explanation: Conditional jumps and indirect jumps use two different predictors. The relative performance of conditional jumps vs. indirect jump depends on relative pressure on each predictor. .NET code tends to have more indirect jumps than an average code. It means that the indirect jump predictor sees more pressure in .NET code and it is more profitable to depend on indirect jump predictor in .NET memcpy implementation.

@EgorBo

EgorBo commented Oct 11, 2023

Copy link
Copy Markdown
MemberAuthor

Codegen on arm64:

New codegen:
; Method Prog:ClearWithReferences2(byref,ulong) (FullOpts)G_M64195_IG01: ;; offset=0x0000 stp fp, lr,[sp, #-0x10]!mov fp,sp ;; size=8 bbWeight=8 PerfScore 12.00G_M64195_IG02: ;; offset=0x0008mov x2, x1cmp x2, #8 bhi G_M64195_IG12 ;; size=12 bbWeight=8 PerfScore 16.00G_M64195_IG03: ;; offset=0x0014cmp w2, #8 bhi G_M64195_IG12mov w1, w2 adr x2,[@RWD00] ldr w2,[x2, x1,LSL #2] adr x3,[G_M64195_IG02]add x2, x2, x3 br x2 ;; size=32 bbWeight=4 PerfScore 30.00G_M64195_IG04: ;; offset=0x0034str xzr,[x0] b G_M64195_IG15align[0 bytes for IG13]align[0 bytes]align[0 bytes]align[0 bytes] ;; size=8 bbWeight=0.50 PerfScore 1.00G_M64195_IG05: ;; offset=0x003C stp xzr, xzr,[x0] b G_M64195_IG15 ;; size=8 bbWeight=0.50 PerfScore 1.00G_M64195_IG06: ;; offset=0x0044 stp xzr, xzr,[x0]str xzr,[x0, #0x10] b G_M64195_IG15 ;; size=12 bbWeight=0.50 PerfScore 1.50G_M64195_IG07: ;; offset=0x0050 stp xzr, xzr,[x0] stp xzr, xzr,[x0, #0x10] b G_M64195_IG15 ;; size=12 bbWeight=0.50 PerfScore 1.50G_M64195_IG08: ;; offset=0x005C stp xzr, xzr,[x0] stp xzr, xzr,[x0, #0x10]str xzr,[x0, #0x20] b G_M64195_IG15 ;; size=16 bbWeight=0.50 PerfScore 2.00G_M64195_IG09: ;; offset=0x006C stp xzr, xzr,[x0] stp xzr, xzr,[x0, #0x10] stp xzr, xzr,[x0, #0x20] b G_M64195_IG15 ;; size=16 bbWeight=0.50 PerfScore 2.00G_M64195_IG10: ;; offset=0x007C stp xzr, xzr,[x0] stp xzr, xzr,[x0, #0x10] stp xzr, xzr,[x0, #0x20]str xzr,[x0, #0x30] b G_M64195_IG15 ;; size=20 bbWeight=0.50 PerfScore 2.50G_M64195_IG11: ;; offset=0x0090 stp xzr, xzr,[x0] stp xzr, xzr,[x0, #0x10] stp xzr, xzr,[x0, #0x20] stp xzr, xzr,[x0, #0x30] b G_M64195_IG15 ;; size=20 bbWeight=0.50 PerfScore 2.50G_M64195_IG12: ;; offset=0x00A4 lsr x2, x1, #3 ;; size=4 bbWeight=4 PerfScore 4.00G_M64195_IG13: ;; offset=0x00A8 stp xzr, xzr,[x0] stp xzr, xzr,[x0, #0x10] stp xzr, xzr,[x0, #0x20] stp xzr, xzr,[x0, #0x30]add x0, x0, #64sub x2, x2, #1 cbnz x2, G_M64195_IG13 ;; size=28 bbWeight=32 PerfScore 192.00G_M64195_IG14: ;; offset=0x00C4and x1, x1, #7 b G_M64195_IG02 ;; size=8 bbWeight=4 PerfScore 6.00G_M64195_IG15: ;; offset=0x00CC ldp fp, lr,[sp], #0x10ret lr ;; size=8 bbWeight=1 PerfScore 2.00RWD00 dd	000000C4h ; case G_M64195_IG15dd0000002Ch ; case G_M64195_IG04dd00000034h ; case G_M64195_IG05dd0000003Ch ; case G_M64195_IG06dd00000048h ; case G_M64195_IG07dd00000054h ; case G_M64195_IG08dd00000064h ; case G_M64195_IG09dd00000074h ; case G_M64195_IG10dd00000088h ; case G_M64195_IG11; Total bytes of code: 212
Previous codegen:
; Method Prog:ClearWithReferences(byref,ulong) (FullOpts)G_M45361_IG01: ;; offset=0x0000 stp fp, lr,[sp, #-0x10]!mov fp,sp ;; size=8 bbWeight=1 PerfScore 1.50G_M45361_IG02: ;; offset=0x0008cmp x1, #8 blo G_M45361_IG04align[0 bytes for IG03]align[0 bytes]align[0 bytes]align[0 bytes] ;; size=8 bbWeight=1 PerfScore 1.50G_M45361_IG03: ;; offset=0x0010lsl x2, x1, #3add x3, x0, x2str xzr,[x3, #-0x08]add x3, x0, x2str xzr,[x3, #-0x10]add x3, x0, x2str xzr,[x3, #-0x18]add x3, x0, x2str xzr,[x3, #-0x20]add x3, x0, x2str xzr,[x3, #-0x28]add x3, x0, x2str xzr,[x3, #-0x30]add x3, x0, x2str xzr,[x3, #-0x38]add x2, x0, x2str xzr,[x2, #-0x40]sub x1, x1, #8cmp x1, #8 bhs G_M45361_IG03 ;; size=80 bbWeight=4 PerfScore 60.00G_M45361_IG04: ;; offset=0x0060cmp x1, #4 bhs G_M45361_IG07 ;; size=8 bbWeight=1 PerfScore 1.50G_M45361_IG05: ;; offset=0x0068cmp x1, #2 bhs G_M45361_IG08 cbnz x1, G_M45361_IG09 ;; size=12 bbWeight=0.50 PerfScore 1.25G_M45361_IG06: ;; offset=0x0074 ldp fp, lr,[sp], #0x10ret lr ;; size=8 bbWeight=0.50 PerfScore 1.00G_M45361_IG07: ;; offset=0x007C stp xzr, xzr,[x0, #0x10]lsl x2, x1, #3add x3, x0, x2str xzr,[x3, #-0x18]add x2, x0, x2str xzr,[x2, #-0x10] ;; size=24 bbWeight=0.50 PerfScore 2.50G_M45361_IG08: ;; offset=0x0094str xzr,[x0, #0x08]lsl x2, x1, #3add x1, x0, x2str xzr,[x1, #-0x08] ;; size=16 bbWeight=0.50 PerfScore 1.75G_M45361_IG09: ;; offset=0x00A4str xzr,[x0] ;; size=4 bbWeight=0.50 PerfScore 0.50G_M45361_IG10: ;; offset=0x00A8 ldp fp, lr,[sp], #0x10ret lr ;; size=8 bbWeight=0.50 PerfScore 1.00; Total bytes of code: 176

I can tune JIT to fold

stp xzr, xzr, [x0]
stp xzr, xzr, [x0, #..]

to SIMD in the newly added LowerIndirCoelsceStore, but it seems to be fast as is.

@EgorBo
EgorBo marked this pull request as draft October 11, 2023 22:59
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.cs Outdated
@EgorBo

EgorBo commented Oct 13, 2023

Copy link
Copy Markdown
MemberAuthor

I have mixed results with this change, I'll wait for fixes for #93382 (needed for the remainder) and #76067 (comment) (needed for the main loop) and try again after

@ghostghost locked as resolved and limited conversation to collaborators Nov 12, 2023
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

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

Improve SpanHelpers.ClearWithReferences for arm64 - #93346

Closed
EgorBo wants to merge 6 commits into
dotnet:mainfrom
EgorBo:opt-clear-with-ref
Closed

Improve SpanHelpers.ClearWithReferences for arm64#93346
EgorBo wants to merge 6 commits into
dotnet:mainfrom
EgorBo:opt-clear-with-ref

Conversation

@EgorBo

@EgorBoEgorBo commented Oct 11, 2023

Copy link
Copy Markdown
Member

Follow up to #93214, now for Span<GC>.Clear();

I noticed that the existing implementation is slightly sub-optimal on arm: it's relying on complex addressing modes + is not aligned to 16 bytes. Turns out, 16-byte alignment makes a lot of sense even if we don't use SIMD - stp (two 8-byte stores) benefits from it as well.

Standalone benchmark (for simpler tests)

Apple M2 Max (also tested on Ampere):

| Method | len | Mean | Ratio |
|----------- |----- |-----------:|------:|-
| Clear_Base | 1 | 1.199 ns | 1.00 |
| Clear_PR | 1 | 1.186 ns | 0.99 |
| | | | |
| Clear_Base | 5 | 1.215 ns | 1.00 |
| Clear_PR | 5 | 1.215 ns | 1.00 |
| | | | |
| Clear_Base | 8 | 1.617 ns | 1.00 |
| Clear_PR | 8 | 1.269 ns | 0.78 |
| | | | |
| Clear_Base | 16 | 2.405 ns | 1.00 |
| Clear_PR | 16 | 2.061 ns | 0.86 |
| | | | |
| Clear_Base | 64 | 8.727 ns | 1.00 |
| Clear_PR | 64 | 5.415 ns | 0.62 |
| | | | |
| Clear_Base | 512 | 70.081 ns | 1.00 |
| Clear_PR | 512 | 35.482 ns | 0.51 |
| | | | |
| Clear_Base | 1024 | 140.080 ns | 1.00 |
| Clear_PR | 1024 | 72.254 ns | 0.52 |
| | | | |
| Clear_Base | 4096 | 559.551 ns | 1.00 |
| Clear_PR | 4096 | 333.439 ns | 0.60 |

Ryzen 7950X:

| Method | len | Mean | Ratio |
|----------- |----- |------------:|------:|-
| Clear_Base | 1 | 0.3854 ns | 1.00 |
| Clear_PR | 1 | 0.3798 ns | 0.99 |
| | | | |
| Clear_Base | 5 | 0.5310 ns | 1.00 |
| Clear_PR | 5 | 0.3907 ns | 0.74 |
| | | | |
| Clear_Base | 8 | 0.8995 ns | 1.00 |
| Clear_PR | 8 | 0.5264 ns | 0.59 |
| | | | |
| Clear_Base | 16 | 1.4398 ns | 1.00 |
| Clear_PR | 16 | 1.2039 ns | 0.84 |
| | | | |
| Clear_Base | 64 | 5.1603 ns | 1.00 |
| Clear_PR | 64 | 5.1023 ns | 0.99 |
| | | | |
| Clear_Base | 512 | 46.0601 ns | 1.00 |
| Clear_PR | 512 | 45.9100 ns | 1.00 |
| | | | |
| Clear_Base | 1024 | 94.9471 ns | 1.00 |
| Clear_PR | 1024 | 94.7812 ns | 1.00 |
| | | | |
| Clear_Base | 4096 | 380.4915 ns | 1.00 |
| Clear_PR | 4096 | 375.3246 ns | 0.99 |

For X64, it slightly improves small inputs. It seems like if we pin the input and use SIMD we can see >2X improvement for large inputs but I was not sure about doing pinning (from GC's point of view, atomicity is fine) here and left as is, cc @jkotas

@ghostghost assigned EgorBoOct 11, 2023
@ghost

Copy link
Copy Markdown

Tagging subscribers to this area: @dotnet/area-system-memory
See info in area-owners.md if you want to be subscribed.

Issue Details

Follow up to #93214, now for Span<GC>.Clear();

I noticed that the existing implementation is slightly sub-optimal on arm: it's relying on complex addressing modes + is not aligned to 16 bytes. Turns out, 16-byte alignment makes a lot of sense even if we don't use SIMD - stp (two 8-byte stores) benefits from it as well.

Standalone benchmark (for simpler tests)

Apple M2 Max (also tested on Ampere):

| Method | len | Mean | Ratio |
|----------- |----- |-----------:|------:|-
| Clear_Base | 1 | 1.199 ns | 1.00 |
| Clear_PR | 1 | 1.186 ns | 0.99 |
| | | | |
| Clear_Base | 5 | 1.215 ns | 1.00 |
| Clear_PR | 5 | 1.215 ns | 1.00 |
| | | | |
| Clear_Base | 8 | 1.617 ns | 1.00 |
| Clear_PR | 8 | 1.269 ns | 0.78 |
| | | | |
| Clear_Base | 16 | 2.405 ns | 1.00 |
| Clear_PR | 16 | 2.061 ns | 0.86 |
| | | | |
| Clear_Base | 64 | 8.727 ns | 1.00 |
| Clear_PR | 64 | 5.415 ns | 0.62 |
| | | | |
| Clear_Base | 512 | 70.081 ns | 1.00 |
| Clear_PR | 512 | 35.482 ns | 0.51 |
| | | | |
| Clear_Base | 1024 | 140.080 ns | 1.00 |
| Clear_PR | 1024 | 72.254 ns | 0.52 |
| | | | |
| Clear_Base | 4096 | 559.551 ns | 1.00 |
| Clear_PR | 4096 | 333.439 ns | 0.60 |

Ryzen 7950X:

| Method | len | Mean | Ratio |
|----------- |----- |------------:|------:|-
| Clear_Base | 1 | 0.3854 ns | 1.00 |
| Clear_PR | 1 | 0.3798 ns | 0.99 |
| | | | |
| Clear_Base | 5 | 0.5310 ns | 1.00 |
| Clear_PR | 5 | 0.3907 ns | 0.74 |
| | | | |
| Clear_Base | 8 | 0.8995 ns | 1.00 |
| Clear_PR | 8 | 0.5264 ns | 0.59 |
| | | | |
| Clear_Base | 16 | 1.4398 ns | 1.00 |
| Clear_PR | 16 | 1.2039 ns | 0.84 |
| | | | |
| Clear_Base | 64 | 5.1603 ns | 1.00 |
| Clear_PR | 64 | 5.1023 ns | 0.99 |
| | | | |
| Clear_Base | 512 | 46.0601 ns | 1.00 |
| Clear_PR | 512 | 45.9100 ns | 1.00 |
| | | | |
| Clear_Base | 1024 | 94.9471 ns | 1.00 |
| Clear_PR | 1024 | 94.7812 ns | 1.00 |
| | | | |
| Clear_Base | 4096 | 380.4915 ns | 1.00 |
| Clear_PR | 4096 | 375.3246 ns | 0.99 |

For X64, it slightly improves small inputs. It seems like if we pin the input and use SIMD we can see >2X improvement for large inputs but I was not sure about doing pinning (from GC's point of view, atomicity is fine) here and left as is, cc @jkotas

Author:EgorBo
Assignees:EgorBo
Labels:

area-System.Memory

Milestone:-

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.

This works great for microbenchmarks, but it works poorly for real work loads where the jump target is poorly predicated. We used to have switch table like this in managed memcopy originally, but it was later removed.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

This works great for microbenchmarks, but it works poorly for real work loads where the jump target is poorly predicated. We used to have switch table like this in managed memcopy originally, but it was later removed.

Should I revert to the previous shape? I was mostly interested in aligning to 16 bytes (it's the reason I am seeing nice results on arm64) and slightly different addressing mode in the main loop, to make it foldable into stp

@EgorBoEgorBoOct 11, 2023

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Btw, presumably, PGO can re-shuffle the switch (or convert to if-else) based on profile. However, in the real world it's likely has a mixed profile.

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.

Should I revert to the previous shape?

Yes, unless you can prove that the switch with jump table is better for real workloads.

For reference, dotnet/coreclr#9786 is the PR that got rid of the switch from memory copy. It went through extensive validation.

@tannergoodingtannergoodingOct 13, 2023

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.

This works great for microbenchmarks, but it works poorly for real work loads where the jump target is poorly predicated. We used to have switch table like this in managed memcopy originally, but it was later removed.

@jkotas, I'm not sure what you mean here, could you elaborate?

The hardware optimization manuals and underlying native memcpy algorithms across all 3 major implementations (MSVC, GCC, Clang; for x64 and Arm64) use/recommend a jump table explicitly for this reason. Because an unconditional and unpredictable branch, is better than a tight tree of conditional and still unpredictable branches.

The premise is that a fallthrough jump table allows a tight, unrolled version of handling for the trailing elements to exist. This then requires 1 unpredicted branch to decide which of the immediately following and equidistant switch cases needs to be jumped to. These cases are often already in the instruction/decode cache due to being in the same or following 32-byte code window.

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.

dotnet/coreclr#9786 was done by Intel engineers to address bottlenecks observed in real workloads. Yes, it goes contrary to the guidance you have quoted.

Possible explanation: Conditional jumps and indirect jumps use two different predictors. The relative performance of conditional jumps vs. indirect jump depends on relative pressure on each predictor. .NET code tends to have more indirect jumps than an average code. It means that the indirect jump predictor sees more pressure in .NET code and it is more profitable to depend on indirect jump predictor in .NET memcpy implementation.

@EgorBo

EgorBo commented Oct 11, 2023

Copy link
Copy Markdown
MemberAuthor

Codegen on arm64:

New codegen:
; Method Prog:ClearWithReferences2(byref,ulong) (FullOpts)G_M64195_IG01: ;; offset=0x0000 stp fp, lr,[sp, #-0x10]!mov fp,sp ;; size=8 bbWeight=8 PerfScore 12.00G_M64195_IG02: ;; offset=0x0008mov x2, x1cmp x2, #8 bhi G_M64195_IG12 ;; size=12 bbWeight=8 PerfScore 16.00G_M64195_IG03: ;; offset=0x0014cmp w2, #8 bhi G_M64195_IG12mov w1, w2 adr x2,[@RWD00] ldr w2,[x2, x1,LSL #2] adr x3,[G_M64195_IG02]add x2, x2, x3 br x2 ;; size=32 bbWeight=4 PerfScore 30.00G_M64195_IG04: ;; offset=0x0034str xzr,[x0] b G_M64195_IG15align[0 bytes for IG13]align[0 bytes]align[0 bytes]align[0 bytes] ;; size=8 bbWeight=0.50 PerfScore 1.00G_M64195_IG05: ;; offset=0x003C stp xzr, xzr,[x0] b G_M64195_IG15 ;; size=8 bbWeight=0.50 PerfScore 1.00G_M64195_IG06: ;; offset=0x0044 stp xzr, xzr,[x0]str xzr,[x0, #0x10] b G_M64195_IG15 ;; size=12 bbWeight=0.50 PerfScore 1.50G_M64195_IG07: ;; offset=0x0050 stp xzr, xzr,[x0] stp xzr, xzr,[x0, #0x10] b G_M64195_IG15 ;; size=12 bbWeight=0.50 PerfScore 1.50G_M64195_IG08: ;; offset=0x005C stp xzr, xzr,[x0] stp xzr, xzr,[x0, #0x10]str xzr,[x0, #0x20] b G_M64195_IG15 ;; size=16 bbWeight=0.50 PerfScore 2.00G_M64195_IG09: ;; offset=0x006C stp xzr, xzr,[x0] stp xzr, xzr,[x0, #0x10] stp xzr, xzr,[x0, #0x20] b G_M64195_IG15 ;; size=16 bbWeight=0.50 PerfScore 2.00G_M64195_IG10: ;; offset=0x007C stp xzr, xzr,[x0] stp xzr, xzr,[x0, #0x10] stp xzr, xzr,[x0, #0x20]str xzr,[x0, #0x30] b G_M64195_IG15 ;; size=20 bbWeight=0.50 PerfScore 2.50G_M64195_IG11: ;; offset=0x0090 stp xzr, xzr,[x0] stp xzr, xzr,[x0, #0x10] stp xzr, xzr,[x0, #0x20] stp xzr, xzr,[x0, #0x30] b G_M64195_IG15 ;; size=20 bbWeight=0.50 PerfScore 2.50G_M64195_IG12: ;; offset=0x00A4 lsr x2, x1, #3 ;; size=4 bbWeight=4 PerfScore 4.00G_M64195_IG13: ;; offset=0x00A8 stp xzr, xzr,[x0] stp xzr, xzr,[x0, #0x10] stp xzr, xzr,[x0, #0x20] stp xzr, xzr,[x0, #0x30]add x0, x0, #64sub x2, x2, #1 cbnz x2, G_M64195_IG13 ;; size=28 bbWeight=32 PerfScore 192.00G_M64195_IG14: ;; offset=0x00C4and x1, x1, #7 b G_M64195_IG02 ;; size=8 bbWeight=4 PerfScore 6.00G_M64195_IG15: ;; offset=0x00CC ldp fp, lr,[sp], #0x10ret lr ;; size=8 bbWeight=1 PerfScore 2.00RWD00 dd	000000C4h ; case G_M64195_IG15dd0000002Ch ; case G_M64195_IG04dd00000034h ; case G_M64195_IG05dd0000003Ch ; case G_M64195_IG06dd00000048h ; case G_M64195_IG07dd00000054h ; case G_M64195_IG08dd00000064h ; case G_M64195_IG09dd00000074h ; case G_M64195_IG10dd00000088h ; case G_M64195_IG11; Total bytes of code: 212
Previous codegen:
; Method Prog:ClearWithReferences(byref,ulong) (FullOpts)G_M45361_IG01: ;; offset=0x0000 stp fp, lr,[sp, #-0x10]!mov fp,sp ;; size=8 bbWeight=1 PerfScore 1.50G_M45361_IG02: ;; offset=0x0008cmp x1, #8 blo G_M45361_IG04align[0 bytes for IG03]align[0 bytes]align[0 bytes]align[0 bytes] ;; size=8 bbWeight=1 PerfScore 1.50G_M45361_IG03: ;; offset=0x0010lsl x2, x1, #3add x3, x0, x2str xzr,[x3, #-0x08]add x3, x0, x2str xzr,[x3, #-0x10]add x3, x0, x2str xzr,[x3, #-0x18]add x3, x0, x2str xzr,[x3, #-0x20]add x3, x0, x2str xzr,[x3, #-0x28]add x3, x0, x2str xzr,[x3, #-0x30]add x3, x0, x2str xzr,[x3, #-0x38]add x2, x0, x2str xzr,[x2, #-0x40]sub x1, x1, #8cmp x1, #8 bhs G_M45361_IG03 ;; size=80 bbWeight=4 PerfScore 60.00G_M45361_IG04: ;; offset=0x0060cmp x1, #4 bhs G_M45361_IG07 ;; size=8 bbWeight=1 PerfScore 1.50G_M45361_IG05: ;; offset=0x0068cmp x1, #2 bhs G_M45361_IG08 cbnz x1, G_M45361_IG09 ;; size=12 bbWeight=0.50 PerfScore 1.25G_M45361_IG06: ;; offset=0x0074 ldp fp, lr,[sp], #0x10ret lr ;; size=8 bbWeight=0.50 PerfScore 1.00G_M45361_IG07: ;; offset=0x007C stp xzr, xzr,[x0, #0x10]lsl x2, x1, #3add x3, x0, x2str xzr,[x3, #-0x18]add x2, x0, x2str xzr,[x2, #-0x10] ;; size=24 bbWeight=0.50 PerfScore 2.50G_M45361_IG08: ;; offset=0x0094str xzr,[x0, #0x08]lsl x2, x1, #3add x1, x0, x2str xzr,[x1, #-0x08] ;; size=16 bbWeight=0.50 PerfScore 1.75G_M45361_IG09: ;; offset=0x00A4str xzr,[x0] ;; size=4 bbWeight=0.50 PerfScore 0.50G_M45361_IG10: ;; offset=0x00A8 ldp fp, lr,[sp], #0x10ret lr ;; size=8 bbWeight=0.50 PerfScore 1.00; Total bytes of code: 176

I can tune JIT to fold

stp xzr, xzr, [x0]
stp xzr, xzr, [x0, #..]

to SIMD in the newly added LowerIndirCoelsceStore, but it seems to be fast as is.

@EgorBo
EgorBo marked this pull request as draft October 11, 2023 22:59
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.cs Outdated
@EgorBo

EgorBo commented Oct 13, 2023

Copy link
Copy Markdown
MemberAuthor

I have mixed results with this change, I'll wait for fixes for #93382 (needed for the remainder) and #76067 (comment) (needed for the main loop) and try again after

@ghostghost locked as resolved and limited conversation to collaborators Nov 12, 2023
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

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

Improve SpanHelpers.ClearWithReferences for arm64 - #93346

Closed
EgorBo wants to merge 6 commits into
dotnet:mainfrom
EgorBo:opt-clear-with-ref
Closed

Improve SpanHelpers.ClearWithReferences for arm64#93346
EgorBo wants to merge 6 commits into
dotnet:mainfrom
EgorBo:opt-clear-with-ref

Conversation

@EgorBo

@EgorBoEgorBo commented Oct 11, 2023

Copy link
Copy Markdown
Member

Follow up to #93214, now for Span<GC>.Clear();

I noticed that the existing implementation is slightly sub-optimal on arm: it's relying on complex addressing modes + is not aligned to 16 bytes. Turns out, 16-byte alignment makes a lot of sense even if we don't use SIMD - stp (two 8-byte stores) benefits from it as well.

Standalone benchmark (for simpler tests)

Apple M2 Max (also tested on Ampere):

| Method | len | Mean | Ratio |
|----------- |----- |-----------:|------:|-
| Clear_Base | 1 | 1.199 ns | 1.00 |
| Clear_PR | 1 | 1.186 ns | 0.99 |
| | | | |
| Clear_Base | 5 | 1.215 ns | 1.00 |
| Clear_PR | 5 | 1.215 ns | 1.00 |
| | | | |
| Clear_Base | 8 | 1.617 ns | 1.00 |
| Clear_PR | 8 | 1.269 ns | 0.78 |
| | | | |
| Clear_Base | 16 | 2.405 ns | 1.00 |
| Clear_PR | 16 | 2.061 ns | 0.86 |
| | | | |
| Clear_Base | 64 | 8.727 ns | 1.00 |
| Clear_PR | 64 | 5.415 ns | 0.62 |
| | | | |
| Clear_Base | 512 | 70.081 ns | 1.00 |
| Clear_PR | 512 | 35.482 ns | 0.51 |
| | | | |
| Clear_Base | 1024 | 140.080 ns | 1.00 |
| Clear_PR | 1024 | 72.254 ns | 0.52 |
| | | | |
| Clear_Base | 4096 | 559.551 ns | 1.00 |
| Clear_PR | 4096 | 333.439 ns | 0.60 |

Ryzen 7950X:

| Method | len | Mean | Ratio |
|----------- |----- |------------:|------:|-
| Clear_Base | 1 | 0.3854 ns | 1.00 |
| Clear_PR | 1 | 0.3798 ns | 0.99 |
| | | | |
| Clear_Base | 5 | 0.5310 ns | 1.00 |
| Clear_PR | 5 | 0.3907 ns | 0.74 |
| | | | |
| Clear_Base | 8 | 0.8995 ns | 1.00 |
| Clear_PR | 8 | 0.5264 ns | 0.59 |
| | | | |
| Clear_Base | 16 | 1.4398 ns | 1.00 |
| Clear_PR | 16 | 1.2039 ns | 0.84 |
| | | | |
| Clear_Base | 64 | 5.1603 ns | 1.00 |
| Clear_PR | 64 | 5.1023 ns | 0.99 |
| | | | |
| Clear_Base | 512 | 46.0601 ns | 1.00 |
| Clear_PR | 512 | 45.9100 ns | 1.00 |
| | | | |
| Clear_Base | 1024 | 94.9471 ns | 1.00 |
| Clear_PR | 1024 | 94.7812 ns | 1.00 |
| | | | |
| Clear_Base | 4096 | 380.4915 ns | 1.00 |
| Clear_PR | 4096 | 375.3246 ns | 0.99 |

For X64, it slightly improves small inputs. It seems like if we pin the input and use SIMD we can see >2X improvement for large inputs but I was not sure about doing pinning (from GC's point of view, atomicity is fine) here and left as is, cc @jkotas

@ghostghost assigned EgorBoOct 11, 2023
@ghost

Copy link
Copy Markdown

Tagging subscribers to this area: @dotnet/area-system-memory
See info in area-owners.md if you want to be subscribed.

Issue Details

Follow up to #93214, now for Span<GC>.Clear();

I noticed that the existing implementation is slightly sub-optimal on arm: it's relying on complex addressing modes + is not aligned to 16 bytes. Turns out, 16-byte alignment makes a lot of sense even if we don't use SIMD - stp (two 8-byte stores) benefits from it as well.

Standalone benchmark (for simpler tests)

Apple M2 Max (also tested on Ampere):

| Method | len | Mean | Ratio |
|----------- |----- |-----------:|------:|-
| Clear_Base | 1 | 1.199 ns | 1.00 |
| Clear_PR | 1 | 1.186 ns | 0.99 |
| | | | |
| Clear_Base | 5 | 1.215 ns | 1.00 |
| Clear_PR | 5 | 1.215 ns | 1.00 |
| | | | |
| Clear_Base | 8 | 1.617 ns | 1.00 |
| Clear_PR | 8 | 1.269 ns | 0.78 |
| | | | |
| Clear_Base | 16 | 2.405 ns | 1.00 |
| Clear_PR | 16 | 2.061 ns | 0.86 |
| | | | |
| Clear_Base | 64 | 8.727 ns | 1.00 |
| Clear_PR | 64 | 5.415 ns | 0.62 |
| | | | |
| Clear_Base | 512 | 70.081 ns | 1.00 |
| Clear_PR | 512 | 35.482 ns | 0.51 |
| | | | |
| Clear_Base | 1024 | 140.080 ns | 1.00 |
| Clear_PR | 1024 | 72.254 ns | 0.52 |
| | | | |
| Clear_Base | 4096 | 559.551 ns | 1.00 |
| Clear_PR | 4096 | 333.439 ns | 0.60 |

Ryzen 7950X:

| Method | len | Mean | Ratio |
|----------- |----- |------------:|------:|-
| Clear_Base | 1 | 0.3854 ns | 1.00 |
| Clear_PR | 1 | 0.3798 ns | 0.99 |
| | | | |
| Clear_Base | 5 | 0.5310 ns | 1.00 |
| Clear_PR | 5 | 0.3907 ns | 0.74 |
| | | | |
| Clear_Base | 8 | 0.8995 ns | 1.00 |
| Clear_PR | 8 | 0.5264 ns | 0.59 |
| | | | |
| Clear_Base | 16 | 1.4398 ns | 1.00 |
| Clear_PR | 16 | 1.2039 ns | 0.84 |
| | | | |
| Clear_Base | 64 | 5.1603 ns | 1.00 |
| Clear_PR | 64 | 5.1023 ns | 0.99 |
| | | | |
| Clear_Base | 512 | 46.0601 ns | 1.00 |
| Clear_PR | 512 | 45.9100 ns | 1.00 |
| | | | |
| Clear_Base | 1024 | 94.9471 ns | 1.00 |
| Clear_PR | 1024 | 94.7812 ns | 1.00 |
| | | | |
| Clear_Base | 4096 | 380.4915 ns | 1.00 |
| Clear_PR | 4096 | 375.3246 ns | 0.99 |

For X64, it slightly improves small inputs. It seems like if we pin the input and use SIMD we can see >2X improvement for large inputs but I was not sure about doing pinning (from GC's point of view, atomicity is fine) here and left as is, cc @jkotas

Author:EgorBo
Assignees:EgorBo
Labels:

area-System.Memory

Milestone:-

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.

This works great for microbenchmarks, but it works poorly for real work loads where the jump target is poorly predicated. We used to have switch table like this in managed memcopy originally, but it was later removed.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

This works great for microbenchmarks, but it works poorly for real work loads where the jump target is poorly predicated. We used to have switch table like this in managed memcopy originally, but it was later removed.

Should I revert to the previous shape? I was mostly interested in aligning to 16 bytes (it's the reason I am seeing nice results on arm64) and slightly different addressing mode in the main loop, to make it foldable into stp

@EgorBoEgorBoOct 11, 2023

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Btw, presumably, PGO can re-shuffle the switch (or convert to if-else) based on profile. However, in the real world it's likely has a mixed profile.

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.

Should I revert to the previous shape?

Yes, unless you can prove that the switch with jump table is better for real workloads.

For reference, dotnet/coreclr#9786 is the PR that got rid of the switch from memory copy. It went through extensive validation.

@tannergoodingtannergoodingOct 13, 2023

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.

This works great for microbenchmarks, but it works poorly for real work loads where the jump target is poorly predicated. We used to have switch table like this in managed memcopy originally, but it was later removed.

@jkotas, I'm not sure what you mean here, could you elaborate?

The hardware optimization manuals and underlying native memcpy algorithms across all 3 major implementations (MSVC, GCC, Clang; for x64 and Arm64) use/recommend a jump table explicitly for this reason. Because an unconditional and unpredictable branch, is better than a tight tree of conditional and still unpredictable branches.

The premise is that a fallthrough jump table allows a tight, unrolled version of handling for the trailing elements to exist. This then requires 1 unpredicted branch to decide which of the immediately following and equidistant switch cases needs to be jumped to. These cases are often already in the instruction/decode cache due to being in the same or following 32-byte code window.

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.

dotnet/coreclr#9786 was done by Intel engineers to address bottlenecks observed in real workloads. Yes, it goes contrary to the guidance you have quoted.

Possible explanation: Conditional jumps and indirect jumps use two different predictors. The relative performance of conditional jumps vs. indirect jump depends on relative pressure on each predictor. .NET code tends to have more indirect jumps than an average code. It means that the indirect jump predictor sees more pressure in .NET code and it is more profitable to depend on indirect jump predictor in .NET memcpy implementation.

@EgorBo

EgorBo commented Oct 11, 2023

Copy link
Copy Markdown
MemberAuthor

Codegen on arm64:

New codegen:
; Method Prog:ClearWithReferences2(byref,ulong) (FullOpts)G_M64195_IG01: ;; offset=0x0000 stp fp, lr,[sp, #-0x10]!mov fp,sp ;; size=8 bbWeight=8 PerfScore 12.00G_M64195_IG02: ;; offset=0x0008mov x2, x1cmp x2, #8 bhi G_M64195_IG12 ;; size=12 bbWeight=8 PerfScore 16.00G_M64195_IG03: ;; offset=0x0014cmp w2, #8 bhi G_M64195_IG12mov w1, w2 adr x2,[@RWD00] ldr w2,[x2, x1,LSL #2] adr x3,[G_M64195_IG02]add x2, x2, x3 br x2 ;; size=32 bbWeight=4 PerfScore 30.00G_M64195_IG04: ;; offset=0x0034str xzr,[x0] b G_M64195_IG15align[0 bytes for IG13]align[0 bytes]align[0 bytes]align[0 bytes] ;; size=8 bbWeight=0.50 PerfScore 1.00G_M64195_IG05: ;; offset=0x003C stp xzr, xzr,[x0] b G_M64195_IG15 ;; size=8 bbWeight=0.50 PerfScore 1.00G_M64195_IG06: ;; offset=0x0044 stp xzr, xzr,[x0]str xzr,[x0, #0x10] b G_M64195_IG15 ;; size=12 bbWeight=0.50 PerfScore 1.50G_M64195_IG07: ;; offset=0x0050 stp xzr, xzr,[x0] stp xzr, xzr,[x0, #0x10] b G_M64195_IG15 ;; size=12 bbWeight=0.50 PerfScore 1.50G_M64195_IG08: ;; offset=0x005C stp xzr, xzr,[x0] stp xzr, xzr,[x0, #0x10]str xzr,[x0, #0x20] b G_M64195_IG15 ;; size=16 bbWeight=0.50 PerfScore 2.00G_M64195_IG09: ;; offset=0x006C stp xzr, xzr,[x0] stp xzr, xzr,[x0, #0x10] stp xzr, xzr,[x0, #0x20] b G_M64195_IG15 ;; size=16 bbWeight=0.50 PerfScore 2.00G_M64195_IG10: ;; offset=0x007C stp xzr, xzr,[x0] stp xzr, xzr,[x0, #0x10] stp xzr, xzr,[x0, #0x20]str xzr,[x0, #0x30] b G_M64195_IG15 ;; size=20 bbWeight=0.50 PerfScore 2.50G_M64195_IG11: ;; offset=0x0090 stp xzr, xzr,[x0] stp xzr, xzr,[x0, #0x10] stp xzr, xzr,[x0, #0x20] stp xzr, xzr,[x0, #0x30] b G_M64195_IG15 ;; size=20 bbWeight=0.50 PerfScore 2.50G_M64195_IG12: ;; offset=0x00A4 lsr x2, x1, #3 ;; size=4 bbWeight=4 PerfScore 4.00G_M64195_IG13: ;; offset=0x00A8 stp xzr, xzr,[x0] stp xzr, xzr,[x0, #0x10] stp xzr, xzr,[x0, #0x20] stp xzr, xzr,[x0, #0x30]add x0, x0, #64sub x2, x2, #1 cbnz x2, G_M64195_IG13 ;; size=28 bbWeight=32 PerfScore 192.00G_M64195_IG14: ;; offset=0x00C4and x1, x1, #7 b G_M64195_IG02 ;; size=8 bbWeight=4 PerfScore 6.00G_M64195_IG15: ;; offset=0x00CC ldp fp, lr,[sp], #0x10ret lr ;; size=8 bbWeight=1 PerfScore 2.00RWD00 dd	000000C4h ; case G_M64195_IG15dd0000002Ch ; case G_M64195_IG04dd00000034h ; case G_M64195_IG05dd0000003Ch ; case G_M64195_IG06dd00000048h ; case G_M64195_IG07dd00000054h ; case G_M64195_IG08dd00000064h ; case G_M64195_IG09dd00000074h ; case G_M64195_IG10dd00000088h ; case G_M64195_IG11; Total bytes of code: 212
Previous codegen:
; Method Prog:ClearWithReferences(byref,ulong) (FullOpts)G_M45361_IG01: ;; offset=0x0000 stp fp, lr,[sp, #-0x10]!mov fp,sp ;; size=8 bbWeight=1 PerfScore 1.50G_M45361_IG02: ;; offset=0x0008cmp x1, #8 blo G_M45361_IG04align[0 bytes for IG03]align[0 bytes]align[0 bytes]align[0 bytes] ;; size=8 bbWeight=1 PerfScore 1.50G_M45361_IG03: ;; offset=0x0010lsl x2, x1, #3add x3, x0, x2str xzr,[x3, #-0x08]add x3, x0, x2str xzr,[x3, #-0x10]add x3, x0, x2str xzr,[x3, #-0x18]add x3, x0, x2str xzr,[x3, #-0x20]add x3, x0, x2str xzr,[x3, #-0x28]add x3, x0, x2str xzr,[x3, #-0x30]add x3, x0, x2str xzr,[x3, #-0x38]add x2, x0, x2str xzr,[x2, #-0x40]sub x1, x1, #8cmp x1, #8 bhs G_M45361_IG03 ;; size=80 bbWeight=4 PerfScore 60.00G_M45361_IG04: ;; offset=0x0060cmp x1, #4 bhs G_M45361_IG07 ;; size=8 bbWeight=1 PerfScore 1.50G_M45361_IG05: ;; offset=0x0068cmp x1, #2 bhs G_M45361_IG08 cbnz x1, G_M45361_IG09 ;; size=12 bbWeight=0.50 PerfScore 1.25G_M45361_IG06: ;; offset=0x0074 ldp fp, lr,[sp], #0x10ret lr ;; size=8 bbWeight=0.50 PerfScore 1.00G_M45361_IG07: ;; offset=0x007C stp xzr, xzr,[x0, #0x10]lsl x2, x1, #3add x3, x0, x2str xzr,[x3, #-0x18]add x2, x0, x2str xzr,[x2, #-0x10] ;; size=24 bbWeight=0.50 PerfScore 2.50G_M45361_IG08: ;; offset=0x0094str xzr,[x0, #0x08]lsl x2, x1, #3add x1, x0, x2str xzr,[x1, #-0x08] ;; size=16 bbWeight=0.50 PerfScore 1.75G_M45361_IG09: ;; offset=0x00A4str xzr,[x0] ;; size=4 bbWeight=0.50 PerfScore 0.50G_M45361_IG10: ;; offset=0x00A8 ldp fp, lr,[sp], #0x10ret lr ;; size=8 bbWeight=0.50 PerfScore 1.00; Total bytes of code: 176

I can tune JIT to fold

stp xzr, xzr, [x0]
stp xzr, xzr, [x0, #..]

to SIMD in the newly added LowerIndirCoelsceStore, but it seems to be fast as is.

@EgorBo
EgorBo marked this pull request as draft October 11, 2023 22:59
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.cs Outdated
@EgorBo

EgorBo commented Oct 13, 2023

Copy link
Copy Markdown
MemberAuthor

I have mixed results with this change, I'll wait for fixes for #93382 (needed for the remainder) and #76067 (comment) (needed for the main loop) and try again after

@ghostghost locked as resolved and limited conversation to collaborators Nov 12, 2023
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@EgorBo@jkotas@tannergooding
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Improve SpanHelpers.ClearWithReferences for arm64 - #93346

Closed
EgorBo wants to merge 6 commits into
dotnet:mainfrom
EgorBo:opt-clear-with-ref
Closed

Improve SpanHelpers.ClearWithReferences for arm64#93346
EgorBo wants to merge 6 commits into
dotnet:mainfrom
EgorBo:opt-clear-with-ref

Conversation

@EgorBo

@EgorBoEgorBo commented Oct 11, 2023

Copy link
Copy Markdown
Member

Follow up to #93214, now for Span<GC>.Clear();

I noticed that the existing implementation is slightly sub-optimal on arm: it's relying on complex addressing modes + is not aligned to 16 bytes. Turns out, 16-byte alignment makes a lot of sense even if we don't use SIMD - stp (two 8-byte stores) benefits from it as well.

Standalone benchmark (for simpler tests)

Apple M2 Max (also tested on Ampere):

| Method | len | Mean | Ratio |
|----------- |----- |-----------:|------:|-
| Clear_Base | 1 | 1.199 ns | 1.00 |
| Clear_PR | 1 | 1.186 ns | 0.99 |
| | | | |
| Clear_Base | 5 | 1.215 ns | 1.00 |
| Clear_PR | 5 | 1.215 ns | 1.00 |
| | | | |
| Clear_Base | 8 | 1.617 ns | 1.00 |
| Clear_PR | 8 | 1.269 ns | 0.78 |
| | | | |
| Clear_Base | 16 | 2.405 ns | 1.00 |
| Clear_PR | 16 | 2.061 ns | 0.86 |
| | | | |
| Clear_Base | 64 | 8.727 ns | 1.00 |
| Clear_PR | 64 | 5.415 ns | 0.62 |
| | | | |
| Clear_Base | 512 | 70.081 ns | 1.00 |
| Clear_PR | 512 | 35.482 ns | 0.51 |
| | | | |
| Clear_Base | 1024 | 140.080 ns | 1.00 |
| Clear_PR | 1024 | 72.254 ns | 0.52 |
| | | | |
| Clear_Base | 4096 | 559.551 ns | 1.00 |
| Clear_PR | 4096 | 333.439 ns | 0.60 |

Ryzen 7950X:

| Method | len | Mean | Ratio |
|----------- |----- |------------:|------:|-
| Clear_Base | 1 | 0.3854 ns | 1.00 |
| Clear_PR | 1 | 0.3798 ns | 0.99 |
| | | | |
| Clear_Base | 5 | 0.5310 ns | 1.00 |
| Clear_PR | 5 | 0.3907 ns | 0.74 |
| | | | |
| Clear_Base | 8 | 0.8995 ns | 1.00 |
| Clear_PR | 8 | 0.5264 ns | 0.59 |
| | | | |
| Clear_Base | 16 | 1.4398 ns | 1.00 |
| Clear_PR | 16 | 1.2039 ns | 0.84 |
| | | | |
| Clear_Base | 64 | 5.1603 ns | 1.00 |
| Clear_PR | 64 | 5.1023 ns | 0.99 |
| | | | |
| Clear_Base | 512 | 46.0601 ns | 1.00 |
| Clear_PR | 512 | 45.9100 ns | 1.00 |
| | | | |
| Clear_Base | 1024 | 94.9471 ns | 1.00 |
| Clear_PR | 1024 | 94.7812 ns | 1.00 |
| | | | |
| Clear_Base | 4096 | 380.4915 ns | 1.00 |
| Clear_PR | 4096 | 375.3246 ns | 0.99 |

For X64, it slightly improves small inputs. It seems like if we pin the input and use SIMD we can see >2X improvement for large inputs but I was not sure about doing pinning (from GC's point of view, atomicity is fine) here and left as is, cc @jkotas

@ghostghost assigned EgorBoOct 11, 2023
@ghost

Copy link
Copy Markdown

Tagging subscribers to this area: @dotnet/area-system-memory
See info in area-owners.md if you want to be subscribed.

Issue Details

Follow up to #93214, now for Span<GC>.Clear();

I noticed that the existing implementation is slightly sub-optimal on arm: it's relying on complex addressing modes + is not aligned to 16 bytes. Turns out, 16-byte alignment makes a lot of sense even if we don't use SIMD - stp (two 8-byte stores) benefits from it as well.

Standalone benchmark (for simpler tests)

Apple M2 Max (also tested on Ampere):

| Method | len | Mean | Ratio |
|----------- |----- |-----------:|------:|-
| Clear_Base | 1 | 1.199 ns | 1.00 |
| Clear_PR | 1 | 1.186 ns | 0.99 |
| | | | |
| Clear_Base | 5 | 1.215 ns | 1.00 |
| Clear_PR | 5 | 1.215 ns | 1.00 |
| | | | |
| Clear_Base | 8 | 1.617 ns | 1.00 |
| Clear_PR | 8 | 1.269 ns | 0.78 |
| | | | |
| Clear_Base | 16 | 2.405 ns | 1.00 |
| Clear_PR | 16 | 2.061 ns | 0.86 |
| | | | |
| Clear_Base | 64 | 8.727 ns | 1.00 |
| Clear_PR | 64 | 5.415 ns | 0.62 |
| | | | |
| Clear_Base | 512 | 70.081 ns | 1.00 |
| Clear_PR | 512 | 35.482 ns | 0.51 |
| | | | |
| Clear_Base | 1024 | 140.080 ns | 1.00 |
| Clear_PR | 1024 | 72.254 ns | 0.52 |
| | | | |
| Clear_Base | 4096 | 559.551 ns | 1.00 |
| Clear_PR | 4096 | 333.439 ns | 0.60 |

Ryzen 7950X:

| Method | len | Mean | Ratio |
|----------- |----- |------------:|------:|-
| Clear_Base | 1 | 0.3854 ns | 1.00 |
| Clear_PR | 1 | 0.3798 ns | 0.99 |
| | | | |
| Clear_Base | 5 | 0.5310 ns | 1.00 |
| Clear_PR | 5 | 0.3907 ns | 0.74 |
| | | | |
| Clear_Base | 8 | 0.8995 ns | 1.00 |
| Clear_PR | 8 | 0.5264 ns | 0.59 |
| | | | |
| Clear_Base | 16 | 1.4398 ns | 1.00 |
| Clear_PR | 16 | 1.2039 ns | 0.84 |
| | | | |
| Clear_Base | 64 | 5.1603 ns | 1.00 |
| Clear_PR | 64 | 5.1023 ns | 0.99 |
| | | | |
| Clear_Base | 512 | 46.0601 ns | 1.00 |
| Clear_PR | 512 | 45.9100 ns | 1.00 |
| | | | |
| Clear_Base | 1024 | 94.9471 ns | 1.00 |
| Clear_PR | 1024 | 94.7812 ns | 1.00 |
| | | | |
| Clear_Base | 4096 | 380.4915 ns | 1.00 |
| Clear_PR | 4096 | 375.3246 ns | 0.99 |

For X64, it slightly improves small inputs. It seems like if we pin the input and use SIMD we can see >2X improvement for large inputs but I was not sure about doing pinning (from GC's point of view, atomicity is fine) here and left as is, cc @jkotas

Author:EgorBo
Assignees:EgorBo
Labels:

area-System.Memory

Milestone:-

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.

This works great for microbenchmarks, but it works poorly for real work loads where the jump target is poorly predicated. We used to have switch table like this in managed memcopy originally, but it was later removed.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

This works great for microbenchmarks, but it works poorly for real work loads where the jump target is poorly predicated. We used to have switch table like this in managed memcopy originally, but it was later removed.

Should I revert to the previous shape? I was mostly interested in aligning to 16 bytes (it's the reason I am seeing nice results on arm64) and slightly different addressing mode in the main loop, to make it foldable into stp

@EgorBoEgorBoOct 11, 2023

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Btw, presumably, PGO can re-shuffle the switch (or convert to if-else) based on profile. However, in the real world it's likely has a mixed profile.

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.

Should I revert to the previous shape?

Yes, unless you can prove that the switch with jump table is better for real workloads.

For reference, dotnet/coreclr#9786 is the PR that got rid of the switch from memory copy. It went through extensive validation.

@tannergoodingtannergoodingOct 13, 2023

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.

This works great for microbenchmarks, but it works poorly for real work loads where the jump target is poorly predicated. We used to have switch table like this in managed memcopy originally, but it was later removed.

@jkotas, I'm not sure what you mean here, could you elaborate?

The hardware optimization manuals and underlying native memcpy algorithms across all 3 major implementations (MSVC, GCC, Clang; for x64 and Arm64) use/recommend a jump table explicitly for this reason. Because an unconditional and unpredictable branch, is better than a tight tree of conditional and still unpredictable branches.

The premise is that a fallthrough jump table allows a tight, unrolled version of handling for the trailing elements to exist. This then requires 1 unpredicted branch to decide which of the immediately following and equidistant switch cases needs to be jumped to. These cases are often already in the instruction/decode cache due to being in the same or following 32-byte code window.

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.

dotnet/coreclr#9786 was done by Intel engineers to address bottlenecks observed in real workloads. Yes, it goes contrary to the guidance you have quoted.

Possible explanation: Conditional jumps and indirect jumps use two different predictors. The relative performance of conditional jumps vs. indirect jump depends on relative pressure on each predictor. .NET code tends to have more indirect jumps than an average code. It means that the indirect jump predictor sees more pressure in .NET code and it is more profitable to depend on indirect jump predictor in .NET memcpy implementation.

@EgorBo

EgorBo commented Oct 11, 2023

Copy link
Copy Markdown
MemberAuthor

Codegen on arm64:

New codegen:
; Method Prog:ClearWithReferences2(byref,ulong) (FullOpts)G_M64195_IG01: ;; offset=0x0000 stp fp, lr,[sp, #-0x10]!mov fp,sp ;; size=8 bbWeight=8 PerfScore 12.00G_M64195_IG02: ;; offset=0x0008mov x2, x1cmp x2, #8 bhi G_M64195_IG12 ;; size=12 bbWeight=8 PerfScore 16.00G_M64195_IG03: ;; offset=0x0014cmp w2, #8 bhi G_M64195_IG12mov w1, w2 adr x2,[@RWD00] ldr w2,[x2, x1,LSL #2] adr x3,[G_M64195_IG02]add x2, x2, x3 br x2 ;; size=32 bbWeight=4 PerfScore 30.00G_M64195_IG04: ;; offset=0x0034str xzr,[x0] b G_M64195_IG15align[0 bytes for IG13]align[0 bytes]align[0 bytes]align[0 bytes] ;; size=8 bbWeight=0.50 PerfScore 1.00G_M64195_IG05: ;; offset=0x003C stp xzr, xzr,[x0] b G_M64195_IG15 ;; size=8 bbWeight=0.50 PerfScore 1.00G_M64195_IG06: ;; offset=0x0044 stp xzr, xzr,[x0]str xzr,[x0, #0x10] b G_M64195_IG15 ;; size=12 bbWeight=0.50 PerfScore 1.50G_M64195_IG07: ;; offset=0x0050 stp xzr, xzr,[x0] stp xzr, xzr,[x0, #0x10] b G_M64195_IG15 ;; size=12 bbWeight=0.50 PerfScore 1.50G_M64195_IG08: ;; offset=0x005C stp xzr, xzr,[x0] stp xzr, xzr,[x0, #0x10]str xzr,[x0, #0x20] b G_M64195_IG15 ;; size=16 bbWeight=0.50 PerfScore 2.00G_M64195_IG09: ;; offset=0x006C stp xzr, xzr,[x0] stp xzr, xzr,[x0, #0x10] stp xzr, xzr,[x0, #0x20] b G_M64195_IG15 ;; size=16 bbWeight=0.50 PerfScore 2.00G_M64195_IG10: ;; offset=0x007C stp xzr, xzr,[x0] stp xzr, xzr,[x0, #0x10] stp xzr, xzr,[x0, #0x20]str xzr,[x0, #0x30] b G_M64195_IG15 ;; size=20 bbWeight=0.50 PerfScore 2.50G_M64195_IG11: ;; offset=0x0090 stp xzr, xzr,[x0] stp xzr, xzr,[x0, #0x10] stp xzr, xzr,[x0, #0x20] stp xzr, xzr,[x0, #0x30] b G_M64195_IG15 ;; size=20 bbWeight=0.50 PerfScore 2.50G_M64195_IG12: ;; offset=0x00A4 lsr x2, x1, #3 ;; size=4 bbWeight=4 PerfScore 4.00G_M64195_IG13: ;; offset=0x00A8 stp xzr, xzr,[x0] stp xzr, xzr,[x0, #0x10] stp xzr, xzr,[x0, #0x20] stp xzr, xzr,[x0, #0x30]add x0, x0, #64sub x2, x2, #1 cbnz x2, G_M64195_IG13 ;; size=28 bbWeight=32 PerfScore 192.00G_M64195_IG14: ;; offset=0x00C4and x1, x1, #7 b G_M64195_IG02 ;; size=8 bbWeight=4 PerfScore 6.00G_M64195_IG15: ;; offset=0x00CC ldp fp, lr,[sp], #0x10ret lr ;; size=8 bbWeight=1 PerfScore 2.00RWD00 dd	000000C4h ; case G_M64195_IG15dd0000002Ch ; case G_M64195_IG04dd00000034h ; case G_M64195_IG05dd0000003Ch ; case G_M64195_IG06dd00000048h ; case G_M64195_IG07dd00000054h ; case G_M64195_IG08dd00000064h ; case G_M64195_IG09dd00000074h ; case G_M64195_IG10dd00000088h ; case G_M64195_IG11; Total bytes of code: 212
Previous codegen:
; Method Prog:ClearWithReferences(byref,ulong) (FullOpts)G_M45361_IG01: ;; offset=0x0000 stp fp, lr,[sp, #-0x10]!mov fp,sp ;; size=8 bbWeight=1 PerfScore 1.50G_M45361_IG02: ;; offset=0x0008cmp x1, #8 blo G_M45361_IG04align[0 bytes for IG03]align[0 bytes]align[0 bytes]align[0 bytes] ;; size=8 bbWeight=1 PerfScore 1.50G_M45361_IG03: ;; offset=0x0010lsl x2, x1, #3add x3, x0, x2str xzr,[x3, #-0x08]add x3, x0, x2str xzr,[x3, #-0x10]add x3, x0, x2str xzr,[x3, #-0x18]add x3, x0, x2str xzr,[x3, #-0x20]add x3, x0, x2str xzr,[x3, #-0x28]add x3, x0, x2str xzr,[x3, #-0x30]add x3, x0, x2str xzr,[x3, #-0x38]add x2, x0, x2str xzr,[x2, #-0x40]sub x1, x1, #8cmp x1, #8 bhs G_M45361_IG03 ;; size=80 bbWeight=4 PerfScore 60.00G_M45361_IG04: ;; offset=0x0060cmp x1, #4 bhs G_M45361_IG07 ;; size=8 bbWeight=1 PerfScore 1.50G_M45361_IG05: ;; offset=0x0068cmp x1, #2 bhs G_M45361_IG08 cbnz x1, G_M45361_IG09 ;; size=12 bbWeight=0.50 PerfScore 1.25G_M45361_IG06: ;; offset=0x0074 ldp fp, lr,[sp], #0x10ret lr ;; size=8 bbWeight=0.50 PerfScore 1.00G_M45361_IG07: ;; offset=0x007C stp xzr, xzr,[x0, #0x10]lsl x2, x1, #3add x3, x0, x2str xzr,[x3, #-0x18]add x2, x0, x2str xzr,[x2, #-0x10] ;; size=24 bbWeight=0.50 PerfScore 2.50G_M45361_IG08: ;; offset=0x0094str xzr,[x0, #0x08]lsl x2, x1, #3add x1, x0, x2str xzr,[x1, #-0x08] ;; size=16 bbWeight=0.50 PerfScore 1.75G_M45361_IG09: ;; offset=0x00A4str xzr,[x0] ;; size=4 bbWeight=0.50 PerfScore 0.50G_M45361_IG10: ;; offset=0x00A8 ldp fp, lr,[sp], #0x10ret lr ;; size=8 bbWeight=0.50 PerfScore 1.00; Total bytes of code: 176

I can tune JIT to fold

stp xzr, xzr, [x0]
stp xzr, xzr, [x0, #..]

to SIMD in the newly added LowerIndirCoelsceStore, but it seems to be fast as is.

@EgorBo
EgorBo marked this pull request as draft October 11, 2023 22:59
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.cs Outdated
@EgorBo

EgorBo commented Oct 13, 2023

Copy link
Copy Markdown
MemberAuthor

I have mixed results with this change, I'll wait for fixes for #93382 (needed for the remainder) and #76067 (comment) (needed for the main loop) and try again after

@ghostghost locked as resolved and limited conversation to collaborators Nov 12, 2023
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@EgorBo@jkotas@tannergooding
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Improve SpanHelpers.ClearWithReferences for arm64 - #93346

Closed
EgorBo wants to merge 6 commits into
dotnet:mainfrom
EgorBo:opt-clear-with-ref
Closed

Improve SpanHelpers.ClearWithReferences for arm64#93346
EgorBo wants to merge 6 commits into
dotnet:mainfrom
EgorBo:opt-clear-with-ref

Conversation

@EgorBo

@EgorBoEgorBo commented Oct 11, 2023

Copy link
Copy Markdown
Member

Follow up to #93214, now for Span<GC>.Clear();

I noticed that the existing implementation is slightly sub-optimal on arm: it's relying on complex addressing modes + is not aligned to 16 bytes. Turns out, 16-byte alignment makes a lot of sense even if we don't use SIMD - stp (two 8-byte stores) benefits from it as well.

Standalone benchmark (for simpler tests)

Apple M2 Max (also tested on Ampere):

| Method | len | Mean | Ratio |
|----------- |----- |-----------:|------:|-
| Clear_Base | 1 | 1.199 ns | 1.00 |
| Clear_PR | 1 | 1.186 ns | 0.99 |
| | | | |
| Clear_Base | 5 | 1.215 ns | 1.00 |
| Clear_PR | 5 | 1.215 ns | 1.00 |
| | | | |
| Clear_Base | 8 | 1.617 ns | 1.00 |
| Clear_PR | 8 | 1.269 ns | 0.78 |
| | | | |
| Clear_Base | 16 | 2.405 ns | 1.00 |
| Clear_PR | 16 | 2.061 ns | 0.86 |
| | | | |
| Clear_Base | 64 | 8.727 ns | 1.00 |
| Clear_PR | 64 | 5.415 ns | 0.62 |
| | | | |
| Clear_Base | 512 | 70.081 ns | 1.00 |
| Clear_PR | 512 | 35.482 ns | 0.51 |
| | | | |
| Clear_Base | 1024 | 140.080 ns | 1.00 |
| Clear_PR | 1024 | 72.254 ns | 0.52 |
| | | | |
| Clear_Base | 4096 | 559.551 ns | 1.00 |
| Clear_PR | 4096 | 333.439 ns | 0.60 |

Ryzen 7950X:

| Method | len | Mean | Ratio |
|----------- |----- |------------:|------:|-
| Clear_Base | 1 | 0.3854 ns | 1.00 |
| Clear_PR | 1 | 0.3798 ns | 0.99 |
| | | | |
| Clear_Base | 5 | 0.5310 ns | 1.00 |
| Clear_PR | 5 | 0.3907 ns | 0.74 |
| | | | |
| Clear_Base | 8 | 0.8995 ns | 1.00 |
| Clear_PR | 8 | 0.5264 ns | 0.59 |
| | | | |
| Clear_Base | 16 | 1.4398 ns | 1.00 |
| Clear_PR | 16 | 1.2039 ns | 0.84 |
| | | | |
| Clear_Base | 64 | 5.1603 ns | 1.00 |
| Clear_PR | 64 | 5.1023 ns | 0.99 |
| | | | |
| Clear_Base | 512 | 46.0601 ns | 1.00 |
| Clear_PR | 512 | 45.9100 ns | 1.00 |
| | | | |
| Clear_Base | 1024 | 94.9471 ns | 1.00 |
| Clear_PR | 1024 | 94.7812 ns | 1.00 |
| | | | |
| Clear_Base | 4096 | 380.4915 ns | 1.00 |
| Clear_PR | 4096 | 375.3246 ns | 0.99 |

For X64, it slightly improves small inputs. It seems like if we pin the input and use SIMD we can see >2X improvement for large inputs but I was not sure about doing pinning (from GC's point of view, atomicity is fine) here and left as is, cc @jkotas

@ghostghost assigned EgorBoOct 11, 2023
@ghost

Copy link
Copy Markdown

Tagging subscribers to this area: @dotnet/area-system-memory
See info in area-owners.md if you want to be subscribed.

Issue Details

Follow up to #93214, now for Span<GC>.Clear();

I noticed that the existing implementation is slightly sub-optimal on arm: it's relying on complex addressing modes + is not aligned to 16 bytes. Turns out, 16-byte alignment makes a lot of sense even if we don't use SIMD - stp (two 8-byte stores) benefits from it as well.

Standalone benchmark (for simpler tests)

Apple M2 Max (also tested on Ampere):

| Method | len | Mean | Ratio |
|----------- |----- |-----------:|------:|-
| Clear_Base | 1 | 1.199 ns | 1.00 |
| Clear_PR | 1 | 1.186 ns | 0.99 |
| | | | |
| Clear_Base | 5 | 1.215 ns | 1.00 |
| Clear_PR | 5 | 1.215 ns | 1.00 |
| | | | |
| Clear_Base | 8 | 1.617 ns | 1.00 |
| Clear_PR | 8 | 1.269 ns | 0.78 |
| | | | |
| Clear_Base | 16 | 2.405 ns | 1.00 |
| Clear_PR | 16 | 2.061 ns | 0.86 |
| | | | |
| Clear_Base | 64 | 8.727 ns | 1.00 |
| Clear_PR | 64 | 5.415 ns | 0.62 |
| | | | |
| Clear_Base | 512 | 70.081 ns | 1.00 |
| Clear_PR | 512 | 35.482 ns | 0.51 |
| | | | |
| Clear_Base | 1024 | 140.080 ns | 1.00 |
| Clear_PR | 1024 | 72.254 ns | 0.52 |
| | | | |
| Clear_Base | 4096 | 559.551 ns | 1.00 |
| Clear_PR | 4096 | 333.439 ns | 0.60 |

Ryzen 7950X:

| Method | len | Mean | Ratio |
|----------- |----- |------------:|------:|-
| Clear_Base | 1 | 0.3854 ns | 1.00 |
| Clear_PR | 1 | 0.3798 ns | 0.99 |
| | | | |
| Clear_Base | 5 | 0.5310 ns | 1.00 |
| Clear_PR | 5 | 0.3907 ns | 0.74 |
| | | | |
| Clear_Base | 8 | 0.8995 ns | 1.00 |
| Clear_PR | 8 | 0.5264 ns | 0.59 |
| | | | |
| Clear_Base | 16 | 1.4398 ns | 1.00 |
| Clear_PR | 16 | 1.2039 ns | 0.84 |
| | | | |
| Clear_Base | 64 | 5.1603 ns | 1.00 |
| Clear_PR | 64 | 5.1023 ns | 0.99 |
| | | | |
| Clear_Base | 512 | 46.0601 ns | 1.00 |
| Clear_PR | 512 | 45.9100 ns | 1.00 |
| | | | |
| Clear_Base | 1024 | 94.9471 ns | 1.00 |
| Clear_PR | 1024 | 94.7812 ns | 1.00 |
| | | | |
| Clear_Base | 4096 | 380.4915 ns | 1.00 |
| Clear_PR | 4096 | 375.3246 ns | 0.99 |

For X64, it slightly improves small inputs. It seems like if we pin the input and use SIMD we can see >2X improvement for large inputs but I was not sure about doing pinning (from GC's point of view, atomicity is fine) here and left as is, cc @jkotas

Author:EgorBo
Assignees:EgorBo
Labels:

area-System.Memory

Milestone:-

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.

This works great for microbenchmarks, but it works poorly for real work loads where the jump target is poorly predicated. We used to have switch table like this in managed memcopy originally, but it was later removed.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

This works great for microbenchmarks, but it works poorly for real work loads where the jump target is poorly predicated. We used to have switch table like this in managed memcopy originally, but it was later removed.

Should I revert to the previous shape? I was mostly interested in aligning to 16 bytes (it's the reason I am seeing nice results on arm64) and slightly different addressing mode in the main loop, to make it foldable into stp

@EgorBoEgorBoOct 11, 2023

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Btw, presumably, PGO can re-shuffle the switch (or convert to if-else) based on profile. However, in the real world it's likely has a mixed profile.

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.

Should I revert to the previous shape?

Yes, unless you can prove that the switch with jump table is better for real workloads.

For reference, dotnet/coreclr#9786 is the PR that got rid of the switch from memory copy. It went through extensive validation.

@tannergoodingtannergoodingOct 13, 2023

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.

This works great for microbenchmarks, but it works poorly for real work loads where the jump target is poorly predicated. We used to have switch table like this in managed memcopy originally, but it was later removed.

@jkotas, I'm not sure what you mean here, could you elaborate?

The hardware optimization manuals and underlying native memcpy algorithms across all 3 major implementations (MSVC, GCC, Clang; for x64 and Arm64) use/recommend a jump table explicitly for this reason. Because an unconditional and unpredictable branch, is better than a tight tree of conditional and still unpredictable branches.

The premise is that a fallthrough jump table allows a tight, unrolled version of handling for the trailing elements to exist. This then requires 1 unpredicted branch to decide which of the immediately following and equidistant switch cases needs to be jumped to. These cases are often already in the instruction/decode cache due to being in the same or following 32-byte code window.

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.

dotnet/coreclr#9786 was done by Intel engineers to address bottlenecks observed in real workloads. Yes, it goes contrary to the guidance you have quoted.

Possible explanation: Conditional jumps and indirect jumps use two different predictors. The relative performance of conditional jumps vs. indirect jump depends on relative pressure on each predictor. .NET code tends to have more indirect jumps than an average code. It means that the indirect jump predictor sees more pressure in .NET code and it is more profitable to depend on indirect jump predictor in .NET memcpy implementation.

@EgorBo

EgorBo commented Oct 11, 2023

Copy link
Copy Markdown
MemberAuthor

Codegen on arm64:

New codegen:
; Method Prog:ClearWithReferences2(byref,ulong) (FullOpts)G_M64195_IG01: ;; offset=0x0000 stp fp, lr,[sp, #-0x10]!mov fp,sp ;; size=8 bbWeight=8 PerfScore 12.00G_M64195_IG02: ;; offset=0x0008mov x2, x1cmp x2, #8 bhi G_M64195_IG12 ;; size=12 bbWeight=8 PerfScore 16.00G_M64195_IG03: ;; offset=0x0014cmp w2, #8 bhi G_M64195_IG12mov w1, w2 adr x2,[@RWD00] ldr w2,[x2, x1,LSL #2] adr x3,[G_M64195_IG02]add x2, x2, x3 br x2 ;; size=32 bbWeight=4 PerfScore 30.00G_M64195_IG04: ;; offset=0x0034str xzr,[x0] b G_M64195_IG15align[0 bytes for IG13]align[0 bytes]align[0 bytes]align[0 bytes] ;; size=8 bbWeight=0.50 PerfScore 1.00G_M64195_IG05: ;; offset=0x003C stp xzr, xzr,[x0] b G_M64195_IG15 ;; size=8 bbWeight=0.50 PerfScore 1.00G_M64195_IG06: ;; offset=0x0044 stp xzr, xzr,[x0]str xzr,[x0, #0x10] b G_M64195_IG15 ;; size=12 bbWeight=0.50 PerfScore 1.50G_M64195_IG07: ;; offset=0x0050 stp xzr, xzr,[x0] stp xzr, xzr,[x0, #0x10] b G_M64195_IG15 ;; size=12 bbWeight=0.50 PerfScore 1.50G_M64195_IG08: ;; offset=0x005C stp xzr, xzr,[x0] stp xzr, xzr,[x0, #0x10]str xzr,[x0, #0x20] b G_M64195_IG15 ;; size=16 bbWeight=0.50 PerfScore 2.00G_M64195_IG09: ;; offset=0x006C stp xzr, xzr,[x0] stp xzr, xzr,[x0, #0x10] stp xzr, xzr,[x0, #0x20] b G_M64195_IG15 ;; size=16 bbWeight=0.50 PerfScore 2.00G_M64195_IG10: ;; offset=0x007C stp xzr, xzr,[x0] stp xzr, xzr,[x0, #0x10] stp xzr, xzr,[x0, #0x20]str xzr,[x0, #0x30] b G_M64195_IG15 ;; size=20 bbWeight=0.50 PerfScore 2.50G_M64195_IG11: ;; offset=0x0090 stp xzr, xzr,[x0] stp xzr, xzr,[x0, #0x10] stp xzr, xzr,[x0, #0x20] stp xzr, xzr,[x0, #0x30] b G_M64195_IG15 ;; size=20 bbWeight=0.50 PerfScore 2.50G_M64195_IG12: ;; offset=0x00A4 lsr x2, x1, #3 ;; size=4 bbWeight=4 PerfScore 4.00G_M64195_IG13: ;; offset=0x00A8 stp xzr, xzr,[x0] stp xzr, xzr,[x0, #0x10] stp xzr, xzr,[x0, #0x20] stp xzr, xzr,[x0, #0x30]add x0, x0, #64sub x2, x2, #1 cbnz x2, G_M64195_IG13 ;; size=28 bbWeight=32 PerfScore 192.00G_M64195_IG14: ;; offset=0x00C4and x1, x1, #7 b G_M64195_IG02 ;; size=8 bbWeight=4 PerfScore 6.00G_M64195_IG15: ;; offset=0x00CC ldp fp, lr,[sp], #0x10ret lr ;; size=8 bbWeight=1 PerfScore 2.00RWD00 dd	000000C4h ; case G_M64195_IG15dd0000002Ch ; case G_M64195_IG04dd00000034h ; case G_M64195_IG05dd0000003Ch ; case G_M64195_IG06dd00000048h ; case G_M64195_IG07dd00000054h ; case G_M64195_IG08dd00000064h ; case G_M64195_IG09dd00000074h ; case G_M64195_IG10dd00000088h ; case G_M64195_IG11; Total bytes of code: 212
Previous codegen:
; Method Prog:ClearWithReferences(byref,ulong) (FullOpts)G_M45361_IG01: ;; offset=0x0000 stp fp, lr,[sp, #-0x10]!mov fp,sp ;; size=8 bbWeight=1 PerfScore 1.50G_M45361_IG02: ;; offset=0x0008cmp x1, #8 blo G_M45361_IG04align[0 bytes for IG03]align[0 bytes]align[0 bytes]align[0 bytes] ;; size=8 bbWeight=1 PerfScore 1.50G_M45361_IG03: ;; offset=0x0010lsl x2, x1, #3add x3, x0, x2str xzr,[x3, #-0x08]add x3, x0, x2str xzr,[x3, #-0x10]add x3, x0, x2str xzr,[x3, #-0x18]add x3, x0, x2str xzr,[x3, #-0x20]add x3, x0, x2str xzr,[x3, #-0x28]add x3, x0, x2str xzr,[x3, #-0x30]add x3, x0, x2str xzr,[x3, #-0x38]add x2, x0, x2str xzr,[x2, #-0x40]sub x1, x1, #8cmp x1, #8 bhs G_M45361_IG03 ;; size=80 bbWeight=4 PerfScore 60.00G_M45361_IG04: ;; offset=0x0060cmp x1, #4 bhs G_M45361_IG07 ;; size=8 bbWeight=1 PerfScore 1.50G_M45361_IG05: ;; offset=0x0068cmp x1, #2 bhs G_M45361_IG08 cbnz x1, G_M45361_IG09 ;; size=12 bbWeight=0.50 PerfScore 1.25G_M45361_IG06: ;; offset=0x0074 ldp fp, lr,[sp], #0x10ret lr ;; size=8 bbWeight=0.50 PerfScore 1.00G_M45361_IG07: ;; offset=0x007C stp xzr, xzr,[x0, #0x10]lsl x2, x1, #3add x3, x0, x2str xzr,[x3, #-0x18]add x2, x0, x2str xzr,[x2, #-0x10] ;; size=24 bbWeight=0.50 PerfScore 2.50G_M45361_IG08: ;; offset=0x0094str xzr,[x0, #0x08]lsl x2, x1, #3add x1, x0, x2str xzr,[x1, #-0x08] ;; size=16 bbWeight=0.50 PerfScore 1.75G_M45361_IG09: ;; offset=0x00A4str xzr,[x0] ;; size=4 bbWeight=0.50 PerfScore 0.50G_M45361_IG10: ;; offset=0x00A8 ldp fp, lr,[sp], #0x10ret lr ;; size=8 bbWeight=0.50 PerfScore 1.00; Total bytes of code: 176

I can tune JIT to fold

stp xzr, xzr, [x0]
stp xzr, xzr, [x0, #..]

to SIMD in the newly added LowerIndirCoelsceStore, but it seems to be fast as is.

@EgorBo
EgorBo marked this pull request as draft October 11, 2023 22:59
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.cs Outdated
@EgorBo

EgorBo commented Oct 13, 2023

Copy link
Copy Markdown
MemberAuthor

I have mixed results with this change, I'll wait for fixes for #93382 (needed for the remainder) and #76067 (comment) (needed for the main loop) and try again after

@ghostghost locked as resolved and limited conversation to collaborators Nov 12, 2023
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

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

Improve SpanHelpers.ClearWithReferences for arm64 - #93346

Closed
EgorBo wants to merge 6 commits into
dotnet:mainfrom
EgorBo:opt-clear-with-ref
Closed

Improve SpanHelpers.ClearWithReferences for arm64#93346
EgorBo wants to merge 6 commits into
dotnet:mainfrom
EgorBo:opt-clear-with-ref

Conversation

@EgorBo

@EgorBoEgorBo commented Oct 11, 2023

Copy link
Copy Markdown
Member

Follow up to #93214, now for Span<GC>.Clear();

I noticed that the existing implementation is slightly sub-optimal on arm: it's relying on complex addressing modes + is not aligned to 16 bytes. Turns out, 16-byte alignment makes a lot of sense even if we don't use SIMD - stp (two 8-byte stores) benefits from it as well.

Standalone benchmark (for simpler tests)

Apple M2 Max (also tested on Ampere):

| Method | len | Mean | Ratio |
|----------- |----- |-----------:|------:|-
| Clear_Base | 1 | 1.199 ns | 1.00 |
| Clear_PR | 1 | 1.186 ns | 0.99 |
| | | | |
| Clear_Base | 5 | 1.215 ns | 1.00 |
| Clear_PR | 5 | 1.215 ns | 1.00 |
| | | | |
| Clear_Base | 8 | 1.617 ns | 1.00 |
| Clear_PR | 8 | 1.269 ns | 0.78 |
| | | | |
| Clear_Base | 16 | 2.405 ns | 1.00 |
| Clear_PR | 16 | 2.061 ns | 0.86 |
| | | | |
| Clear_Base | 64 | 8.727 ns | 1.00 |
| Clear_PR | 64 | 5.415 ns | 0.62 |
| | | | |
| Clear_Base | 512 | 70.081 ns | 1.00 |
| Clear_PR | 512 | 35.482 ns | 0.51 |
| | | | |
| Clear_Base | 1024 | 140.080 ns | 1.00 |
| Clear_PR | 1024 | 72.254 ns | 0.52 |
| | | | |
| Clear_Base | 4096 | 559.551 ns | 1.00 |
| Clear_PR | 4096 | 333.439 ns | 0.60 |

Ryzen 7950X:

| Method | len | Mean | Ratio |
|----------- |----- |------------:|------:|-
| Clear_Base | 1 | 0.3854 ns | 1.00 |
| Clear_PR | 1 | 0.3798 ns | 0.99 |
| | | | |
| Clear_Base | 5 | 0.5310 ns | 1.00 |
| Clear_PR | 5 | 0.3907 ns | 0.74 |
| | | | |
| Clear_Base | 8 | 0.8995 ns | 1.00 |
| Clear_PR | 8 | 0.5264 ns | 0.59 |
| | | | |
| Clear_Base | 16 | 1.4398 ns | 1.00 |
| Clear_PR | 16 | 1.2039 ns | 0.84 |
| | | | |
| Clear_Base | 64 | 5.1603 ns | 1.00 |
| Clear_PR | 64 | 5.1023 ns | 0.99 |
| | | | |
| Clear_Base | 512 | 46.0601 ns | 1.00 |
| Clear_PR | 512 | 45.9100 ns | 1.00 |
| | | | |
| Clear_Base | 1024 | 94.9471 ns | 1.00 |
| Clear_PR | 1024 | 94.7812 ns | 1.00 |
| | | | |
| Clear_Base | 4096 | 380.4915 ns | 1.00 |
| Clear_PR | 4096 | 375.3246 ns | 0.99 |

For X64, it slightly improves small inputs. It seems like if we pin the input and use SIMD we can see >2X improvement for large inputs but I was not sure about doing pinning (from GC's point of view, atomicity is fine) here and left as is, cc @jkotas

@ghostghost assigned EgorBoOct 11, 2023
@ghost

Copy link
Copy Markdown

Tagging subscribers to this area: @dotnet/area-system-memory
See info in area-owners.md if you want to be subscribed.

Issue Details

Follow up to #93214, now for Span<GC>.Clear();

I noticed that the existing implementation is slightly sub-optimal on arm: it's relying on complex addressing modes + is not aligned to 16 bytes. Turns out, 16-byte alignment makes a lot of sense even if we don't use SIMD - stp (two 8-byte stores) benefits from it as well.

Standalone benchmark (for simpler tests)

Apple M2 Max (also tested on Ampere):

| Method | len | Mean | Ratio |
|----------- |----- |-----------:|------:|-
| Clear_Base | 1 | 1.199 ns | 1.00 |
| Clear_PR | 1 | 1.186 ns | 0.99 |
| | | | |
| Clear_Base | 5 | 1.215 ns | 1.00 |
| Clear_PR | 5 | 1.215 ns | 1.00 |
| | | | |
| Clear_Base | 8 | 1.617 ns | 1.00 |
| Clear_PR | 8 | 1.269 ns | 0.78 |
| | | | |
| Clear_Base | 16 | 2.405 ns | 1.00 |
| Clear_PR | 16 | 2.061 ns | 0.86 |
| | | | |
| Clear_Base | 64 | 8.727 ns | 1.00 |
| Clear_PR | 64 | 5.415 ns | 0.62 |
| | | | |
| Clear_Base | 512 | 70.081 ns | 1.00 |
| Clear_PR | 512 | 35.482 ns | 0.51 |
| | | | |
| Clear_Base | 1024 | 140.080 ns | 1.00 |
| Clear_PR | 1024 | 72.254 ns | 0.52 |
| | | | |
| Clear_Base | 4096 | 559.551 ns | 1.00 |
| Clear_PR | 4096 | 333.439 ns | 0.60 |

Ryzen 7950X:

| Method | len | Mean | Ratio |
|----------- |----- |------------:|------:|-
| Clear_Base | 1 | 0.3854 ns | 1.00 |
| Clear_PR | 1 | 0.3798 ns | 0.99 |
| | | | |
| Clear_Base | 5 | 0.5310 ns | 1.00 |
| Clear_PR | 5 | 0.3907 ns | 0.74 |
| | | | |
| Clear_Base | 8 | 0.8995 ns | 1.00 |
| Clear_PR | 8 | 0.5264 ns | 0.59 |
| | | | |
| Clear_Base | 16 | 1.4398 ns | 1.00 |
| Clear_PR | 16 | 1.2039 ns | 0.84 |
| | | | |
| Clear_Base | 64 | 5.1603 ns | 1.00 |
| Clear_PR | 64 | 5.1023 ns | 0.99 |
| | | | |
| Clear_Base | 512 | 46.0601 ns | 1.00 |
| Clear_PR | 512 | 45.9100 ns | 1.00 |
| | | | |
| Clear_Base | 1024 | 94.9471 ns | 1.00 |
| Clear_PR | 1024 | 94.7812 ns | 1.00 |
| | | | |
| Clear_Base | 4096 | 380.4915 ns | 1.00 |
| Clear_PR | 4096 | 375.3246 ns | 0.99 |

For X64, it slightly improves small inputs. It seems like if we pin the input and use SIMD we can see >2X improvement for large inputs but I was not sure about doing pinning (from GC's point of view, atomicity is fine) here and left as is, cc @jkotas

Author:EgorBo
Assignees:EgorBo
Labels:

area-System.Memory

Milestone:-

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.

This works great for microbenchmarks, but it works poorly for real work loads where the jump target is poorly predicated. We used to have switch table like this in managed memcopy originally, but it was later removed.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

This works great for microbenchmarks, but it works poorly for real work loads where the jump target is poorly predicated. We used to have switch table like this in managed memcopy originally, but it was later removed.

Should I revert to the previous shape? I was mostly interested in aligning to 16 bytes (it's the reason I am seeing nice results on arm64) and slightly different addressing mode in the main loop, to make it foldable into stp

@EgorBoEgorBoOct 11, 2023

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Btw, presumably, PGO can re-shuffle the switch (or convert to if-else) based on profile. However, in the real world it's likely has a mixed profile.

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.

Should I revert to the previous shape?

Yes, unless you can prove that the switch with jump table is better for real workloads.

For reference, dotnet/coreclr#9786 is the PR that got rid of the switch from memory copy. It went through extensive validation.

@tannergoodingtannergoodingOct 13, 2023

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.

This works great for microbenchmarks, but it works poorly for real work loads where the jump target is poorly predicated. We used to have switch table like this in managed memcopy originally, but it was later removed.

@jkotas, I'm not sure what you mean here, could you elaborate?

The hardware optimization manuals and underlying native memcpy algorithms across all 3 major implementations (MSVC, GCC, Clang; for x64 and Arm64) use/recommend a jump table explicitly for this reason. Because an unconditional and unpredictable branch, is better than a tight tree of conditional and still unpredictable branches.

The premise is that a fallthrough jump table allows a tight, unrolled version of handling for the trailing elements to exist. This then requires 1 unpredicted branch to decide which of the immediately following and equidistant switch cases needs to be jumped to. These cases are often already in the instruction/decode cache due to being in the same or following 32-byte code window.

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.

dotnet/coreclr#9786 was done by Intel engineers to address bottlenecks observed in real workloads. Yes, it goes contrary to the guidance you have quoted.

Possible explanation: Conditional jumps and indirect jumps use two different predictors. The relative performance of conditional jumps vs. indirect jump depends on relative pressure on each predictor. .NET code tends to have more indirect jumps than an average code. It means that the indirect jump predictor sees more pressure in .NET code and it is more profitable to depend on indirect jump predictor in .NET memcpy implementation.

@EgorBo

EgorBo commented Oct 11, 2023

Copy link
Copy Markdown
MemberAuthor

Codegen on arm64:

New codegen:
; Method Prog:ClearWithReferences2(byref,ulong) (FullOpts)G_M64195_IG01: ;; offset=0x0000 stp fp, lr,[sp, #-0x10]!mov fp,sp ;; size=8 bbWeight=8 PerfScore 12.00G_M64195_IG02: ;; offset=0x0008mov x2, x1cmp x2, #8 bhi G_M64195_IG12 ;; size=12 bbWeight=8 PerfScore 16.00G_M64195_IG03: ;; offset=0x0014cmp w2, #8 bhi G_M64195_IG12mov w1, w2 adr x2,[@RWD00] ldr w2,[x2, x1,LSL #2] adr x3,[G_M64195_IG02]add x2, x2, x3 br x2 ;; size=32 bbWeight=4 PerfScore 30.00G_M64195_IG04: ;; offset=0x0034str xzr,[x0] b G_M64195_IG15align[0 bytes for IG13]align[0 bytes]align[0 bytes]align[0 bytes] ;; size=8 bbWeight=0.50 PerfScore 1.00G_M64195_IG05: ;; offset=0x003C stp xzr, xzr,[x0] b G_M64195_IG15 ;; size=8 bbWeight=0.50 PerfScore 1.00G_M64195_IG06: ;; offset=0x0044 stp xzr, xzr,[x0]str xzr,[x0, #0x10] b G_M64195_IG15 ;; size=12 bbWeight=0.50 PerfScore 1.50G_M64195_IG07: ;; offset=0x0050 stp xzr, xzr,[x0] stp xzr, xzr,[x0, #0x10] b G_M64195_IG15 ;; size=12 bbWeight=0.50 PerfScore 1.50G_M64195_IG08: ;; offset=0x005C stp xzr, xzr,[x0] stp xzr, xzr,[x0, #0x10]str xzr,[x0, #0x20] b G_M64195_IG15 ;; size=16 bbWeight=0.50 PerfScore 2.00G_M64195_IG09: ;; offset=0x006C stp xzr, xzr,[x0] stp xzr, xzr,[x0, #0x10] stp xzr, xzr,[x0, #0x20] b G_M64195_IG15 ;; size=16 bbWeight=0.50 PerfScore 2.00G_M64195_IG10: ;; offset=0x007C stp xzr, xzr,[x0] stp xzr, xzr,[x0, #0x10] stp xzr, xzr,[x0, #0x20]str xzr,[x0, #0x30] b G_M64195_IG15 ;; size=20 bbWeight=0.50 PerfScore 2.50G_M64195_IG11: ;; offset=0x0090 stp xzr, xzr,[x0] stp xzr, xzr,[x0, #0x10] stp xzr, xzr,[x0, #0x20] stp xzr, xzr,[x0, #0x30] b G_M64195_IG15 ;; size=20 bbWeight=0.50 PerfScore 2.50G_M64195_IG12: ;; offset=0x00A4 lsr x2, x1, #3 ;; size=4 bbWeight=4 PerfScore 4.00G_M64195_IG13: ;; offset=0x00A8 stp xzr, xzr,[x0] stp xzr, xzr,[x0, #0x10] stp xzr, xzr,[x0, #0x20] stp xzr, xzr,[x0, #0x30]add x0, x0, #64sub x2, x2, #1 cbnz x2, G_M64195_IG13 ;; size=28 bbWeight=32 PerfScore 192.00G_M64195_IG14: ;; offset=0x00C4and x1, x1, #7 b G_M64195_IG02 ;; size=8 bbWeight=4 PerfScore 6.00G_M64195_IG15: ;; offset=0x00CC ldp fp, lr,[sp], #0x10ret lr ;; size=8 bbWeight=1 PerfScore 2.00RWD00 dd	000000C4h ; case G_M64195_IG15dd0000002Ch ; case G_M64195_IG04dd00000034h ; case G_M64195_IG05dd0000003Ch ; case G_M64195_IG06dd00000048h ; case G_M64195_IG07dd00000054h ; case G_M64195_IG08dd00000064h ; case G_M64195_IG09dd00000074h ; case G_M64195_IG10dd00000088h ; case G_M64195_IG11; Total bytes of code: 212
Previous codegen:
; Method Prog:ClearWithReferences(byref,ulong) (FullOpts)G_M45361_IG01: ;; offset=0x0000 stp fp, lr,[sp, #-0x10]!mov fp,sp ;; size=8 bbWeight=1 PerfScore 1.50G_M45361_IG02: ;; offset=0x0008cmp x1, #8 blo G_M45361_IG04align[0 bytes for IG03]align[0 bytes]align[0 bytes]align[0 bytes] ;; size=8 bbWeight=1 PerfScore 1.50G_M45361_IG03: ;; offset=0x0010lsl x2, x1, #3add x3, x0, x2str xzr,[x3, #-0x08]add x3, x0, x2str xzr,[x3, #-0x10]add x3, x0, x2str xzr,[x3, #-0x18]add x3, x0, x2str xzr,[x3, #-0x20]add x3, x0, x2str xzr,[x3, #-0x28]add x3, x0, x2str xzr,[x3, #-0x30]add x3, x0, x2str xzr,[x3, #-0x38]add x2, x0, x2str xzr,[x2, #-0x40]sub x1, x1, #8cmp x1, #8 bhs G_M45361_IG03 ;; size=80 bbWeight=4 PerfScore 60.00G_M45361_IG04: ;; offset=0x0060cmp x1, #4 bhs G_M45361_IG07 ;; size=8 bbWeight=1 PerfScore 1.50G_M45361_IG05: ;; offset=0x0068cmp x1, #2 bhs G_M45361_IG08 cbnz x1, G_M45361_IG09 ;; size=12 bbWeight=0.50 PerfScore 1.25G_M45361_IG06: ;; offset=0x0074 ldp fp, lr,[sp], #0x10ret lr ;; size=8 bbWeight=0.50 PerfScore 1.00G_M45361_IG07: ;; offset=0x007C stp xzr, xzr,[x0, #0x10]lsl x2, x1, #3add x3, x0, x2str xzr,[x3, #-0x18]add x2, x0, x2str xzr,[x2, #-0x10] ;; size=24 bbWeight=0.50 PerfScore 2.50G_M45361_IG08: ;; offset=0x0094str xzr,[x0, #0x08]lsl x2, x1, #3add x1, x0, x2str xzr,[x1, #-0x08] ;; size=16 bbWeight=0.50 PerfScore 1.75G_M45361_IG09: ;; offset=0x00A4str xzr,[x0] ;; size=4 bbWeight=0.50 PerfScore 0.50G_M45361_IG10: ;; offset=0x00A8 ldp fp, lr,[sp], #0x10ret lr ;; size=8 bbWeight=0.50 PerfScore 1.00; Total bytes of code: 176

I can tune JIT to fold

stp xzr, xzr, [x0]
stp xzr, xzr, [x0, #..]

to SIMD in the newly added LowerIndirCoelsceStore, but it seems to be fast as is.

@EgorBo
EgorBo marked this pull request as draft October 11, 2023 22:59
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.cs Outdated
@EgorBo

EgorBo commented Oct 13, 2023

Copy link
Copy Markdown
MemberAuthor

I have mixed results with this change, I'll wait for fixes for #93382 (needed for the remainder) and #76067 (comment) (needed for the main loop) and try again after

@ghostghost locked as resolved and limited conversation to collaborators Nov 12, 2023
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@EgorBo@jkotas@tannergooding