Try to reduce cost of Async - #101605

Merged
MichalStrehovsky merged 2 commits into
dotnet:mainfrom
MichalStrehovsky:async
May 2, 2024
Merged

Try to reduce cost of Async#101605
MichalStrehovsky merged 2 commits into
dotnet:mainfrom
MichalStrehovsky:async

Conversation

@MichalStrehovsky

Copy link
Copy Markdown
Member

Contributes to #79204.

I don't know if this will be considered mergeable, but I wanted to at least try something. For apps that use async a lot (like the Stage2 app we use for Goldilocks), the async infrastructure can cost 10% of the entire executable.

Shuffling a couple things in GetStateMachineBox I was able to get 0.23% saving. It's miniscule. In general async is death by a thousand papercuts so I don't see a silver bullet.

Click to expand existing assembly
 TodosApi!System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<System.Threading.Tasks.VoidTaskResult>__GetStateMachineBox<S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>:00000001`409833704156pushr1400000001`4098337257pushrdi00000001`4098337356pushrsi00000001`4098337455pushrbp00000001`4098337553pushrbx00000001`40983376 4883ec20 subrsp,20h00000001`4098337a 488bf1 movrsi, stateMachine (rcx)00000001`4098337d 488bda movrbx, taskField (rdx)00000001`40983380 e8eb68aaff call TodosApi!System.Threading.ExecutionContext__Capture (140429c70)00000001`40983385 488be8 movrbp,rax00000001`40983388 488b3b movrdi, qword ptr [taskField (rbx)]00000001`4098338b 4885ff testrdi,rdi00000001`4098338e 742c je TodosApi!System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<System.Threading.Tasks.VoidTaskResult>__GetStateMachineBox<S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>+0x4c (1409833bc)00000001`40983390 488d0df93f3e00 learcx,[TodosApi!??_7System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1_AsyncStateMachineBox`1<System.Threading.Tasks.VoidTaskResult__S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>@@6B@ (140d67390)]00000001`40983397 48390f cmp qword ptr [rdi],rcx00000001`4098339a 7520jne TodosApi!System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<System.Threading.Tasks.VoidTaskResult>__GetStateMachineBox<S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>+0x4c (1409833bc)00000001`4098339c 48396f10 cmp qword ptr [stronglyTypedBox (rdi)+10h], currentContext (rbp)00000001`409833a0 740c je TodosApi!System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<System.Threading.Tasks.VoidTaskResult>__GetStateMachineBox<S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>+0x3e (1409833ae)00000001`409833a2 488d4f10 learcx,[stronglyTypedBox (rdi)+10h]00000001`409833a6 488bd5 movrdx,rbp00000001`409833a9 e8921a69ff call TodosApi!RhpAssignRefAVLocation (140014e40)00000001`409833ae 488bc7 movrax,rdi00000001`409833b1 4883c420 addrsp,20h00000001`409833b5 5b poprbx00000001`409833b6 5d poprbp00000001`409833b7 5e poprsi00000001`409833b8 5f poprdi00000001`409833b9 415e popr1400000001`409833bb c3 ret00000001`409833bc 4c8b33 movr14, qword ptr [taskField (rbx)]00000001`409833bf 4d85f6 testr14,r1400000001`409833c2 7461je TodosApi!System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<System.Threading.Tasks.VoidTaskResult>__GetStateMachineBox<S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>+0xb5 (140983425)00000001`409833c4 488d0d7d193e00 learcx,[TodosApi!??_7System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1_AsyncStateMachineBox`1<System.Threading.Tasks.VoidTaskResult__System.Runtime.CompilerServices.IAsyncStateMachine>@@6B@ (140d64d48)]00000001`409833cb 49390e cmp qword ptr [r14],rcx00000001`409833ce 7555jne TodosApi!System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<System.Threading.Tasks.VoidTaskResult>__GetStateMachineBox<S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>+0xb5 (140983425)00000001`409833d0 49837e4000 cmp qword ptr [weaklyTypedBox (r14)+40h],000000001`409833d5 7534jne TodosApi!System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<System.Threading.Tasks.VoidTaskResult>__GetStateMachineBox<S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>+0x9b (14098340b)00000001`409833d7 488d0d1a622a00 learcx,[TodosApi!Boxed_S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100::`vftable' (140c295f8)]00000001`409833de e84d1869ff call TodosApi!RhpNewFast (140014c30)00000001`409833e3 488bd0 movrdx,rax00000001`409833e3 488bd0 movrdx,rax00000001`409833e6 488d7a08 leardi,[rdx+8]00000001`409833ea e8411c69ff call TodosApi!RhpByRefAssignRefAVLocation1 (140015030)00000001`409833ef e83c1c69ff call TodosApi!RhpByRefAssignRefAVLocation1 (140015030)00000001`409833f4 48a5 movs qword ptr [rdi], qword ptr [rsi]00000001`409833f6 e8351c69ff call TodosApi!RhpByRefAssignRefAVLocation1 (140015030)00000001`409833fb e8301c69ff call TodosApi!RhpByRefAssignRefAVLocation1 (140015030)00000001`40983400 48a5 movs qword ptr [rdi], qword ptr [rsi]00000001`40983402 498d4e40 learcx,[weaklyTypedBox (r14)+40h]00000001`40983406 e8351a69ff call TodosApi!RhpAssignRefAVLocation (140014e40)00000001`4098340b 498d4e10 learcx,[weaklyTypedBox (r14)+10h]00000001`4098340f 488bd5 movrdx,rbp00000001`40983412 e8291a69ff call TodosApi!RhpAssignRefAVLocation (140014e40)00000001`40983417 498bc6 movrax,r1400000001`4098341a 4883c420 addrsp,20h00000001`4098341e 5b poprbx00000001`4098341f 5d poprbp00000001`40983420 5e poprsi00000001`40983421 5f poprdi00000001`40983422 415e popr1400000001`40983424 c3 ret00000001`40983425 488d0d643f3e00 learcx,[TodosApi!??_7System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1_AsyncStateMachineBox`1<System.Threading.Tasks.VoidTaskResult__S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>@@6B@ (140d67390)]00000001`4098342c e8ff1769ff call TodosApi!RhpNewFast (140014c30)00000001`40983431 4c8bf0 movr14,rax00000001`40983434 41c7463400040002 mov dword ptr [r14+34h],2000400h00000001`4098343c 41814e3400080000 or dword ptr [r14+34h],800h00000001`40983444 488bcb movrcx,rbx00000001`40983447 498bd6 movrdx,r1400000001`4098344a e8611a69ff call TodosApi!RhpCheckedAssignRefAVLocation (140014eb0)00000001`4098344f 498d7e40 leardi,[r14+40h]00000001`40983453 e8d81b69ff call TodosApi!RhpByRefAssignRefAVLocation1 (140015030)00000001`40983458 e8d31b69ff call TodosApi!RhpByRefAssignRefAVLocation1 (140015030)00000001`4098345d 48a5 movs qword ptr [rdi], qword ptr [rsi]00000001`4098345f e8cc1b69ff call TodosApi!RhpByRefAssignRefAVLocation1 (140015030)00000001`40983464 e8c71b69ff call TodosApi!RhpByRefAssignRefAVLocation1 (140015030)00000001`40983469 48a5 movs qword ptr [rdi], qword ptr [rsi]00000001`4098346b 498d4e10 learcx,[r14+10h]00000001`4098346f 488bd5 movrdx,rbp00000001`40983472 e8c91969ff call TodosApi!RhpAssignRefAVLocation (140014e40)00000001`40983477 488d0dea6f1f00 learcx,[TodosApi!?__NONGCSTATICS@System.Threading.Tasks.TplEventSource@@ (140b7a468)]00000001`4098347e 488379f800 cmp qword ptr [rcx-8],000000001`409834837559jne TodosApi!System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<System.Threading.Tasks.VoidTaskResult>__GetStateMachineBox<S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>+0x16e (1409834de)00000001`40983485 488b0d94b0c400 movrcx, qword ptr [TodosApi!?__GCSTATICS@System.Threading.Tasks.TplEventSource@@ (1415ce520)]00000001`4098348c 488b5908 movrbx, qword ptr [rcx+8]00000001`40983490 80bb9d00000000 cmp byte ptr [rbx+9Dh],000000001`409834977437je TodosApi!System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<System.Threading.Tasks.VoidTaskResult>__GetStateMachineBox<S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>+0x160 (1409834d0)00000001`40983499 498bce movrcx,r1400000001`4098349c e83f27abff call TodosApi!System.Threading.Tasks.Task__get_Id (140435be0)00000001`409834a1 8bf0 movesi,eax00000001`409834a3 488d0de68c1a00 learcx,[TodosApi!__RuntimeType_S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100 (140b2c190)]00000001`409834aa e85161a4ff call TodosApi!System.RuntimeType__get_Name (1403c9600)00000001`409834af 488bd0 movrdx,rax00000001`409834b2 488d0dff5c0e00 learcx,[TodosApi!__Str_Async___4E51D638FE527E2B0672DE04B6E5F6B84BA81E64E12AA0136B0691A50E6F80FE (140a691b8)]00000001`409834b9 e89285a4ff call TodosApi!String__Concat_6 (1403cba50)00000001`409834be 4c8bc0 movr8,rax00000001`409834c1 488bcb movrcx,rbx00000001`409834c4 8bd6 movedx,esi00000001`409834c6 4533c9 xorr9d,r9d00000001`409834c9 3909cmp dword ptr [rcx],ecx00000001`409834cb e8608cabff call TodosApi!System.Threading.Tasks.TplEventSource__TraceOperationBegin (14043c130)00000001`409834d0 498bc6 movrax,r1400000001`409834d3 4883c420 addrsp,20h00000001`409834d7 5b poprbx00000001`409834d8 5d poprbp00000001`409834d9 5e poprsi00000001`409834da 5f poprdi00000001`409834db 415e popr1400000001`409834dd c3 ret00000001`409834de e86a0f68ff call TodosApi!__GetGCStaticBase_System.Threading.Tasks.TplEventSource (14000444d)00000001`409834e3 eba0 jmp TodosApi!System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<System.Threading.Tasks.VoidTaskResult>__GetStateMachineBox<S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>+0x115 (140983485)

Addresses following problems:

  • Multiple expensive epilogs
  • Copying the state machine is costly because they tend to be huge and are a mix of reference fields (need write barriers) and value fields. We need to copy twice - one is an assignment, the other is a box. See clusters of RhpByRefAssignRefAVLocation1 - that's the write barrier. I'm making the box take a less efficient path based on comments.
  • Logging is constructing a string that doesn't really need to be in generic code.

This is just a couple dozen bytes in savings per method, but ends up saving 50 kB for the whole app because that's how many specializations of the method we have.

Cc @stephentoub@jkotas for thoughts.

Contributes to dotnet#79204.
I don't know if this will be considered mergeable, but I wanted to at least try something. For apps that use async a lot (like the Stage2 app we use for Goldilocks), the async infrastructure can cost 10% of the entire executable.
Shuffling a couple things in `GetStateMachineBox` I was able to get 0.23% saving. It's miniscule. In general async is death by a thousand papercuts so I don't see a silver bullet.
@jkotas

Copy link
Copy Markdown
Member

Copying the state machine is costly because they tend to be huge and are a mix of reference fields (need write barriers) and value fields

That's because RyuJIT inlines the box helper for large structs. I think this should be fixed in the JIT.

Outlining of the event tracing helper looks good.

if (taskField is AsyncStateMachineBox<IAsyncStateMachine> weaklyTypedBox)
else if (taskField is AsyncStateMachineBox<IAsyncStateMachine> weaklyTypedBox)
{
// If this is the first await, we won't yet have a state machine, so store it.

@stephentoubstephentoubApr 26, 2024

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.

If we're trying to squeeze water from a stone, this whole block should be extremely cold (only used in debugging scenarios), so if you can come up with a good way to outline it all to a separate helper, that'd be reasonable. Not sure if that'd help much, though, given that stateMachine is generic.

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.

Yeah, it would likely make things worse because it's still the same code, but with extra unwinding/GC tracking data structures because we have a new method.

@MichalStrehovsky

Copy link
Copy Markdown
MemberAuthor

That's because RyuJIT inlines the box helper for large structs. I think this should be fixed in the JIT.

Do you have a heuristic in mind? It seems like what RyuJIT is doing now is strictly better from throughput perspective (we call the right allocation helper, assign all the fields, only do write barriers for GC pointers). Falling back to the regular boxing helper would be slower. Or are you thinking about a object RuntimeHelpers.Box<T>(ref T memory) that would be like a box, but guaranteed not to be expanded into alloc+set individual fields.

@EgorBo

EgorBo commented Apr 26, 2024

Copy link
Copy Markdown
Member

That's because RyuJIT inlines the box helper for large structs. I think this should be fixed in the JIT.

Do you have a heuristic in mind? It seems like what RyuJIT is doing now is strictly better from throughput perspective (we call the right allocation helper, assign all the fields, only do write barriers for GC pointers). Falling back to the regular boxing helper would be slower. Or are you thinking about a object RuntimeHelpers.Box<T>(ref T memory) that would be like a box, but guaranteed not to be expanded into alloc+set individual fields.

I presume Jan means that for a struct with many gc fields (JIT can check that) it's better for VM/Box helper to do a batch move (BulkMoveWithWriteBarrier) instead of individual write barriers

@jkotas

Copy link
Copy Markdown
Member

It seems like what RyuJIT is doing now is strictly better from throughput perspective

I expect that the bulk copy in the box helper starts to win in throughput above certain size. The JIT should probably give up inlining the copy way below this threshold for good code size / throughput tradeoff.

Also, the current implementation of the box is slow since the box helper is almost never used today. Jeremy hit that problem when switching a few places in reflection to use the box helper (WIP #101137).

@EgorBo

Copy link
Copy Markdown
Member

Quick test:

[Benchmark]publicobjectBox()=>ms;MyStructms;structMyStruct{stringa1;stringa2;stringa3;stringa4;stringa5;stringa6;}

Benchmark for current impl vs using CORINFO_HELP_BOX (which is lowered down to memmoveGCRefs basically):

| Method | Toolchain | Mean |
|------- |---------------------------- |----------:|
| Box | \Core_Root_PR\corerun.exe | 5.832 ns |
| Box | \Core_Root_base\corerun.exe | 10.236 ns |

@MichalStrehovsky

Copy link
Copy Markdown
MemberAuthor

I presume Jan means that for a struct with many gc fields (JIT can check that) it's better for VM/Box helper to do a batch move (BulkMoveWithWriteBarrier) instead of individual write barriers

I wonder if this also means the JIT should use the batch move when assigning structs with many GC fields. Looks like there is a CORINFO_HELP_ASSIGN_STRUCT but I can't find any place in the JIT codebase where it would be used.

The method I'm changing here does two struct assignments with these large structs - one as part of the box, and the other as part of the non-boxing path. Looks like it might be beneficial to treat both cases the same in the JIT and if one is using CORINFO_HELP_BOX, the other should be using ASSIGN_STRUCT.

@EgorBo

EgorBo commented Apr 27, 2024

Copy link
Copy Markdown
Member

I wonder if this also means the JIT should use the batch move when assigning structs with many GC fields.

We discussed this in #99096 tldr: there is always a trade-off

E.g. the code we currently generate is interruption-friendly and is more precise from GC's point of view (we update card table for each gc handle in a struct)

@EgorBo

Copy link
Copy Markdown
Member

Fun fact: no-optimization also improves perf! 🙂

image

@jkotas

Copy link
Copy Markdown
Member

is more precise from GC's point of view (we update card table for each gc handle in a struct)

For box helper, this precision loss in card table updates should not be a concern. The object was just allocated and so it is very unlikely that will be any card table updates necessary. (There was even a discussion at one point about what it would take to skip the write barrier calls completely in cases like this.)

@MichalStrehovsky

Copy link
Copy Markdown
MemberAuthor

I made a draft PR to use the regular boxing helper above a certain number of GC fields at #101669. I hope we'll be able to tune the number with the bots/automation that exists in this repo. A struct with 6 fields, all of them GC fields should obviously use the box helper path but I'm less clear on the criteria for others (does mixing GC and non-GC fields make any difference, etc.?).

// generating this extra code until a better solution is implemented.
var box = new AsyncStateMachineBox<TStateMachine>();
// DebugFinalizableAsyncStateMachineBox looks like a small type, but it actually is not because
// it will have a copy of all the slots from its parent. It will add another hundred(s) bytes

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It might be out of scope for this PR, but how about using an unspecialized DebugFinalizableAsyncStateMachineBox<IAsyncStateMachine> for NativeAOT?

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.

Don't know much about this - if it's only used by managed debuggers, it would not be useful for native AOT anyway. It's ifdeffed out so I'm not concerned about it.

@github-actionsgithub-actionsBot mentioned this pull request May 2, 2024
@MichalStrehovsky
MichalStrehovsky merged commit 507abdf into dotnet:mainMay 2, 2024
@MichalStrehovsky
MichalStrehovsky deleted the async branch May 2, 2024 09:23
michaelgsharp pushed a commit to michaelgsharp/runtime that referenced this pull request May 9, 2024
Contributes to dotnet#79204.
I don't know if this will be considered mergeable, but I wanted to at least try something. For apps that use async a lot (like the Stage2 app we use for Goldilocks), the async infrastructure can cost 10% of the entire executable.
Shuffling a couple things in `GetStateMachineBox` I was able to get 0.23% saving. It's miniscule. In general async is death by a thousand papercuts so I don't see a silver bullet.
Ruihan-Yin pushed a commit to Ruihan-Yin/runtime that referenced this pull request May 30, 2024
Contributes to dotnet#79204.
I don't know if this will be considered mergeable, but I wanted to at least try something. For apps that use async a lot (like the Stage2 app we use for Goldilocks), the async infrastructure can cost 10% of the entire executable.
Shuffling a couple things in `GetStateMachineBox` I was able to get 0.23% saving. It's miniscule. In general async is death by a thousand papercuts so I don't see a silver bullet.
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jun 2, 2024
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.

5 participants

@MichalStrehovsky@jkotas@EgorBo@stephentoub@teo-tsirpanis
, '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

Try to reduce cost of Async - #101605

Merged
MichalStrehovsky merged 2 commits into
dotnet:mainfrom
MichalStrehovsky:async
May 2, 2024
Merged

Try to reduce cost of Async#101605
MichalStrehovsky merged 2 commits into
dotnet:mainfrom
MichalStrehovsky:async

Conversation

@MichalStrehovsky

Copy link
Copy Markdown
Member

Contributes to #79204.

I don't know if this will be considered mergeable, but I wanted to at least try something. For apps that use async a lot (like the Stage2 app we use for Goldilocks), the async infrastructure can cost 10% of the entire executable.

Shuffling a couple things in GetStateMachineBox I was able to get 0.23% saving. It's miniscule. In general async is death by a thousand papercuts so I don't see a silver bullet.

Click to expand existing assembly
 TodosApi!System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<System.Threading.Tasks.VoidTaskResult>__GetStateMachineBox<S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>:00000001`409833704156pushr1400000001`4098337257pushrdi00000001`4098337356pushrsi00000001`4098337455pushrbp00000001`4098337553pushrbx00000001`40983376 4883ec20 subrsp,20h00000001`4098337a 488bf1 movrsi, stateMachine (rcx)00000001`4098337d 488bda movrbx, taskField (rdx)00000001`40983380 e8eb68aaff call TodosApi!System.Threading.ExecutionContext__Capture (140429c70)00000001`40983385 488be8 movrbp,rax00000001`40983388 488b3b movrdi, qword ptr [taskField (rbx)]00000001`4098338b 4885ff testrdi,rdi00000001`4098338e 742c je TodosApi!System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<System.Threading.Tasks.VoidTaskResult>__GetStateMachineBox<S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>+0x4c (1409833bc)00000001`40983390 488d0df93f3e00 learcx,[TodosApi!??_7System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1_AsyncStateMachineBox`1<System.Threading.Tasks.VoidTaskResult__S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>@@6B@ (140d67390)]00000001`40983397 48390f cmp qword ptr [rdi],rcx00000001`4098339a 7520jne TodosApi!System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<System.Threading.Tasks.VoidTaskResult>__GetStateMachineBox<S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>+0x4c (1409833bc)00000001`4098339c 48396f10 cmp qword ptr [stronglyTypedBox (rdi)+10h], currentContext (rbp)00000001`409833a0 740c je TodosApi!System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<System.Threading.Tasks.VoidTaskResult>__GetStateMachineBox<S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>+0x3e (1409833ae)00000001`409833a2 488d4f10 learcx,[stronglyTypedBox (rdi)+10h]00000001`409833a6 488bd5 movrdx,rbp00000001`409833a9 e8921a69ff call TodosApi!RhpAssignRefAVLocation (140014e40)00000001`409833ae 488bc7 movrax,rdi00000001`409833b1 4883c420 addrsp,20h00000001`409833b5 5b poprbx00000001`409833b6 5d poprbp00000001`409833b7 5e poprsi00000001`409833b8 5f poprdi00000001`409833b9 415e popr1400000001`409833bb c3 ret00000001`409833bc 4c8b33 movr14, qword ptr [taskField (rbx)]00000001`409833bf 4d85f6 testr14,r1400000001`409833c2 7461je TodosApi!System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<System.Threading.Tasks.VoidTaskResult>__GetStateMachineBox<S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>+0xb5 (140983425)00000001`409833c4 488d0d7d193e00 learcx,[TodosApi!??_7System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1_AsyncStateMachineBox`1<System.Threading.Tasks.VoidTaskResult__System.Runtime.CompilerServices.IAsyncStateMachine>@@6B@ (140d64d48)]00000001`409833cb 49390e cmp qword ptr [r14],rcx00000001`409833ce 7555jne TodosApi!System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<System.Threading.Tasks.VoidTaskResult>__GetStateMachineBox<S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>+0xb5 (140983425)00000001`409833d0 49837e4000 cmp qword ptr [weaklyTypedBox (r14)+40h],000000001`409833d5 7534jne TodosApi!System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<System.Threading.Tasks.VoidTaskResult>__GetStateMachineBox<S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>+0x9b (14098340b)00000001`409833d7 488d0d1a622a00 learcx,[TodosApi!Boxed_S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100::`vftable' (140c295f8)]00000001`409833de e84d1869ff call TodosApi!RhpNewFast (140014c30)00000001`409833e3 488bd0 movrdx,rax00000001`409833e3 488bd0 movrdx,rax00000001`409833e6 488d7a08 leardi,[rdx+8]00000001`409833ea e8411c69ff call TodosApi!RhpByRefAssignRefAVLocation1 (140015030)00000001`409833ef e83c1c69ff call TodosApi!RhpByRefAssignRefAVLocation1 (140015030)00000001`409833f4 48a5 movs qword ptr [rdi], qword ptr [rsi]00000001`409833f6 e8351c69ff call TodosApi!RhpByRefAssignRefAVLocation1 (140015030)00000001`409833fb e8301c69ff call TodosApi!RhpByRefAssignRefAVLocation1 (140015030)00000001`40983400 48a5 movs qword ptr [rdi], qword ptr [rsi]00000001`40983402 498d4e40 learcx,[weaklyTypedBox (r14)+40h]00000001`40983406 e8351a69ff call TodosApi!RhpAssignRefAVLocation (140014e40)00000001`4098340b 498d4e10 learcx,[weaklyTypedBox (r14)+10h]00000001`4098340f 488bd5 movrdx,rbp00000001`40983412 e8291a69ff call TodosApi!RhpAssignRefAVLocation (140014e40)00000001`40983417 498bc6 movrax,r1400000001`4098341a 4883c420 addrsp,20h00000001`4098341e 5b poprbx00000001`4098341f 5d poprbp00000001`40983420 5e poprsi00000001`40983421 5f poprdi00000001`40983422 415e popr1400000001`40983424 c3 ret00000001`40983425 488d0d643f3e00 learcx,[TodosApi!??_7System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1_AsyncStateMachineBox`1<System.Threading.Tasks.VoidTaskResult__S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>@@6B@ (140d67390)]00000001`4098342c e8ff1769ff call TodosApi!RhpNewFast (140014c30)00000001`40983431 4c8bf0 movr14,rax00000001`40983434 41c7463400040002 mov dword ptr [r14+34h],2000400h00000001`4098343c 41814e3400080000 or dword ptr [r14+34h],800h00000001`40983444 488bcb movrcx,rbx00000001`40983447 498bd6 movrdx,r1400000001`4098344a e8611a69ff call TodosApi!RhpCheckedAssignRefAVLocation (140014eb0)00000001`4098344f 498d7e40 leardi,[r14+40h]00000001`40983453 e8d81b69ff call TodosApi!RhpByRefAssignRefAVLocation1 (140015030)00000001`40983458 e8d31b69ff call TodosApi!RhpByRefAssignRefAVLocation1 (140015030)00000001`4098345d 48a5 movs qword ptr [rdi], qword ptr [rsi]00000001`4098345f e8cc1b69ff call TodosApi!RhpByRefAssignRefAVLocation1 (140015030)00000001`40983464 e8c71b69ff call TodosApi!RhpByRefAssignRefAVLocation1 (140015030)00000001`40983469 48a5 movs qword ptr [rdi], qword ptr [rsi]00000001`4098346b 498d4e10 learcx,[r14+10h]00000001`4098346f 488bd5 movrdx,rbp00000001`40983472 e8c91969ff call TodosApi!RhpAssignRefAVLocation (140014e40)00000001`40983477 488d0dea6f1f00 learcx,[TodosApi!?__NONGCSTATICS@System.Threading.Tasks.TplEventSource@@ (140b7a468)]00000001`4098347e 488379f800 cmp qword ptr [rcx-8],000000001`409834837559jne TodosApi!System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<System.Threading.Tasks.VoidTaskResult>__GetStateMachineBox<S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>+0x16e (1409834de)00000001`40983485 488b0d94b0c400 movrcx, qword ptr [TodosApi!?__GCSTATICS@System.Threading.Tasks.TplEventSource@@ (1415ce520)]00000001`4098348c 488b5908 movrbx, qword ptr [rcx+8]00000001`40983490 80bb9d00000000 cmp byte ptr [rbx+9Dh],000000001`409834977437je TodosApi!System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<System.Threading.Tasks.VoidTaskResult>__GetStateMachineBox<S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>+0x160 (1409834d0)00000001`40983499 498bce movrcx,r1400000001`4098349c e83f27abff call TodosApi!System.Threading.Tasks.Task__get_Id (140435be0)00000001`409834a1 8bf0 movesi,eax00000001`409834a3 488d0de68c1a00 learcx,[TodosApi!__RuntimeType_S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100 (140b2c190)]00000001`409834aa e85161a4ff call TodosApi!System.RuntimeType__get_Name (1403c9600)00000001`409834af 488bd0 movrdx,rax00000001`409834b2 488d0dff5c0e00 learcx,[TodosApi!__Str_Async___4E51D638FE527E2B0672DE04B6E5F6B84BA81E64E12AA0136B0691A50E6F80FE (140a691b8)]00000001`409834b9 e89285a4ff call TodosApi!String__Concat_6 (1403cba50)00000001`409834be 4c8bc0 movr8,rax00000001`409834c1 488bcb movrcx,rbx00000001`409834c4 8bd6 movedx,esi00000001`409834c6 4533c9 xorr9d,r9d00000001`409834c9 3909cmp dword ptr [rcx],ecx00000001`409834cb e8608cabff call TodosApi!System.Threading.Tasks.TplEventSource__TraceOperationBegin (14043c130)00000001`409834d0 498bc6 movrax,r1400000001`409834d3 4883c420 addrsp,20h00000001`409834d7 5b poprbx00000001`409834d8 5d poprbp00000001`409834d9 5e poprsi00000001`409834da 5f poprdi00000001`409834db 415e popr1400000001`409834dd c3 ret00000001`409834de e86a0f68ff call TodosApi!__GetGCStaticBase_System.Threading.Tasks.TplEventSource (14000444d)00000001`409834e3 eba0 jmp TodosApi!System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<System.Threading.Tasks.VoidTaskResult>__GetStateMachineBox<S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>+0x115 (140983485)

Addresses following problems:

  • Multiple expensive epilogs
  • Copying the state machine is costly because they tend to be huge and are a mix of reference fields (need write barriers) and value fields. We need to copy twice - one is an assignment, the other is a box. See clusters of RhpByRefAssignRefAVLocation1 - that's the write barrier. I'm making the box take a less efficient path based on comments.
  • Logging is constructing a string that doesn't really need to be in generic code.

This is just a couple dozen bytes in savings per method, but ends up saving 50 kB for the whole app because that's how many specializations of the method we have.

Cc @stephentoub@jkotas for thoughts.

Contributes to dotnet#79204.
I don't know if this will be considered mergeable, but I wanted to at least try something. For apps that use async a lot (like the Stage2 app we use for Goldilocks), the async infrastructure can cost 10% of the entire executable.
Shuffling a couple things in `GetStateMachineBox` I was able to get 0.23% saving. It's miniscule. In general async is death by a thousand papercuts so I don't see a silver bullet.
@jkotas

Copy link
Copy Markdown
Member

Copying the state machine is costly because they tend to be huge and are a mix of reference fields (need write barriers) and value fields

That's because RyuJIT inlines the box helper for large structs. I think this should be fixed in the JIT.

Outlining of the event tracing helper looks good.

if (taskField is AsyncStateMachineBox<IAsyncStateMachine> weaklyTypedBox)
else if (taskField is AsyncStateMachineBox<IAsyncStateMachine> weaklyTypedBox)
{
// If this is the first await, we won't yet have a state machine, so store it.

@stephentoubstephentoubApr 26, 2024

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.

If we're trying to squeeze water from a stone, this whole block should be extremely cold (only used in debugging scenarios), so if you can come up with a good way to outline it all to a separate helper, that'd be reasonable. Not sure if that'd help much, though, given that stateMachine is generic.

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.

Yeah, it would likely make things worse because it's still the same code, but with extra unwinding/GC tracking data structures because we have a new method.

@MichalStrehovsky

Copy link
Copy Markdown
MemberAuthor

That's because RyuJIT inlines the box helper for large structs. I think this should be fixed in the JIT.

Do you have a heuristic in mind? It seems like what RyuJIT is doing now is strictly better from throughput perspective (we call the right allocation helper, assign all the fields, only do write barriers for GC pointers). Falling back to the regular boxing helper would be slower. Or are you thinking about a object RuntimeHelpers.Box<T>(ref T memory) that would be like a box, but guaranteed not to be expanded into alloc+set individual fields.

@EgorBo

EgorBo commented Apr 26, 2024

Copy link
Copy Markdown
Member

That's because RyuJIT inlines the box helper for large structs. I think this should be fixed in the JIT.

Do you have a heuristic in mind? It seems like what RyuJIT is doing now is strictly better from throughput perspective (we call the right allocation helper, assign all the fields, only do write barriers for GC pointers). Falling back to the regular boxing helper would be slower. Or are you thinking about a object RuntimeHelpers.Box<T>(ref T memory) that would be like a box, but guaranteed not to be expanded into alloc+set individual fields.

I presume Jan means that for a struct with many gc fields (JIT can check that) it's better for VM/Box helper to do a batch move (BulkMoveWithWriteBarrier) instead of individual write barriers

@jkotas

Copy link
Copy Markdown
Member

It seems like what RyuJIT is doing now is strictly better from throughput perspective

I expect that the bulk copy in the box helper starts to win in throughput above certain size. The JIT should probably give up inlining the copy way below this threshold for good code size / throughput tradeoff.

Also, the current implementation of the box is slow since the box helper is almost never used today. Jeremy hit that problem when switching a few places in reflection to use the box helper (WIP #101137).

@EgorBo

Copy link
Copy Markdown
Member

Quick test:

[Benchmark]publicobjectBox()=>ms;MyStructms;structMyStruct{stringa1;stringa2;stringa3;stringa4;stringa5;stringa6;}

Benchmark for current impl vs using CORINFO_HELP_BOX (which is lowered down to memmoveGCRefs basically):

| Method | Toolchain | Mean |
|------- |---------------------------- |----------:|
| Box | \Core_Root_PR\corerun.exe | 5.832 ns |
| Box | \Core_Root_base\corerun.exe | 10.236 ns |

@MichalStrehovsky

Copy link
Copy Markdown
MemberAuthor

I presume Jan means that for a struct with many gc fields (JIT can check that) it's better for VM/Box helper to do a batch move (BulkMoveWithWriteBarrier) instead of individual write barriers

I wonder if this also means the JIT should use the batch move when assigning structs with many GC fields. Looks like there is a CORINFO_HELP_ASSIGN_STRUCT but I can't find any place in the JIT codebase where it would be used.

The method I'm changing here does two struct assignments with these large structs - one as part of the box, and the other as part of the non-boxing path. Looks like it might be beneficial to treat both cases the same in the JIT and if one is using CORINFO_HELP_BOX, the other should be using ASSIGN_STRUCT.

@EgorBo

EgorBo commented Apr 27, 2024

Copy link
Copy Markdown
Member

I wonder if this also means the JIT should use the batch move when assigning structs with many GC fields.

We discussed this in #99096 tldr: there is always a trade-off

E.g. the code we currently generate is interruption-friendly and is more precise from GC's point of view (we update card table for each gc handle in a struct)

@EgorBo

Copy link
Copy Markdown
Member

Fun fact: no-optimization also improves perf! 🙂

image

@jkotas

Copy link
Copy Markdown
Member

is more precise from GC's point of view (we update card table for each gc handle in a struct)

For box helper, this precision loss in card table updates should not be a concern. The object was just allocated and so it is very unlikely that will be any card table updates necessary. (There was even a discussion at one point about what it would take to skip the write barrier calls completely in cases like this.)

@MichalStrehovsky

Copy link
Copy Markdown
MemberAuthor

I made a draft PR to use the regular boxing helper above a certain number of GC fields at #101669. I hope we'll be able to tune the number with the bots/automation that exists in this repo. A struct with 6 fields, all of them GC fields should obviously use the box helper path but I'm less clear on the criteria for others (does mixing GC and non-GC fields make any difference, etc.?).

// generating this extra code until a better solution is implemented.
var box = new AsyncStateMachineBox<TStateMachine>();
// DebugFinalizableAsyncStateMachineBox looks like a small type, but it actually is not because
// it will have a copy of all the slots from its parent. It will add another hundred(s) bytes

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It might be out of scope for this PR, but how about using an unspecialized DebugFinalizableAsyncStateMachineBox<IAsyncStateMachine> for NativeAOT?

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.

Don't know much about this - if it's only used by managed debuggers, it would not be useful for native AOT anyway. It's ifdeffed out so I'm not concerned about it.

@github-actionsgithub-actionsBot mentioned this pull request May 2, 2024
@MichalStrehovsky
MichalStrehovsky merged commit 507abdf into dotnet:mainMay 2, 2024
@MichalStrehovsky
MichalStrehovsky deleted the async branch May 2, 2024 09:23
michaelgsharp pushed a commit to michaelgsharp/runtime that referenced this pull request May 9, 2024
Contributes to dotnet#79204.
I don't know if this will be considered mergeable, but I wanted to at least try something. For apps that use async a lot (like the Stage2 app we use for Goldilocks), the async infrastructure can cost 10% of the entire executable.
Shuffling a couple things in `GetStateMachineBox` I was able to get 0.23% saving. It's miniscule. In general async is death by a thousand papercuts so I don't see a silver bullet.
Ruihan-Yin pushed a commit to Ruihan-Yin/runtime that referenced this pull request May 30, 2024
Contributes to dotnet#79204.
I don't know if this will be considered mergeable, but I wanted to at least try something. For apps that use async a lot (like the Stage2 app we use for Goldilocks), the async infrastructure can cost 10% of the entire executable.
Shuffling a couple things in `GetStateMachineBox` I was able to get 0.23% saving. It's miniscule. In general async is death by a thousand papercuts so I don't see a silver bullet.
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jun 2, 2024
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.

5 participants

@MichalStrehovsky@jkotas@EgorBo@stephentoub@teo-tsirpanis
, '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

Try to reduce cost of Async - #101605

Merged
MichalStrehovsky merged 2 commits into
dotnet:mainfrom
MichalStrehovsky:async
May 2, 2024
Merged

Try to reduce cost of Async#101605
MichalStrehovsky merged 2 commits into
dotnet:mainfrom
MichalStrehovsky:async

Conversation

@MichalStrehovsky

Copy link
Copy Markdown
Member

Contributes to #79204.

I don't know if this will be considered mergeable, but I wanted to at least try something. For apps that use async a lot (like the Stage2 app we use for Goldilocks), the async infrastructure can cost 10% of the entire executable.

Shuffling a couple things in GetStateMachineBox I was able to get 0.23% saving. It's miniscule. In general async is death by a thousand papercuts so I don't see a silver bullet.

Click to expand existing assembly
 TodosApi!System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<System.Threading.Tasks.VoidTaskResult>__GetStateMachineBox<S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>:00000001`409833704156pushr1400000001`4098337257pushrdi00000001`4098337356pushrsi00000001`4098337455pushrbp00000001`4098337553pushrbx00000001`40983376 4883ec20 subrsp,20h00000001`4098337a 488bf1 movrsi, stateMachine (rcx)00000001`4098337d 488bda movrbx, taskField (rdx)00000001`40983380 e8eb68aaff call TodosApi!System.Threading.ExecutionContext__Capture (140429c70)00000001`40983385 488be8 movrbp,rax00000001`40983388 488b3b movrdi, qword ptr [taskField (rbx)]00000001`4098338b 4885ff testrdi,rdi00000001`4098338e 742c je TodosApi!System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<System.Threading.Tasks.VoidTaskResult>__GetStateMachineBox<S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>+0x4c (1409833bc)00000001`40983390 488d0df93f3e00 learcx,[TodosApi!??_7System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1_AsyncStateMachineBox`1<System.Threading.Tasks.VoidTaskResult__S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>@@6B@ (140d67390)]00000001`40983397 48390f cmp qword ptr [rdi],rcx00000001`4098339a 7520jne TodosApi!System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<System.Threading.Tasks.VoidTaskResult>__GetStateMachineBox<S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>+0x4c (1409833bc)00000001`4098339c 48396f10 cmp qword ptr [stronglyTypedBox (rdi)+10h], currentContext (rbp)00000001`409833a0 740c je TodosApi!System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<System.Threading.Tasks.VoidTaskResult>__GetStateMachineBox<S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>+0x3e (1409833ae)00000001`409833a2 488d4f10 learcx,[stronglyTypedBox (rdi)+10h]00000001`409833a6 488bd5 movrdx,rbp00000001`409833a9 e8921a69ff call TodosApi!RhpAssignRefAVLocation (140014e40)00000001`409833ae 488bc7 movrax,rdi00000001`409833b1 4883c420 addrsp,20h00000001`409833b5 5b poprbx00000001`409833b6 5d poprbp00000001`409833b7 5e poprsi00000001`409833b8 5f poprdi00000001`409833b9 415e popr1400000001`409833bb c3 ret00000001`409833bc 4c8b33 movr14, qword ptr [taskField (rbx)]00000001`409833bf 4d85f6 testr14,r1400000001`409833c2 7461je TodosApi!System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<System.Threading.Tasks.VoidTaskResult>__GetStateMachineBox<S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>+0xb5 (140983425)00000001`409833c4 488d0d7d193e00 learcx,[TodosApi!??_7System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1_AsyncStateMachineBox`1<System.Threading.Tasks.VoidTaskResult__System.Runtime.CompilerServices.IAsyncStateMachine>@@6B@ (140d64d48)]00000001`409833cb 49390e cmp qword ptr [r14],rcx00000001`409833ce 7555jne TodosApi!System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<System.Threading.Tasks.VoidTaskResult>__GetStateMachineBox<S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>+0xb5 (140983425)00000001`409833d0 49837e4000 cmp qword ptr [weaklyTypedBox (r14)+40h],000000001`409833d5 7534jne TodosApi!System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<System.Threading.Tasks.VoidTaskResult>__GetStateMachineBox<S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>+0x9b (14098340b)00000001`409833d7 488d0d1a622a00 learcx,[TodosApi!Boxed_S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100::`vftable' (140c295f8)]00000001`409833de e84d1869ff call TodosApi!RhpNewFast (140014c30)00000001`409833e3 488bd0 movrdx,rax00000001`409833e3 488bd0 movrdx,rax00000001`409833e6 488d7a08 leardi,[rdx+8]00000001`409833ea e8411c69ff call TodosApi!RhpByRefAssignRefAVLocation1 (140015030)00000001`409833ef e83c1c69ff call TodosApi!RhpByRefAssignRefAVLocation1 (140015030)00000001`409833f4 48a5 movs qword ptr [rdi], qword ptr [rsi]00000001`409833f6 e8351c69ff call TodosApi!RhpByRefAssignRefAVLocation1 (140015030)00000001`409833fb e8301c69ff call TodosApi!RhpByRefAssignRefAVLocation1 (140015030)00000001`40983400 48a5 movs qword ptr [rdi], qword ptr [rsi]00000001`40983402 498d4e40 learcx,[weaklyTypedBox (r14)+40h]00000001`40983406 e8351a69ff call TodosApi!RhpAssignRefAVLocation (140014e40)00000001`4098340b 498d4e10 learcx,[weaklyTypedBox (r14)+10h]00000001`4098340f 488bd5 movrdx,rbp00000001`40983412 e8291a69ff call TodosApi!RhpAssignRefAVLocation (140014e40)00000001`40983417 498bc6 movrax,r1400000001`4098341a 4883c420 addrsp,20h00000001`4098341e 5b poprbx00000001`4098341f 5d poprbp00000001`40983420 5e poprsi00000001`40983421 5f poprdi00000001`40983422 415e popr1400000001`40983424 c3 ret00000001`40983425 488d0d643f3e00 learcx,[TodosApi!??_7System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1_AsyncStateMachineBox`1<System.Threading.Tasks.VoidTaskResult__S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>@@6B@ (140d67390)]00000001`4098342c e8ff1769ff call TodosApi!RhpNewFast (140014c30)00000001`40983431 4c8bf0 movr14,rax00000001`40983434 41c7463400040002 mov dword ptr [r14+34h],2000400h00000001`4098343c 41814e3400080000 or dword ptr [r14+34h],800h00000001`40983444 488bcb movrcx,rbx00000001`40983447 498bd6 movrdx,r1400000001`4098344a e8611a69ff call TodosApi!RhpCheckedAssignRefAVLocation (140014eb0)00000001`4098344f 498d7e40 leardi,[r14+40h]00000001`40983453 e8d81b69ff call TodosApi!RhpByRefAssignRefAVLocation1 (140015030)00000001`40983458 e8d31b69ff call TodosApi!RhpByRefAssignRefAVLocation1 (140015030)00000001`4098345d 48a5 movs qword ptr [rdi], qword ptr [rsi]00000001`4098345f e8cc1b69ff call TodosApi!RhpByRefAssignRefAVLocation1 (140015030)00000001`40983464 e8c71b69ff call TodosApi!RhpByRefAssignRefAVLocation1 (140015030)00000001`40983469 48a5 movs qword ptr [rdi], qword ptr [rsi]00000001`4098346b 498d4e10 learcx,[r14+10h]00000001`4098346f 488bd5 movrdx,rbp00000001`40983472 e8c91969ff call TodosApi!RhpAssignRefAVLocation (140014e40)00000001`40983477 488d0dea6f1f00 learcx,[TodosApi!?__NONGCSTATICS@System.Threading.Tasks.TplEventSource@@ (140b7a468)]00000001`4098347e 488379f800 cmp qword ptr [rcx-8],000000001`409834837559jne TodosApi!System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<System.Threading.Tasks.VoidTaskResult>__GetStateMachineBox<S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>+0x16e (1409834de)00000001`40983485 488b0d94b0c400 movrcx, qword ptr [TodosApi!?__GCSTATICS@System.Threading.Tasks.TplEventSource@@ (1415ce520)]00000001`4098348c 488b5908 movrbx, qword ptr [rcx+8]00000001`40983490 80bb9d00000000 cmp byte ptr [rbx+9Dh],000000001`409834977437je TodosApi!System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<System.Threading.Tasks.VoidTaskResult>__GetStateMachineBox<S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>+0x160 (1409834d0)00000001`40983499 498bce movrcx,r1400000001`4098349c e83f27abff call TodosApi!System.Threading.Tasks.Task__get_Id (140435be0)00000001`409834a1 8bf0 movesi,eax00000001`409834a3 488d0de68c1a00 learcx,[TodosApi!__RuntimeType_S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100 (140b2c190)]00000001`409834aa e85161a4ff call TodosApi!System.RuntimeType__get_Name (1403c9600)00000001`409834af 488bd0 movrdx,rax00000001`409834b2 488d0dff5c0e00 learcx,[TodosApi!__Str_Async___4E51D638FE527E2B0672DE04B6E5F6B84BA81E64E12AA0136B0691A50E6F80FE (140a691b8)]00000001`409834b9 e89285a4ff call TodosApi!String__Concat_6 (1403cba50)00000001`409834be 4c8bc0 movr8,rax00000001`409834c1 488bcb movrcx,rbx00000001`409834c4 8bd6 movedx,esi00000001`409834c6 4533c9 xorr9d,r9d00000001`409834c9 3909cmp dword ptr [rcx],ecx00000001`409834cb e8608cabff call TodosApi!System.Threading.Tasks.TplEventSource__TraceOperationBegin (14043c130)00000001`409834d0 498bc6 movrax,r1400000001`409834d3 4883c420 addrsp,20h00000001`409834d7 5b poprbx00000001`409834d8 5d poprbp00000001`409834d9 5e poprsi00000001`409834da 5f poprdi00000001`409834db 415e popr1400000001`409834dd c3 ret00000001`409834de e86a0f68ff call TodosApi!__GetGCStaticBase_System.Threading.Tasks.TplEventSource (14000444d)00000001`409834e3 eba0 jmp TodosApi!System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<System.Threading.Tasks.VoidTaskResult>__GetStateMachineBox<S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>+0x115 (140983485)

Addresses following problems:

  • Multiple expensive epilogs
  • Copying the state machine is costly because they tend to be huge and are a mix of reference fields (need write barriers) and value fields. We need to copy twice - one is an assignment, the other is a box. See clusters of RhpByRefAssignRefAVLocation1 - that's the write barrier. I'm making the box take a less efficient path based on comments.
  • Logging is constructing a string that doesn't really need to be in generic code.

This is just a couple dozen bytes in savings per method, but ends up saving 50 kB for the whole app because that's how many specializations of the method we have.

Cc @stephentoub@jkotas for thoughts.

Contributes to dotnet#79204.
I don't know if this will be considered mergeable, but I wanted to at least try something. For apps that use async a lot (like the Stage2 app we use for Goldilocks), the async infrastructure can cost 10% of the entire executable.
Shuffling a couple things in `GetStateMachineBox` I was able to get 0.23% saving. It's miniscule. In general async is death by a thousand papercuts so I don't see a silver bullet.
@jkotas

Copy link
Copy Markdown
Member

Copying the state machine is costly because they tend to be huge and are a mix of reference fields (need write barriers) and value fields

That's because RyuJIT inlines the box helper for large structs. I think this should be fixed in the JIT.

Outlining of the event tracing helper looks good.

if (taskField is AsyncStateMachineBox<IAsyncStateMachine> weaklyTypedBox)
else if (taskField is AsyncStateMachineBox<IAsyncStateMachine> weaklyTypedBox)
{
// If this is the first await, we won't yet have a state machine, so store it.

@stephentoubstephentoubApr 26, 2024

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.

If we're trying to squeeze water from a stone, this whole block should be extremely cold (only used in debugging scenarios), so if you can come up with a good way to outline it all to a separate helper, that'd be reasonable. Not sure if that'd help much, though, given that stateMachine is generic.

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.

Yeah, it would likely make things worse because it's still the same code, but with extra unwinding/GC tracking data structures because we have a new method.

@MichalStrehovsky

Copy link
Copy Markdown
MemberAuthor

That's because RyuJIT inlines the box helper for large structs. I think this should be fixed in the JIT.

Do you have a heuristic in mind? It seems like what RyuJIT is doing now is strictly better from throughput perspective (we call the right allocation helper, assign all the fields, only do write barriers for GC pointers). Falling back to the regular boxing helper would be slower. Or are you thinking about a object RuntimeHelpers.Box<T>(ref T memory) that would be like a box, but guaranteed not to be expanded into alloc+set individual fields.

@EgorBo

EgorBo commented Apr 26, 2024

Copy link
Copy Markdown
Member

That's because RyuJIT inlines the box helper for large structs. I think this should be fixed in the JIT.

Do you have a heuristic in mind? It seems like what RyuJIT is doing now is strictly better from throughput perspective (we call the right allocation helper, assign all the fields, only do write barriers for GC pointers). Falling back to the regular boxing helper would be slower. Or are you thinking about a object RuntimeHelpers.Box<T>(ref T memory) that would be like a box, but guaranteed not to be expanded into alloc+set individual fields.

I presume Jan means that for a struct with many gc fields (JIT can check that) it's better for VM/Box helper to do a batch move (BulkMoveWithWriteBarrier) instead of individual write barriers

@jkotas

Copy link
Copy Markdown
Member

It seems like what RyuJIT is doing now is strictly better from throughput perspective

I expect that the bulk copy in the box helper starts to win in throughput above certain size. The JIT should probably give up inlining the copy way below this threshold for good code size / throughput tradeoff.

Also, the current implementation of the box is slow since the box helper is almost never used today. Jeremy hit that problem when switching a few places in reflection to use the box helper (WIP #101137).

@EgorBo

Copy link
Copy Markdown
Member

Quick test:

[Benchmark]publicobjectBox()=>ms;MyStructms;structMyStruct{stringa1;stringa2;stringa3;stringa4;stringa5;stringa6;}

Benchmark for current impl vs using CORINFO_HELP_BOX (which is lowered down to memmoveGCRefs basically):

| Method | Toolchain | Mean |
|------- |---------------------------- |----------:|
| Box | \Core_Root_PR\corerun.exe | 5.832 ns |
| Box | \Core_Root_base\corerun.exe | 10.236 ns |

@MichalStrehovsky

Copy link
Copy Markdown
MemberAuthor

I presume Jan means that for a struct with many gc fields (JIT can check that) it's better for VM/Box helper to do a batch move (BulkMoveWithWriteBarrier) instead of individual write barriers

I wonder if this also means the JIT should use the batch move when assigning structs with many GC fields. Looks like there is a CORINFO_HELP_ASSIGN_STRUCT but I can't find any place in the JIT codebase where it would be used.

The method I'm changing here does two struct assignments with these large structs - one as part of the box, and the other as part of the non-boxing path. Looks like it might be beneficial to treat both cases the same in the JIT and if one is using CORINFO_HELP_BOX, the other should be using ASSIGN_STRUCT.

@EgorBo

EgorBo commented Apr 27, 2024

Copy link
Copy Markdown
Member

I wonder if this also means the JIT should use the batch move when assigning structs with many GC fields.

We discussed this in #99096 tldr: there is always a trade-off

E.g. the code we currently generate is interruption-friendly and is more precise from GC's point of view (we update card table for each gc handle in a struct)

@EgorBo

Copy link
Copy Markdown
Member

Fun fact: no-optimization also improves perf! 🙂

image

@jkotas

Copy link
Copy Markdown
Member

is more precise from GC's point of view (we update card table for each gc handle in a struct)

For box helper, this precision loss in card table updates should not be a concern. The object was just allocated and so it is very unlikely that will be any card table updates necessary. (There was even a discussion at one point about what it would take to skip the write barrier calls completely in cases like this.)

@MichalStrehovsky

Copy link
Copy Markdown
MemberAuthor

I made a draft PR to use the regular boxing helper above a certain number of GC fields at #101669. I hope we'll be able to tune the number with the bots/automation that exists in this repo. A struct with 6 fields, all of them GC fields should obviously use the box helper path but I'm less clear on the criteria for others (does mixing GC and non-GC fields make any difference, etc.?).

// generating this extra code until a better solution is implemented.
var box = new AsyncStateMachineBox<TStateMachine>();
// DebugFinalizableAsyncStateMachineBox looks like a small type, but it actually is not because
// it will have a copy of all the slots from its parent. It will add another hundred(s) bytes

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It might be out of scope for this PR, but how about using an unspecialized DebugFinalizableAsyncStateMachineBox<IAsyncStateMachine> for NativeAOT?

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.

Don't know much about this - if it's only used by managed debuggers, it would not be useful for native AOT anyway. It's ifdeffed out so I'm not concerned about it.

@github-actionsgithub-actionsBot mentioned this pull request May 2, 2024
@MichalStrehovsky
MichalStrehovsky merged commit 507abdf into dotnet:mainMay 2, 2024
@MichalStrehovsky
MichalStrehovsky deleted the async branch May 2, 2024 09:23
michaelgsharp pushed a commit to michaelgsharp/runtime that referenced this pull request May 9, 2024
Contributes to dotnet#79204.
I don't know if this will be considered mergeable, but I wanted to at least try something. For apps that use async a lot (like the Stage2 app we use for Goldilocks), the async infrastructure can cost 10% of the entire executable.
Shuffling a couple things in `GetStateMachineBox` I was able to get 0.23% saving. It's miniscule. In general async is death by a thousand papercuts so I don't see a silver bullet.
Ruihan-Yin pushed a commit to Ruihan-Yin/runtime that referenced this pull request May 30, 2024
Contributes to dotnet#79204.
I don't know if this will be considered mergeable, but I wanted to at least try something. For apps that use async a lot (like the Stage2 app we use for Goldilocks), the async infrastructure can cost 10% of the entire executable.
Shuffling a couple things in `GetStateMachineBox` I was able to get 0.23% saving. It's miniscule. In general async is death by a thousand papercuts so I don't see a silver bullet.
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jun 2, 2024
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.

5 participants

@MichalStrehovsky@jkotas@EgorBo@stephentoub@teo-tsirpanis
, '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

Try to reduce cost of Async - #101605

Merged
MichalStrehovsky merged 2 commits into
dotnet:mainfrom
MichalStrehovsky:async
May 2, 2024
Merged

Try to reduce cost of Async#101605
MichalStrehovsky merged 2 commits into
dotnet:mainfrom
MichalStrehovsky:async

Conversation

@MichalStrehovsky

Copy link
Copy Markdown
Member

Contributes to #79204.

I don't know if this will be considered mergeable, but I wanted to at least try something. For apps that use async a lot (like the Stage2 app we use for Goldilocks), the async infrastructure can cost 10% of the entire executable.

Shuffling a couple things in GetStateMachineBox I was able to get 0.23% saving. It's miniscule. In general async is death by a thousand papercuts so I don't see a silver bullet.

Click to expand existing assembly
 TodosApi!System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<System.Threading.Tasks.VoidTaskResult>__GetStateMachineBox<S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>:00000001`409833704156pushr1400000001`4098337257pushrdi00000001`4098337356pushrsi00000001`4098337455pushrbp00000001`4098337553pushrbx00000001`40983376 4883ec20 subrsp,20h00000001`4098337a 488bf1 movrsi, stateMachine (rcx)00000001`4098337d 488bda movrbx, taskField (rdx)00000001`40983380 e8eb68aaff call TodosApi!System.Threading.ExecutionContext__Capture (140429c70)00000001`40983385 488be8 movrbp,rax00000001`40983388 488b3b movrdi, qword ptr [taskField (rbx)]00000001`4098338b 4885ff testrdi,rdi00000001`4098338e 742c je TodosApi!System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<System.Threading.Tasks.VoidTaskResult>__GetStateMachineBox<S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>+0x4c (1409833bc)00000001`40983390 488d0df93f3e00 learcx,[TodosApi!??_7System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1_AsyncStateMachineBox`1<System.Threading.Tasks.VoidTaskResult__S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>@@6B@ (140d67390)]00000001`40983397 48390f cmp qword ptr [rdi],rcx00000001`4098339a 7520jne TodosApi!System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<System.Threading.Tasks.VoidTaskResult>__GetStateMachineBox<S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>+0x4c (1409833bc)00000001`4098339c 48396f10 cmp qword ptr [stronglyTypedBox (rdi)+10h], currentContext (rbp)00000001`409833a0 740c je TodosApi!System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<System.Threading.Tasks.VoidTaskResult>__GetStateMachineBox<S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>+0x3e (1409833ae)00000001`409833a2 488d4f10 learcx,[stronglyTypedBox (rdi)+10h]00000001`409833a6 488bd5 movrdx,rbp00000001`409833a9 e8921a69ff call TodosApi!RhpAssignRefAVLocation (140014e40)00000001`409833ae 488bc7 movrax,rdi00000001`409833b1 4883c420 addrsp,20h00000001`409833b5 5b poprbx00000001`409833b6 5d poprbp00000001`409833b7 5e poprsi00000001`409833b8 5f poprdi00000001`409833b9 415e popr1400000001`409833bb c3 ret00000001`409833bc 4c8b33 movr14, qword ptr [taskField (rbx)]00000001`409833bf 4d85f6 testr14,r1400000001`409833c2 7461je TodosApi!System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<System.Threading.Tasks.VoidTaskResult>__GetStateMachineBox<S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>+0xb5 (140983425)00000001`409833c4 488d0d7d193e00 learcx,[TodosApi!??_7System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1_AsyncStateMachineBox`1<System.Threading.Tasks.VoidTaskResult__System.Runtime.CompilerServices.IAsyncStateMachine>@@6B@ (140d64d48)]00000001`409833cb 49390e cmp qword ptr [r14],rcx00000001`409833ce 7555jne TodosApi!System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<System.Threading.Tasks.VoidTaskResult>__GetStateMachineBox<S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>+0xb5 (140983425)00000001`409833d0 49837e4000 cmp qword ptr [weaklyTypedBox (r14)+40h],000000001`409833d5 7534jne TodosApi!System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<System.Threading.Tasks.VoidTaskResult>__GetStateMachineBox<S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>+0x9b (14098340b)00000001`409833d7 488d0d1a622a00 learcx,[TodosApi!Boxed_S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100::`vftable' (140c295f8)]00000001`409833de e84d1869ff call TodosApi!RhpNewFast (140014c30)00000001`409833e3 488bd0 movrdx,rax00000001`409833e3 488bd0 movrdx,rax00000001`409833e6 488d7a08 leardi,[rdx+8]00000001`409833ea e8411c69ff call TodosApi!RhpByRefAssignRefAVLocation1 (140015030)00000001`409833ef e83c1c69ff call TodosApi!RhpByRefAssignRefAVLocation1 (140015030)00000001`409833f4 48a5 movs qword ptr [rdi], qword ptr [rsi]00000001`409833f6 e8351c69ff call TodosApi!RhpByRefAssignRefAVLocation1 (140015030)00000001`409833fb e8301c69ff call TodosApi!RhpByRefAssignRefAVLocation1 (140015030)00000001`40983400 48a5 movs qword ptr [rdi], qword ptr [rsi]00000001`40983402 498d4e40 learcx,[weaklyTypedBox (r14)+40h]00000001`40983406 e8351a69ff call TodosApi!RhpAssignRefAVLocation (140014e40)00000001`4098340b 498d4e10 learcx,[weaklyTypedBox (r14)+10h]00000001`4098340f 488bd5 movrdx,rbp00000001`40983412 e8291a69ff call TodosApi!RhpAssignRefAVLocation (140014e40)00000001`40983417 498bc6 movrax,r1400000001`4098341a 4883c420 addrsp,20h00000001`4098341e 5b poprbx00000001`4098341f 5d poprbp00000001`40983420 5e poprsi00000001`40983421 5f poprdi00000001`40983422 415e popr1400000001`40983424 c3 ret00000001`40983425 488d0d643f3e00 learcx,[TodosApi!??_7System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1_AsyncStateMachineBox`1<System.Threading.Tasks.VoidTaskResult__S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>@@6B@ (140d67390)]00000001`4098342c e8ff1769ff call TodosApi!RhpNewFast (140014c30)00000001`40983431 4c8bf0 movr14,rax00000001`40983434 41c7463400040002 mov dword ptr [r14+34h],2000400h00000001`4098343c 41814e3400080000 or dword ptr [r14+34h],800h00000001`40983444 488bcb movrcx,rbx00000001`40983447 498bd6 movrdx,r1400000001`4098344a e8611a69ff call TodosApi!RhpCheckedAssignRefAVLocation (140014eb0)00000001`4098344f 498d7e40 leardi,[r14+40h]00000001`40983453 e8d81b69ff call TodosApi!RhpByRefAssignRefAVLocation1 (140015030)00000001`40983458 e8d31b69ff call TodosApi!RhpByRefAssignRefAVLocation1 (140015030)00000001`4098345d 48a5 movs qword ptr [rdi], qword ptr [rsi]00000001`4098345f e8cc1b69ff call TodosApi!RhpByRefAssignRefAVLocation1 (140015030)00000001`40983464 e8c71b69ff call TodosApi!RhpByRefAssignRefAVLocation1 (140015030)00000001`40983469 48a5 movs qword ptr [rdi], qword ptr [rsi]00000001`4098346b 498d4e10 learcx,[r14+10h]00000001`4098346f 488bd5 movrdx,rbp00000001`40983472 e8c91969ff call TodosApi!RhpAssignRefAVLocation (140014e40)00000001`40983477 488d0dea6f1f00 learcx,[TodosApi!?__NONGCSTATICS@System.Threading.Tasks.TplEventSource@@ (140b7a468)]00000001`4098347e 488379f800 cmp qword ptr [rcx-8],000000001`409834837559jne TodosApi!System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<System.Threading.Tasks.VoidTaskResult>__GetStateMachineBox<S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>+0x16e (1409834de)00000001`40983485 488b0d94b0c400 movrcx, qword ptr [TodosApi!?__GCSTATICS@System.Threading.Tasks.TplEventSource@@ (1415ce520)]00000001`4098348c 488b5908 movrbx, qword ptr [rcx+8]00000001`40983490 80bb9d00000000 cmp byte ptr [rbx+9Dh],000000001`409834977437je TodosApi!System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<System.Threading.Tasks.VoidTaskResult>__GetStateMachineBox<S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>+0x160 (1409834d0)00000001`40983499 498bce movrcx,r1400000001`4098349c e83f27abff call TodosApi!System.Threading.Tasks.Task__get_Id (140435be0)00000001`409834a1 8bf0 movesi,eax00000001`409834a3 488d0de68c1a00 learcx,[TodosApi!__RuntimeType_S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100 (140b2c190)]00000001`409834aa e85161a4ff call TodosApi!System.RuntimeType__get_Name (1403c9600)00000001`409834af 488bd0 movrdx,rax00000001`409834b2 488d0dff5c0e00 learcx,[TodosApi!__Str_Async___4E51D638FE527E2B0672DE04B6E5F6B84BA81E64E12AA0136B0691A50E6F80FE (140a691b8)]00000001`409834b9 e89285a4ff call TodosApi!String__Concat_6 (1403cba50)00000001`409834be 4c8bc0 movr8,rax00000001`409834c1 488bcb movrcx,rbx00000001`409834c4 8bd6 movedx,esi00000001`409834c6 4533c9 xorr9d,r9d00000001`409834c9 3909cmp dword ptr [rcx],ecx00000001`409834cb e8608cabff call TodosApi!System.Threading.Tasks.TplEventSource__TraceOperationBegin (14043c130)00000001`409834d0 498bc6 movrax,r1400000001`409834d3 4883c420 addrsp,20h00000001`409834d7 5b poprbx00000001`409834d8 5d poprbp00000001`409834d9 5e poprsi00000001`409834da 5f poprdi00000001`409834db 415e popr1400000001`409834dd c3 ret00000001`409834de e86a0f68ff call TodosApi!__GetGCStaticBase_System.Threading.Tasks.TplEventSource (14000444d)00000001`409834e3 eba0 jmp TodosApi!System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<System.Threading.Tasks.VoidTaskResult>__GetStateMachineBox<S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>+0x115 (140983485)

Addresses following problems:

  • Multiple expensive epilogs
  • Copying the state machine is costly because they tend to be huge and are a mix of reference fields (need write barriers) and value fields. We need to copy twice - one is an assignment, the other is a box. See clusters of RhpByRefAssignRefAVLocation1 - that's the write barrier. I'm making the box take a less efficient path based on comments.
  • Logging is constructing a string that doesn't really need to be in generic code.

This is just a couple dozen bytes in savings per method, but ends up saving 50 kB for the whole app because that's how many specializations of the method we have.

Cc @stephentoub@jkotas for thoughts.

Contributes to dotnet#79204.
I don't know if this will be considered mergeable, but I wanted to at least try something. For apps that use async a lot (like the Stage2 app we use for Goldilocks), the async infrastructure can cost 10% of the entire executable.
Shuffling a couple things in `GetStateMachineBox` I was able to get 0.23% saving. It's miniscule. In general async is death by a thousand papercuts so I don't see a silver bullet.
@jkotas

Copy link
Copy Markdown
Member

Copying the state machine is costly because they tend to be huge and are a mix of reference fields (need write barriers) and value fields

That's because RyuJIT inlines the box helper for large structs. I think this should be fixed in the JIT.

Outlining of the event tracing helper looks good.

if (taskField is AsyncStateMachineBox<IAsyncStateMachine> weaklyTypedBox)
else if (taskField is AsyncStateMachineBox<IAsyncStateMachine> weaklyTypedBox)
{
// If this is the first await, we won't yet have a state machine, so store it.

@stephentoubstephentoubApr 26, 2024

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.

If we're trying to squeeze water from a stone, this whole block should be extremely cold (only used in debugging scenarios), so if you can come up with a good way to outline it all to a separate helper, that'd be reasonable. Not sure if that'd help much, though, given that stateMachine is generic.

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.

Yeah, it would likely make things worse because it's still the same code, but with extra unwinding/GC tracking data structures because we have a new method.

@MichalStrehovsky

Copy link
Copy Markdown
MemberAuthor

That's because RyuJIT inlines the box helper for large structs. I think this should be fixed in the JIT.

Do you have a heuristic in mind? It seems like what RyuJIT is doing now is strictly better from throughput perspective (we call the right allocation helper, assign all the fields, only do write barriers for GC pointers). Falling back to the regular boxing helper would be slower. Or are you thinking about a object RuntimeHelpers.Box<T>(ref T memory) that would be like a box, but guaranteed not to be expanded into alloc+set individual fields.

@EgorBo

EgorBo commented Apr 26, 2024

Copy link
Copy Markdown
Member

That's because RyuJIT inlines the box helper for large structs. I think this should be fixed in the JIT.

Do you have a heuristic in mind? It seems like what RyuJIT is doing now is strictly better from throughput perspective (we call the right allocation helper, assign all the fields, only do write barriers for GC pointers). Falling back to the regular boxing helper would be slower. Or are you thinking about a object RuntimeHelpers.Box<T>(ref T memory) that would be like a box, but guaranteed not to be expanded into alloc+set individual fields.

I presume Jan means that for a struct with many gc fields (JIT can check that) it's better for VM/Box helper to do a batch move (BulkMoveWithWriteBarrier) instead of individual write barriers

@jkotas

Copy link
Copy Markdown
Member

It seems like what RyuJIT is doing now is strictly better from throughput perspective

I expect that the bulk copy in the box helper starts to win in throughput above certain size. The JIT should probably give up inlining the copy way below this threshold for good code size / throughput tradeoff.

Also, the current implementation of the box is slow since the box helper is almost never used today. Jeremy hit that problem when switching a few places in reflection to use the box helper (WIP #101137).

@EgorBo

Copy link
Copy Markdown
Member

Quick test:

[Benchmark]publicobjectBox()=>ms;MyStructms;structMyStruct{stringa1;stringa2;stringa3;stringa4;stringa5;stringa6;}

Benchmark for current impl vs using CORINFO_HELP_BOX (which is lowered down to memmoveGCRefs basically):

| Method | Toolchain | Mean |
|------- |---------------------------- |----------:|
| Box | \Core_Root_PR\corerun.exe | 5.832 ns |
| Box | \Core_Root_base\corerun.exe | 10.236 ns |

@MichalStrehovsky

Copy link
Copy Markdown
MemberAuthor

I presume Jan means that for a struct with many gc fields (JIT can check that) it's better for VM/Box helper to do a batch move (BulkMoveWithWriteBarrier) instead of individual write barriers

I wonder if this also means the JIT should use the batch move when assigning structs with many GC fields. Looks like there is a CORINFO_HELP_ASSIGN_STRUCT but I can't find any place in the JIT codebase where it would be used.

The method I'm changing here does two struct assignments with these large structs - one as part of the box, and the other as part of the non-boxing path. Looks like it might be beneficial to treat both cases the same in the JIT and if one is using CORINFO_HELP_BOX, the other should be using ASSIGN_STRUCT.

@EgorBo

EgorBo commented Apr 27, 2024

Copy link
Copy Markdown
Member

I wonder if this also means the JIT should use the batch move when assigning structs with many GC fields.

We discussed this in #99096 tldr: there is always a trade-off

E.g. the code we currently generate is interruption-friendly and is more precise from GC's point of view (we update card table for each gc handle in a struct)

@EgorBo

Copy link
Copy Markdown
Member

Fun fact: no-optimization also improves perf! 🙂

image

@jkotas

Copy link
Copy Markdown
Member

is more precise from GC's point of view (we update card table for each gc handle in a struct)

For box helper, this precision loss in card table updates should not be a concern. The object was just allocated and so it is very unlikely that will be any card table updates necessary. (There was even a discussion at one point about what it would take to skip the write barrier calls completely in cases like this.)

@MichalStrehovsky

Copy link
Copy Markdown
MemberAuthor

I made a draft PR to use the regular boxing helper above a certain number of GC fields at #101669. I hope we'll be able to tune the number with the bots/automation that exists in this repo. A struct with 6 fields, all of them GC fields should obviously use the box helper path but I'm less clear on the criteria for others (does mixing GC and non-GC fields make any difference, etc.?).

// generating this extra code until a better solution is implemented.
var box = new AsyncStateMachineBox<TStateMachine>();
// DebugFinalizableAsyncStateMachineBox looks like a small type, but it actually is not because
// it will have a copy of all the slots from its parent. It will add another hundred(s) bytes

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It might be out of scope for this PR, but how about using an unspecialized DebugFinalizableAsyncStateMachineBox<IAsyncStateMachine> for NativeAOT?

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.

Don't know much about this - if it's only used by managed debuggers, it would not be useful for native AOT anyway. It's ifdeffed out so I'm not concerned about it.

@github-actionsgithub-actionsBot mentioned this pull request May 2, 2024
@MichalStrehovsky
MichalStrehovsky merged commit 507abdf into dotnet:mainMay 2, 2024
@MichalStrehovsky
MichalStrehovsky deleted the async branch May 2, 2024 09:23
michaelgsharp pushed a commit to michaelgsharp/runtime that referenced this pull request May 9, 2024
Contributes to dotnet#79204.
I don't know if this will be considered mergeable, but I wanted to at least try something. For apps that use async a lot (like the Stage2 app we use for Goldilocks), the async infrastructure can cost 10% of the entire executable.
Shuffling a couple things in `GetStateMachineBox` I was able to get 0.23% saving. It's miniscule. In general async is death by a thousand papercuts so I don't see a silver bullet.
Ruihan-Yin pushed a commit to Ruihan-Yin/runtime that referenced this pull request May 30, 2024
Contributes to dotnet#79204.
I don't know if this will be considered mergeable, but I wanted to at least try something. For apps that use async a lot (like the Stage2 app we use for Goldilocks), the async infrastructure can cost 10% of the entire executable.
Shuffling a couple things in `GetStateMachineBox` I was able to get 0.23% saving. It's miniscule. In general async is death by a thousand papercuts so I don't see a silver bullet.
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jun 2, 2024
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.

5 participants

@MichalStrehovsky@jkotas@EgorBo@stephentoub@teo-tsirpanis
, '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

Try to reduce cost of Async - #101605

Merged
MichalStrehovsky merged 2 commits into
dotnet:mainfrom
MichalStrehovsky:async
May 2, 2024
Merged

Try to reduce cost of Async#101605
MichalStrehovsky merged 2 commits into
dotnet:mainfrom
MichalStrehovsky:async

Conversation

@MichalStrehovsky

Copy link
Copy Markdown
Member

Contributes to #79204.

I don't know if this will be considered mergeable, but I wanted to at least try something. For apps that use async a lot (like the Stage2 app we use for Goldilocks), the async infrastructure can cost 10% of the entire executable.

Shuffling a couple things in GetStateMachineBox I was able to get 0.23% saving. It's miniscule. In general async is death by a thousand papercuts so I don't see a silver bullet.

Click to expand existing assembly
 TodosApi!System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<System.Threading.Tasks.VoidTaskResult>__GetStateMachineBox<S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>:00000001`409833704156pushr1400000001`4098337257pushrdi00000001`4098337356pushrsi00000001`4098337455pushrbp00000001`4098337553pushrbx00000001`40983376 4883ec20 subrsp,20h00000001`4098337a 488bf1 movrsi, stateMachine (rcx)00000001`4098337d 488bda movrbx, taskField (rdx)00000001`40983380 e8eb68aaff call TodosApi!System.Threading.ExecutionContext__Capture (140429c70)00000001`40983385 488be8 movrbp,rax00000001`40983388 488b3b movrdi, qword ptr [taskField (rbx)]00000001`4098338b 4885ff testrdi,rdi00000001`4098338e 742c je TodosApi!System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<System.Threading.Tasks.VoidTaskResult>__GetStateMachineBox<S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>+0x4c (1409833bc)00000001`40983390 488d0df93f3e00 learcx,[TodosApi!??_7System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1_AsyncStateMachineBox`1<System.Threading.Tasks.VoidTaskResult__S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>@@6B@ (140d67390)]00000001`40983397 48390f cmp qword ptr [rdi],rcx00000001`4098339a 7520jne TodosApi!System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<System.Threading.Tasks.VoidTaskResult>__GetStateMachineBox<S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>+0x4c (1409833bc)00000001`4098339c 48396f10 cmp qword ptr [stronglyTypedBox (rdi)+10h], currentContext (rbp)00000001`409833a0 740c je TodosApi!System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<System.Threading.Tasks.VoidTaskResult>__GetStateMachineBox<S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>+0x3e (1409833ae)00000001`409833a2 488d4f10 learcx,[stronglyTypedBox (rdi)+10h]00000001`409833a6 488bd5 movrdx,rbp00000001`409833a9 e8921a69ff call TodosApi!RhpAssignRefAVLocation (140014e40)00000001`409833ae 488bc7 movrax,rdi00000001`409833b1 4883c420 addrsp,20h00000001`409833b5 5b poprbx00000001`409833b6 5d poprbp00000001`409833b7 5e poprsi00000001`409833b8 5f poprdi00000001`409833b9 415e popr1400000001`409833bb c3 ret00000001`409833bc 4c8b33 movr14, qword ptr [taskField (rbx)]00000001`409833bf 4d85f6 testr14,r1400000001`409833c2 7461je TodosApi!System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<System.Threading.Tasks.VoidTaskResult>__GetStateMachineBox<S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>+0xb5 (140983425)00000001`409833c4 488d0d7d193e00 learcx,[TodosApi!??_7System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1_AsyncStateMachineBox`1<System.Threading.Tasks.VoidTaskResult__System.Runtime.CompilerServices.IAsyncStateMachine>@@6B@ (140d64d48)]00000001`409833cb 49390e cmp qword ptr [r14],rcx00000001`409833ce 7555jne TodosApi!System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<System.Threading.Tasks.VoidTaskResult>__GetStateMachineBox<S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>+0xb5 (140983425)00000001`409833d0 49837e4000 cmp qword ptr [weaklyTypedBox (r14)+40h],000000001`409833d5 7534jne TodosApi!System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<System.Threading.Tasks.VoidTaskResult>__GetStateMachineBox<S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>+0x9b (14098340b)00000001`409833d7 488d0d1a622a00 learcx,[TodosApi!Boxed_S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100::`vftable' (140c295f8)]00000001`409833de e84d1869ff call TodosApi!RhpNewFast (140014c30)00000001`409833e3 488bd0 movrdx,rax00000001`409833e3 488bd0 movrdx,rax00000001`409833e6 488d7a08 leardi,[rdx+8]00000001`409833ea e8411c69ff call TodosApi!RhpByRefAssignRefAVLocation1 (140015030)00000001`409833ef e83c1c69ff call TodosApi!RhpByRefAssignRefAVLocation1 (140015030)00000001`409833f4 48a5 movs qword ptr [rdi], qword ptr [rsi]00000001`409833f6 e8351c69ff call TodosApi!RhpByRefAssignRefAVLocation1 (140015030)00000001`409833fb e8301c69ff call TodosApi!RhpByRefAssignRefAVLocation1 (140015030)00000001`40983400 48a5 movs qword ptr [rdi], qword ptr [rsi]00000001`40983402 498d4e40 learcx,[weaklyTypedBox (r14)+40h]00000001`40983406 e8351a69ff call TodosApi!RhpAssignRefAVLocation (140014e40)00000001`4098340b 498d4e10 learcx,[weaklyTypedBox (r14)+10h]00000001`4098340f 488bd5 movrdx,rbp00000001`40983412 e8291a69ff call TodosApi!RhpAssignRefAVLocation (140014e40)00000001`40983417 498bc6 movrax,r1400000001`4098341a 4883c420 addrsp,20h00000001`4098341e 5b poprbx00000001`4098341f 5d poprbp00000001`40983420 5e poprsi00000001`40983421 5f poprdi00000001`40983422 415e popr1400000001`40983424 c3 ret00000001`40983425 488d0d643f3e00 learcx,[TodosApi!??_7System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1_AsyncStateMachineBox`1<System.Threading.Tasks.VoidTaskResult__S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>@@6B@ (140d67390)]00000001`4098342c e8ff1769ff call TodosApi!RhpNewFast (140014c30)00000001`40983431 4c8bf0 movr14,rax00000001`40983434 41c7463400040002 mov dword ptr [r14+34h],2000400h00000001`4098343c 41814e3400080000 or dword ptr [r14+34h],800h00000001`40983444 488bcb movrcx,rbx00000001`40983447 498bd6 movrdx,r1400000001`4098344a e8611a69ff call TodosApi!RhpCheckedAssignRefAVLocation (140014eb0)00000001`4098344f 498d7e40 leardi,[r14+40h]00000001`40983453 e8d81b69ff call TodosApi!RhpByRefAssignRefAVLocation1 (140015030)00000001`40983458 e8d31b69ff call TodosApi!RhpByRefAssignRefAVLocation1 (140015030)00000001`4098345d 48a5 movs qword ptr [rdi], qword ptr [rsi]00000001`4098345f e8cc1b69ff call TodosApi!RhpByRefAssignRefAVLocation1 (140015030)00000001`40983464 e8c71b69ff call TodosApi!RhpByRefAssignRefAVLocation1 (140015030)00000001`40983469 48a5 movs qword ptr [rdi], qword ptr [rsi]00000001`4098346b 498d4e10 learcx,[r14+10h]00000001`4098346f 488bd5 movrdx,rbp00000001`40983472 e8c91969ff call TodosApi!RhpAssignRefAVLocation (140014e40)00000001`40983477 488d0dea6f1f00 learcx,[TodosApi!?__NONGCSTATICS@System.Threading.Tasks.TplEventSource@@ (140b7a468)]00000001`4098347e 488379f800 cmp qword ptr [rcx-8],000000001`409834837559jne TodosApi!System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<System.Threading.Tasks.VoidTaskResult>__GetStateMachineBox<S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>+0x16e (1409834de)00000001`40983485 488b0d94b0c400 movrcx, qword ptr [TodosApi!?__GCSTATICS@System.Threading.Tasks.TplEventSource@@ (1415ce520)]00000001`4098348c 488b5908 movrbx, qword ptr [rcx+8]00000001`40983490 80bb9d00000000 cmp byte ptr [rbx+9Dh],000000001`409834977437je TodosApi!System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<System.Threading.Tasks.VoidTaskResult>__GetStateMachineBox<S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>+0x160 (1409834d0)00000001`40983499 498bce movrcx,r1400000001`4098349c e83f27abff call TodosApi!System.Threading.Tasks.Task__get_Id (140435be0)00000001`409834a1 8bf0 movesi,eax00000001`409834a3 488d0de68c1a00 learcx,[TodosApi!__RuntimeType_S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100 (140b2c190)]00000001`409834aa e85161a4ff call TodosApi!System.RuntimeType__get_Name (1403c9600)00000001`409834af 488bd0 movrdx,rax00000001`409834b2 488d0dff5c0e00 learcx,[TodosApi!__Str_Async___4E51D638FE527E2B0672DE04B6E5F6B84BA81E64E12AA0136B0691A50E6F80FE (140a691b8)]00000001`409834b9 e89285a4ff call TodosApi!String__Concat_6 (1403cba50)00000001`409834be 4c8bc0 movr8,rax00000001`409834c1 488bcb movrcx,rbx00000001`409834c4 8bd6 movedx,esi00000001`409834c6 4533c9 xorr9d,r9d00000001`409834c9 3909cmp dword ptr [rcx],ecx00000001`409834cb e8608cabff call TodosApi!System.Threading.Tasks.TplEventSource__TraceOperationBegin (14043c130)00000001`409834d0 498bc6 movrax,r1400000001`409834d3 4883c420 addrsp,20h00000001`409834d7 5b poprbx00000001`409834d8 5d poprbp00000001`409834d9 5e poprsi00000001`409834da 5f poprdi00000001`409834db 415e popr1400000001`409834dd c3 ret00000001`409834de e86a0f68ff call TodosApi!__GetGCStaticBase_System.Threading.Tasks.TplEventSource (14000444d)00000001`409834e3 eba0 jmp TodosApi!System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<System.Threading.Tasks.VoidTaskResult>__GetStateMachineBox<S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>+0x115 (140983485)

Addresses following problems:

  • Multiple expensive epilogs
  • Copying the state machine is costly because they tend to be huge and are a mix of reference fields (need write barriers) and value fields. We need to copy twice - one is an assignment, the other is a box. See clusters of RhpByRefAssignRefAVLocation1 - that's the write barrier. I'm making the box take a less efficient path based on comments.
  • Logging is constructing a string that doesn't really need to be in generic code.

This is just a couple dozen bytes in savings per method, but ends up saving 50 kB for the whole app because that's how many specializations of the method we have.

Cc @stephentoub@jkotas for thoughts.

Contributes to dotnet#79204.
I don't know if this will be considered mergeable, but I wanted to at least try something. For apps that use async a lot (like the Stage2 app we use for Goldilocks), the async infrastructure can cost 10% of the entire executable.
Shuffling a couple things in `GetStateMachineBox` I was able to get 0.23% saving. It's miniscule. In general async is death by a thousand papercuts so I don't see a silver bullet.
@jkotas

Copy link
Copy Markdown
Member

Copying the state machine is costly because they tend to be huge and are a mix of reference fields (need write barriers) and value fields

That's because RyuJIT inlines the box helper for large structs. I think this should be fixed in the JIT.

Outlining of the event tracing helper looks good.

if (taskField is AsyncStateMachineBox<IAsyncStateMachine> weaklyTypedBox)
else if (taskField is AsyncStateMachineBox<IAsyncStateMachine> weaklyTypedBox)
{
// If this is the first await, we won't yet have a state machine, so store it.

@stephentoubstephentoubApr 26, 2024

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.

If we're trying to squeeze water from a stone, this whole block should be extremely cold (only used in debugging scenarios), so if you can come up with a good way to outline it all to a separate helper, that'd be reasonable. Not sure if that'd help much, though, given that stateMachine is generic.

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.

Yeah, it would likely make things worse because it's still the same code, but with extra unwinding/GC tracking data structures because we have a new method.

@MichalStrehovsky

Copy link
Copy Markdown
MemberAuthor

That's because RyuJIT inlines the box helper for large structs. I think this should be fixed in the JIT.

Do you have a heuristic in mind? It seems like what RyuJIT is doing now is strictly better from throughput perspective (we call the right allocation helper, assign all the fields, only do write barriers for GC pointers). Falling back to the regular boxing helper would be slower. Or are you thinking about a object RuntimeHelpers.Box<T>(ref T memory) that would be like a box, but guaranteed not to be expanded into alloc+set individual fields.

@EgorBo

EgorBo commented Apr 26, 2024

Copy link
Copy Markdown
Member

That's because RyuJIT inlines the box helper for large structs. I think this should be fixed in the JIT.

Do you have a heuristic in mind? It seems like what RyuJIT is doing now is strictly better from throughput perspective (we call the right allocation helper, assign all the fields, only do write barriers for GC pointers). Falling back to the regular boxing helper would be slower. Or are you thinking about a object RuntimeHelpers.Box<T>(ref T memory) that would be like a box, but guaranteed not to be expanded into alloc+set individual fields.

I presume Jan means that for a struct with many gc fields (JIT can check that) it's better for VM/Box helper to do a batch move (BulkMoveWithWriteBarrier) instead of individual write barriers

@jkotas

Copy link
Copy Markdown
Member

It seems like what RyuJIT is doing now is strictly better from throughput perspective

I expect that the bulk copy in the box helper starts to win in throughput above certain size. The JIT should probably give up inlining the copy way below this threshold for good code size / throughput tradeoff.

Also, the current implementation of the box is slow since the box helper is almost never used today. Jeremy hit that problem when switching a few places in reflection to use the box helper (WIP #101137).

@EgorBo

Copy link
Copy Markdown
Member

Quick test:

[Benchmark]publicobjectBox()=>ms;MyStructms;structMyStruct{stringa1;stringa2;stringa3;stringa4;stringa5;stringa6;}

Benchmark for current impl vs using CORINFO_HELP_BOX (which is lowered down to memmoveGCRefs basically):

| Method | Toolchain | Mean |
|------- |---------------------------- |----------:|
| Box | \Core_Root_PR\corerun.exe | 5.832 ns |
| Box | \Core_Root_base\corerun.exe | 10.236 ns |

@MichalStrehovsky

Copy link
Copy Markdown
MemberAuthor

I presume Jan means that for a struct with many gc fields (JIT can check that) it's better for VM/Box helper to do a batch move (BulkMoveWithWriteBarrier) instead of individual write barriers

I wonder if this also means the JIT should use the batch move when assigning structs with many GC fields. Looks like there is a CORINFO_HELP_ASSIGN_STRUCT but I can't find any place in the JIT codebase where it would be used.

The method I'm changing here does two struct assignments with these large structs - one as part of the box, and the other as part of the non-boxing path. Looks like it might be beneficial to treat both cases the same in the JIT and if one is using CORINFO_HELP_BOX, the other should be using ASSIGN_STRUCT.

@EgorBo

EgorBo commented Apr 27, 2024

Copy link
Copy Markdown
Member

I wonder if this also means the JIT should use the batch move when assigning structs with many GC fields.

We discussed this in #99096 tldr: there is always a trade-off

E.g. the code we currently generate is interruption-friendly and is more precise from GC's point of view (we update card table for each gc handle in a struct)

@EgorBo

Copy link
Copy Markdown
Member

Fun fact: no-optimization also improves perf! 🙂

image

@jkotas

Copy link
Copy Markdown
Member

is more precise from GC's point of view (we update card table for each gc handle in a struct)

For box helper, this precision loss in card table updates should not be a concern. The object was just allocated and so it is very unlikely that will be any card table updates necessary. (There was even a discussion at one point about what it would take to skip the write barrier calls completely in cases like this.)

@MichalStrehovsky

Copy link
Copy Markdown
MemberAuthor

I made a draft PR to use the regular boxing helper above a certain number of GC fields at #101669. I hope we'll be able to tune the number with the bots/automation that exists in this repo. A struct with 6 fields, all of them GC fields should obviously use the box helper path but I'm less clear on the criteria for others (does mixing GC and non-GC fields make any difference, etc.?).

// generating this extra code until a better solution is implemented.
var box = new AsyncStateMachineBox<TStateMachine>();
// DebugFinalizableAsyncStateMachineBox looks like a small type, but it actually is not because
// it will have a copy of all the slots from its parent. It will add another hundred(s) bytes

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It might be out of scope for this PR, but how about using an unspecialized DebugFinalizableAsyncStateMachineBox<IAsyncStateMachine> for NativeAOT?

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.

Don't know much about this - if it's only used by managed debuggers, it would not be useful for native AOT anyway. It's ifdeffed out so I'm not concerned about it.

@github-actionsgithub-actionsBot mentioned this pull request May 2, 2024
@MichalStrehovsky
MichalStrehovsky merged commit 507abdf into dotnet:mainMay 2, 2024
@MichalStrehovsky
MichalStrehovsky deleted the async branch May 2, 2024 09:23
michaelgsharp pushed a commit to michaelgsharp/runtime that referenced this pull request May 9, 2024
Contributes to dotnet#79204.
I don't know if this will be considered mergeable, but I wanted to at least try something. For apps that use async a lot (like the Stage2 app we use for Goldilocks), the async infrastructure can cost 10% of the entire executable.
Shuffling a couple things in `GetStateMachineBox` I was able to get 0.23% saving. It's miniscule. In general async is death by a thousand papercuts so I don't see a silver bullet.
Ruihan-Yin pushed a commit to Ruihan-Yin/runtime that referenced this pull request May 30, 2024
Contributes to dotnet#79204.
I don't know if this will be considered mergeable, but I wanted to at least try something. For apps that use async a lot (like the Stage2 app we use for Goldilocks), the async infrastructure can cost 10% of the entire executable.
Shuffling a couple things in `GetStateMachineBox` I was able to get 0.23% saving. It's miniscule. In general async is death by a thousand papercuts so I don't see a silver bullet.
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jun 2, 2024
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.

5 participants

@MichalStrehovsky@jkotas@EgorBo@stephentoub@teo-tsirpanis
, '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

Try to reduce cost of Async - #101605

Merged
MichalStrehovsky merged 2 commits into
dotnet:mainfrom
MichalStrehovsky:async
May 2, 2024
Merged

Try to reduce cost of Async#101605
MichalStrehovsky merged 2 commits into
dotnet:mainfrom
MichalStrehovsky:async

Conversation

@MichalStrehovsky

Copy link
Copy Markdown
Member

Contributes to #79204.

I don't know if this will be considered mergeable, but I wanted to at least try something. For apps that use async a lot (like the Stage2 app we use for Goldilocks), the async infrastructure can cost 10% of the entire executable.

Shuffling a couple things in GetStateMachineBox I was able to get 0.23% saving. It's miniscule. In general async is death by a thousand papercuts so I don't see a silver bullet.

Click to expand existing assembly
 TodosApi!System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<System.Threading.Tasks.VoidTaskResult>__GetStateMachineBox<S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>:00000001`409833704156pushr1400000001`4098337257pushrdi00000001`4098337356pushrsi00000001`4098337455pushrbp00000001`4098337553pushrbx00000001`40983376 4883ec20 subrsp,20h00000001`4098337a 488bf1 movrsi, stateMachine (rcx)00000001`4098337d 488bda movrbx, taskField (rdx)00000001`40983380 e8eb68aaff call TodosApi!System.Threading.ExecutionContext__Capture (140429c70)00000001`40983385 488be8 movrbp,rax00000001`40983388 488b3b movrdi, qword ptr [taskField (rbx)]00000001`4098338b 4885ff testrdi,rdi00000001`4098338e 742c je TodosApi!System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<System.Threading.Tasks.VoidTaskResult>__GetStateMachineBox<S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>+0x4c (1409833bc)00000001`40983390 488d0df93f3e00 learcx,[TodosApi!??_7System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1_AsyncStateMachineBox`1<System.Threading.Tasks.VoidTaskResult__S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>@@6B@ (140d67390)]00000001`40983397 48390f cmp qword ptr [rdi],rcx00000001`4098339a 7520jne TodosApi!System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<System.Threading.Tasks.VoidTaskResult>__GetStateMachineBox<S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>+0x4c (1409833bc)00000001`4098339c 48396f10 cmp qword ptr [stronglyTypedBox (rdi)+10h], currentContext (rbp)00000001`409833a0 740c je TodosApi!System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<System.Threading.Tasks.VoidTaskResult>__GetStateMachineBox<S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>+0x3e (1409833ae)00000001`409833a2 488d4f10 learcx,[stronglyTypedBox (rdi)+10h]00000001`409833a6 488bd5 movrdx,rbp00000001`409833a9 e8921a69ff call TodosApi!RhpAssignRefAVLocation (140014e40)00000001`409833ae 488bc7 movrax,rdi00000001`409833b1 4883c420 addrsp,20h00000001`409833b5 5b poprbx00000001`409833b6 5d poprbp00000001`409833b7 5e poprsi00000001`409833b8 5f poprdi00000001`409833b9 415e popr1400000001`409833bb c3 ret00000001`409833bc 4c8b33 movr14, qword ptr [taskField (rbx)]00000001`409833bf 4d85f6 testr14,r1400000001`409833c2 7461je TodosApi!System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<System.Threading.Tasks.VoidTaskResult>__GetStateMachineBox<S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>+0xb5 (140983425)00000001`409833c4 488d0d7d193e00 learcx,[TodosApi!??_7System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1_AsyncStateMachineBox`1<System.Threading.Tasks.VoidTaskResult__System.Runtime.CompilerServices.IAsyncStateMachine>@@6B@ (140d64d48)]00000001`409833cb 49390e cmp qword ptr [r14],rcx00000001`409833ce 7555jne TodosApi!System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<System.Threading.Tasks.VoidTaskResult>__GetStateMachineBox<S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>+0xb5 (140983425)00000001`409833d0 49837e4000 cmp qword ptr [weaklyTypedBox (r14)+40h],000000001`409833d5 7534jne TodosApi!System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<System.Threading.Tasks.VoidTaskResult>__GetStateMachineBox<S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>+0x9b (14098340b)00000001`409833d7 488d0d1a622a00 learcx,[TodosApi!Boxed_S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100::`vftable' (140c295f8)]00000001`409833de e84d1869ff call TodosApi!RhpNewFast (140014c30)00000001`409833e3 488bd0 movrdx,rax00000001`409833e3 488bd0 movrdx,rax00000001`409833e6 488d7a08 leardi,[rdx+8]00000001`409833ea e8411c69ff call TodosApi!RhpByRefAssignRefAVLocation1 (140015030)00000001`409833ef e83c1c69ff call TodosApi!RhpByRefAssignRefAVLocation1 (140015030)00000001`409833f4 48a5 movs qword ptr [rdi], qword ptr [rsi]00000001`409833f6 e8351c69ff call TodosApi!RhpByRefAssignRefAVLocation1 (140015030)00000001`409833fb e8301c69ff call TodosApi!RhpByRefAssignRefAVLocation1 (140015030)00000001`40983400 48a5 movs qword ptr [rdi], qword ptr [rsi]00000001`40983402 498d4e40 learcx,[weaklyTypedBox (r14)+40h]00000001`40983406 e8351a69ff call TodosApi!RhpAssignRefAVLocation (140014e40)00000001`4098340b 498d4e10 learcx,[weaklyTypedBox (r14)+10h]00000001`4098340f 488bd5 movrdx,rbp00000001`40983412 e8291a69ff call TodosApi!RhpAssignRefAVLocation (140014e40)00000001`40983417 498bc6 movrax,r1400000001`4098341a 4883c420 addrsp,20h00000001`4098341e 5b poprbx00000001`4098341f 5d poprbp00000001`40983420 5e poprsi00000001`40983421 5f poprdi00000001`40983422 415e popr1400000001`40983424 c3 ret00000001`40983425 488d0d643f3e00 learcx,[TodosApi!??_7System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1_AsyncStateMachineBox`1<System.Threading.Tasks.VoidTaskResult__S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>@@6B@ (140d67390)]00000001`4098342c e8ff1769ff call TodosApi!RhpNewFast (140014c30)00000001`40983431 4c8bf0 movr14,rax00000001`40983434 41c7463400040002 mov dword ptr [r14+34h],2000400h00000001`4098343c 41814e3400080000 or dword ptr [r14+34h],800h00000001`40983444 488bcb movrcx,rbx00000001`40983447 498bd6 movrdx,r1400000001`4098344a e8611a69ff call TodosApi!RhpCheckedAssignRefAVLocation (140014eb0)00000001`4098344f 498d7e40 leardi,[r14+40h]00000001`40983453 e8d81b69ff call TodosApi!RhpByRefAssignRefAVLocation1 (140015030)00000001`40983458 e8d31b69ff call TodosApi!RhpByRefAssignRefAVLocation1 (140015030)00000001`4098345d 48a5 movs qword ptr [rdi], qword ptr [rsi]00000001`4098345f e8cc1b69ff call TodosApi!RhpByRefAssignRefAVLocation1 (140015030)00000001`40983464 e8c71b69ff call TodosApi!RhpByRefAssignRefAVLocation1 (140015030)00000001`40983469 48a5 movs qword ptr [rdi], qword ptr [rsi]00000001`4098346b 498d4e10 learcx,[r14+10h]00000001`4098346f 488bd5 movrdx,rbp00000001`40983472 e8c91969ff call TodosApi!RhpAssignRefAVLocation (140014e40)00000001`40983477 488d0dea6f1f00 learcx,[TodosApi!?__NONGCSTATICS@System.Threading.Tasks.TplEventSource@@ (140b7a468)]00000001`4098347e 488379f800 cmp qword ptr [rcx-8],000000001`409834837559jne TodosApi!System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<System.Threading.Tasks.VoidTaskResult>__GetStateMachineBox<S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>+0x16e (1409834de)00000001`40983485 488b0d94b0c400 movrcx, qword ptr [TodosApi!?__GCSTATICS@System.Threading.Tasks.TplEventSource@@ (1415ce520)]00000001`4098348c 488b5908 movrbx, qword ptr [rcx+8]00000001`40983490 80bb9d00000000 cmp byte ptr [rbx+9Dh],000000001`409834977437je TodosApi!System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<System.Threading.Tasks.VoidTaskResult>__GetStateMachineBox<S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>+0x160 (1409834d0)00000001`40983499 498bce movrcx,r1400000001`4098349c e83f27abff call TodosApi!System.Threading.Tasks.Task__get_Id (140435be0)00000001`409834a1 8bf0 movesi,eax00000001`409834a3 488d0de68c1a00 learcx,[TodosApi!__RuntimeType_S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100 (140b2c190)]00000001`409834aa e85161a4ff call TodosApi!System.RuntimeType__get_Name (1403c9600)00000001`409834af 488bd0 movrdx,rax00000001`409834b2 488d0dff5c0e00 learcx,[TodosApi!__Str_Async___4E51D638FE527E2B0672DE04B6E5F6B84BA81E64E12AA0136B0691A50E6F80FE (140a691b8)]00000001`409834b9 e89285a4ff call TodosApi!String__Concat_6 (1403cba50)00000001`409834be 4c8bc0 movr8,rax00000001`409834c1 488bcb movrcx,rbx00000001`409834c4 8bd6 movedx,esi00000001`409834c6 4533c9 xorr9d,r9d00000001`409834c9 3909cmp dword ptr [rcx],ecx00000001`409834cb e8608cabff call TodosApi!System.Threading.Tasks.TplEventSource__TraceOperationBegin (14043c130)00000001`409834d0 498bc6 movrax,r1400000001`409834d3 4883c420 addrsp,20h00000001`409834d7 5b poprbx00000001`409834d8 5d poprbp00000001`409834d9 5e poprsi00000001`409834da 5f poprdi00000001`409834db 415e popr1400000001`409834dd c3 ret00000001`409834de e86a0f68ff call TodosApi!__GetGCStaticBase_System.Threading.Tasks.TplEventSource (14000444d)00000001`409834e3 eba0 jmp TodosApi!System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<System.Threading.Tasks.VoidTaskResult>__GetStateMachineBox<S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>+0x115 (140983485)

Addresses following problems:

  • Multiple expensive epilogs
  • Copying the state machine is costly because they tend to be huge and are a mix of reference fields (need write barriers) and value fields. We need to copy twice - one is an assignment, the other is a box. See clusters of RhpByRefAssignRefAVLocation1 - that's the write barrier. I'm making the box take a less efficient path based on comments.
  • Logging is constructing a string that doesn't really need to be in generic code.

This is just a couple dozen bytes in savings per method, but ends up saving 50 kB for the whole app because that's how many specializations of the method we have.

Cc @stephentoub@jkotas for thoughts.

Contributes to dotnet#79204.
I don't know if this will be considered mergeable, but I wanted to at least try something. For apps that use async a lot (like the Stage2 app we use for Goldilocks), the async infrastructure can cost 10% of the entire executable.
Shuffling a couple things in `GetStateMachineBox` I was able to get 0.23% saving. It's miniscule. In general async is death by a thousand papercuts so I don't see a silver bullet.
@jkotas

Copy link
Copy Markdown
Member

Copying the state machine is costly because they tend to be huge and are a mix of reference fields (need write barriers) and value fields

That's because RyuJIT inlines the box helper for large structs. I think this should be fixed in the JIT.

Outlining of the event tracing helper looks good.

if (taskField is AsyncStateMachineBox<IAsyncStateMachine> weaklyTypedBox)
else if (taskField is AsyncStateMachineBox<IAsyncStateMachine> weaklyTypedBox)
{
// If this is the first await, we won't yet have a state machine, so store it.

@stephentoubstephentoubApr 26, 2024

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.

If we're trying to squeeze water from a stone, this whole block should be extremely cold (only used in debugging scenarios), so if you can come up with a good way to outline it all to a separate helper, that'd be reasonable. Not sure if that'd help much, though, given that stateMachine is generic.

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.

Yeah, it would likely make things worse because it's still the same code, but with extra unwinding/GC tracking data structures because we have a new method.

@MichalStrehovsky

Copy link
Copy Markdown
MemberAuthor

That's because RyuJIT inlines the box helper for large structs. I think this should be fixed in the JIT.

Do you have a heuristic in mind? It seems like what RyuJIT is doing now is strictly better from throughput perspective (we call the right allocation helper, assign all the fields, only do write barriers for GC pointers). Falling back to the regular boxing helper would be slower. Or are you thinking about a object RuntimeHelpers.Box<T>(ref T memory) that would be like a box, but guaranteed not to be expanded into alloc+set individual fields.

@EgorBo

EgorBo commented Apr 26, 2024

Copy link
Copy Markdown
Member

That's because RyuJIT inlines the box helper for large structs. I think this should be fixed in the JIT.

Do you have a heuristic in mind? It seems like what RyuJIT is doing now is strictly better from throughput perspective (we call the right allocation helper, assign all the fields, only do write barriers for GC pointers). Falling back to the regular boxing helper would be slower. Or are you thinking about a object RuntimeHelpers.Box<T>(ref T memory) that would be like a box, but guaranteed not to be expanded into alloc+set individual fields.

I presume Jan means that for a struct with many gc fields (JIT can check that) it's better for VM/Box helper to do a batch move (BulkMoveWithWriteBarrier) instead of individual write barriers

@jkotas

Copy link
Copy Markdown
Member

It seems like what RyuJIT is doing now is strictly better from throughput perspective

I expect that the bulk copy in the box helper starts to win in throughput above certain size. The JIT should probably give up inlining the copy way below this threshold for good code size / throughput tradeoff.

Also, the current implementation of the box is slow since the box helper is almost never used today. Jeremy hit that problem when switching a few places in reflection to use the box helper (WIP #101137).

@EgorBo

Copy link
Copy Markdown
Member

Quick test:

[Benchmark]publicobjectBox()=>ms;MyStructms;structMyStruct{stringa1;stringa2;stringa3;stringa4;stringa5;stringa6;}

Benchmark for current impl vs using CORINFO_HELP_BOX (which is lowered down to memmoveGCRefs basically):

| Method | Toolchain | Mean |
|------- |---------------------------- |----------:|
| Box | \Core_Root_PR\corerun.exe | 5.832 ns |
| Box | \Core_Root_base\corerun.exe | 10.236 ns |

@MichalStrehovsky

Copy link
Copy Markdown
MemberAuthor

I presume Jan means that for a struct with many gc fields (JIT can check that) it's better for VM/Box helper to do a batch move (BulkMoveWithWriteBarrier) instead of individual write barriers

I wonder if this also means the JIT should use the batch move when assigning structs with many GC fields. Looks like there is a CORINFO_HELP_ASSIGN_STRUCT but I can't find any place in the JIT codebase where it would be used.

The method I'm changing here does two struct assignments with these large structs - one as part of the box, and the other as part of the non-boxing path. Looks like it might be beneficial to treat both cases the same in the JIT and if one is using CORINFO_HELP_BOX, the other should be using ASSIGN_STRUCT.

@EgorBo

EgorBo commented Apr 27, 2024

Copy link
Copy Markdown
Member

I wonder if this also means the JIT should use the batch move when assigning structs with many GC fields.

We discussed this in #99096 tldr: there is always a trade-off

E.g. the code we currently generate is interruption-friendly and is more precise from GC's point of view (we update card table for each gc handle in a struct)

@EgorBo

Copy link
Copy Markdown
Member

Fun fact: no-optimization also improves perf! 🙂

image

@jkotas

Copy link
Copy Markdown
Member

is more precise from GC's point of view (we update card table for each gc handle in a struct)

For box helper, this precision loss in card table updates should not be a concern. The object was just allocated and so it is very unlikely that will be any card table updates necessary. (There was even a discussion at one point about what it would take to skip the write barrier calls completely in cases like this.)

@MichalStrehovsky

Copy link
Copy Markdown
MemberAuthor

I made a draft PR to use the regular boxing helper above a certain number of GC fields at #101669. I hope we'll be able to tune the number with the bots/automation that exists in this repo. A struct with 6 fields, all of them GC fields should obviously use the box helper path but I'm less clear on the criteria for others (does mixing GC and non-GC fields make any difference, etc.?).

// generating this extra code until a better solution is implemented.
var box = new AsyncStateMachineBox<TStateMachine>();
// DebugFinalizableAsyncStateMachineBox looks like a small type, but it actually is not because
// it will have a copy of all the slots from its parent. It will add another hundred(s) bytes

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It might be out of scope for this PR, but how about using an unspecialized DebugFinalizableAsyncStateMachineBox<IAsyncStateMachine> for NativeAOT?

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.

Don't know much about this - if it's only used by managed debuggers, it would not be useful for native AOT anyway. It's ifdeffed out so I'm not concerned about it.

@github-actionsgithub-actionsBot mentioned this pull request May 2, 2024
@MichalStrehovsky
MichalStrehovsky merged commit 507abdf into dotnet:mainMay 2, 2024
@MichalStrehovsky
MichalStrehovsky deleted the async branch May 2, 2024 09:23
michaelgsharp pushed a commit to michaelgsharp/runtime that referenced this pull request May 9, 2024
Contributes to dotnet#79204.
I don't know if this will be considered mergeable, but I wanted to at least try something. For apps that use async a lot (like the Stage2 app we use for Goldilocks), the async infrastructure can cost 10% of the entire executable.
Shuffling a couple things in `GetStateMachineBox` I was able to get 0.23% saving. It's miniscule. In general async is death by a thousand papercuts so I don't see a silver bullet.
Ruihan-Yin pushed a commit to Ruihan-Yin/runtime that referenced this pull request May 30, 2024
Contributes to dotnet#79204.
I don't know if this will be considered mergeable, but I wanted to at least try something. For apps that use async a lot (like the Stage2 app we use for Goldilocks), the async infrastructure can cost 10% of the entire executable.
Shuffling a couple things in `GetStateMachineBox` I was able to get 0.23% saving. It's miniscule. In general async is death by a thousand papercuts so I don't see a silver bullet.
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jun 2, 2024
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.

5 participants

@MichalStrehovsky@jkotas@EgorBo@stephentoub@teo-tsirpanis
, '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

Try to reduce cost of Async - #101605

Merged
MichalStrehovsky merged 2 commits into
dotnet:mainfrom
MichalStrehovsky:async
May 2, 2024
Merged

Try to reduce cost of Async#101605
MichalStrehovsky merged 2 commits into
dotnet:mainfrom
MichalStrehovsky:async

Conversation

@MichalStrehovsky

Copy link
Copy Markdown
Member

Contributes to #79204.

I don't know if this will be considered mergeable, but I wanted to at least try something. For apps that use async a lot (like the Stage2 app we use for Goldilocks), the async infrastructure can cost 10% of the entire executable.

Shuffling a couple things in GetStateMachineBox I was able to get 0.23% saving. It's miniscule. In general async is death by a thousand papercuts so I don't see a silver bullet.

Click to expand existing assembly
 TodosApi!System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<System.Threading.Tasks.VoidTaskResult>__GetStateMachineBox<S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>:00000001`409833704156pushr1400000001`4098337257pushrdi00000001`4098337356pushrsi00000001`4098337455pushrbp00000001`4098337553pushrbx00000001`40983376 4883ec20 subrsp,20h00000001`4098337a 488bf1 movrsi, stateMachine (rcx)00000001`4098337d 488bda movrbx, taskField (rdx)00000001`40983380 e8eb68aaff call TodosApi!System.Threading.ExecutionContext__Capture (140429c70)00000001`40983385 488be8 movrbp,rax00000001`40983388 488b3b movrdi, qword ptr [taskField (rbx)]00000001`4098338b 4885ff testrdi,rdi00000001`4098338e 742c je TodosApi!System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<System.Threading.Tasks.VoidTaskResult>__GetStateMachineBox<S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>+0x4c (1409833bc)00000001`40983390 488d0df93f3e00 learcx,[TodosApi!??_7System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1_AsyncStateMachineBox`1<System.Threading.Tasks.VoidTaskResult__S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>@@6B@ (140d67390)]00000001`40983397 48390f cmp qword ptr [rdi],rcx00000001`4098339a 7520jne TodosApi!System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<System.Threading.Tasks.VoidTaskResult>__GetStateMachineBox<S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>+0x4c (1409833bc)00000001`4098339c 48396f10 cmp qword ptr [stronglyTypedBox (rdi)+10h], currentContext (rbp)00000001`409833a0 740c je TodosApi!System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<System.Threading.Tasks.VoidTaskResult>__GetStateMachineBox<S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>+0x3e (1409833ae)00000001`409833a2 488d4f10 learcx,[stronglyTypedBox (rdi)+10h]00000001`409833a6 488bd5 movrdx,rbp00000001`409833a9 e8921a69ff call TodosApi!RhpAssignRefAVLocation (140014e40)00000001`409833ae 488bc7 movrax,rdi00000001`409833b1 4883c420 addrsp,20h00000001`409833b5 5b poprbx00000001`409833b6 5d poprbp00000001`409833b7 5e poprsi00000001`409833b8 5f poprdi00000001`409833b9 415e popr1400000001`409833bb c3 ret00000001`409833bc 4c8b33 movr14, qword ptr [taskField (rbx)]00000001`409833bf 4d85f6 testr14,r1400000001`409833c2 7461je TodosApi!System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<System.Threading.Tasks.VoidTaskResult>__GetStateMachineBox<S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>+0xb5 (140983425)00000001`409833c4 488d0d7d193e00 learcx,[TodosApi!??_7System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1_AsyncStateMachineBox`1<System.Threading.Tasks.VoidTaskResult__System.Runtime.CompilerServices.IAsyncStateMachine>@@6B@ (140d64d48)]00000001`409833cb 49390e cmp qword ptr [r14],rcx00000001`409833ce 7555jne TodosApi!System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<System.Threading.Tasks.VoidTaskResult>__GetStateMachineBox<S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>+0xb5 (140983425)00000001`409833d0 49837e4000 cmp qword ptr [weaklyTypedBox (r14)+40h],000000001`409833d5 7534jne TodosApi!System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<System.Threading.Tasks.VoidTaskResult>__GetStateMachineBox<S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>+0x9b (14098340b)00000001`409833d7 488d0d1a622a00 learcx,[TodosApi!Boxed_S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100::`vftable' (140c295f8)]00000001`409833de e84d1869ff call TodosApi!RhpNewFast (140014c30)00000001`409833e3 488bd0 movrdx,rax00000001`409833e3 488bd0 movrdx,rax00000001`409833e6 488d7a08 leardi,[rdx+8]00000001`409833ea e8411c69ff call TodosApi!RhpByRefAssignRefAVLocation1 (140015030)00000001`409833ef e83c1c69ff call TodosApi!RhpByRefAssignRefAVLocation1 (140015030)00000001`409833f4 48a5 movs qword ptr [rdi], qword ptr [rsi]00000001`409833f6 e8351c69ff call TodosApi!RhpByRefAssignRefAVLocation1 (140015030)00000001`409833fb e8301c69ff call TodosApi!RhpByRefAssignRefAVLocation1 (140015030)00000001`40983400 48a5 movs qword ptr [rdi], qword ptr [rsi]00000001`40983402 498d4e40 learcx,[weaklyTypedBox (r14)+40h]00000001`40983406 e8351a69ff call TodosApi!RhpAssignRefAVLocation (140014e40)00000001`4098340b 498d4e10 learcx,[weaklyTypedBox (r14)+10h]00000001`4098340f 488bd5 movrdx,rbp00000001`40983412 e8291a69ff call TodosApi!RhpAssignRefAVLocation (140014e40)00000001`40983417 498bc6 movrax,r1400000001`4098341a 4883c420 addrsp,20h00000001`4098341e 5b poprbx00000001`4098341f 5d poprbp00000001`40983420 5e poprsi00000001`40983421 5f poprdi00000001`40983422 415e popr1400000001`40983424 c3 ret00000001`40983425 488d0d643f3e00 learcx,[TodosApi!??_7System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1_AsyncStateMachineBox`1<System.Threading.Tasks.VoidTaskResult__S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>@@6B@ (140d67390)]00000001`4098342c e8ff1769ff call TodosApi!RhpNewFast (140014c30)00000001`40983431 4c8bf0 movr14,rax00000001`40983434 41c7463400040002 mov dword ptr [r14+34h],2000400h00000001`4098343c 41814e3400080000 or dword ptr [r14+34h],800h00000001`40983444 488bcb movrcx,rbx00000001`40983447 498bd6 movrdx,r1400000001`4098344a e8611a69ff call TodosApi!RhpCheckedAssignRefAVLocation (140014eb0)00000001`4098344f 498d7e40 leardi,[r14+40h]00000001`40983453 e8d81b69ff call TodosApi!RhpByRefAssignRefAVLocation1 (140015030)00000001`40983458 e8d31b69ff call TodosApi!RhpByRefAssignRefAVLocation1 (140015030)00000001`4098345d 48a5 movs qword ptr [rdi], qword ptr [rsi]00000001`4098345f e8cc1b69ff call TodosApi!RhpByRefAssignRefAVLocation1 (140015030)00000001`40983464 e8c71b69ff call TodosApi!RhpByRefAssignRefAVLocation1 (140015030)00000001`40983469 48a5 movs qword ptr [rdi], qword ptr [rsi]00000001`4098346b 498d4e10 learcx,[r14+10h]00000001`4098346f 488bd5 movrdx,rbp00000001`40983472 e8c91969ff call TodosApi!RhpAssignRefAVLocation (140014e40)00000001`40983477 488d0dea6f1f00 learcx,[TodosApi!?__NONGCSTATICS@System.Threading.Tasks.TplEventSource@@ (140b7a468)]00000001`4098347e 488379f800 cmp qword ptr [rcx-8],000000001`409834837559jne TodosApi!System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<System.Threading.Tasks.VoidTaskResult>__GetStateMachineBox<S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>+0x16e (1409834de)00000001`40983485 488b0d94b0c400 movrcx, qword ptr [TodosApi!?__GCSTATICS@System.Threading.Tasks.TplEventSource@@ (1415ce520)]00000001`4098348c 488b5908 movrbx, qword ptr [rcx+8]00000001`40983490 80bb9d00000000 cmp byte ptr [rbx+9Dh],000000001`409834977437je TodosApi!System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<System.Threading.Tasks.VoidTaskResult>__GetStateMachineBox<S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>+0x160 (1409834d0)00000001`40983499 498bce movrcx,r1400000001`4098349c e83f27abff call TodosApi!System.Threading.Tasks.Task__get_Id (140435be0)00000001`409834a1 8bf0 movesi,eax00000001`409834a3 488d0de68c1a00 learcx,[TodosApi!__RuntimeType_S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100 (140b2c190)]00000001`409834aa e85161a4ff call TodosApi!System.RuntimeType__get_Name (1403c9600)00000001`409834af 488bd0 movrdx,rax00000001`409834b2 488d0dff5c0e00 learcx,[TodosApi!__Str_Async___4E51D638FE527E2B0672DE04B6E5F6B84BA81E64E12AA0136B0691A50E6F80FE (140a691b8)]00000001`409834b9 e89285a4ff call TodosApi!String__Concat_6 (1403cba50)00000001`409834be 4c8bc0 movr8,rax00000001`409834c1 488bcb movrcx,rbx00000001`409834c4 8bd6 movedx,esi00000001`409834c6 4533c9 xorr9d,r9d00000001`409834c9 3909cmp dword ptr [rcx],ecx00000001`409834cb e8608cabff call TodosApi!System.Threading.Tasks.TplEventSource__TraceOperationBegin (14043c130)00000001`409834d0 498bc6 movrax,r1400000001`409834d3 4883c420 addrsp,20h00000001`409834d7 5b poprbx00000001`409834d8 5d poprbp00000001`409834d9 5e poprsi00000001`409834da 5f poprdi00000001`409834db 415e popr1400000001`409834dd c3 ret00000001`409834de e86a0f68ff call TodosApi!__GetGCStaticBase_System.Threading.Tasks.TplEventSource (14000444d)00000001`409834e3 eba0 jmp TodosApi!System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<System.Threading.Tasks.VoidTaskResult>__GetStateMachineBox<S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>+0x115 (140983485)

Addresses following problems:

  • Multiple expensive epilogs
  • Copying the state machine is costly because they tend to be huge and are a mix of reference fields (need write barriers) and value fields. We need to copy twice - one is an assignment, the other is a box. See clusters of RhpByRefAssignRefAVLocation1 - that's the write barrier. I'm making the box take a less efficient path based on comments.
  • Logging is constructing a string that doesn't really need to be in generic code.

This is just a couple dozen bytes in savings per method, but ends up saving 50 kB for the whole app because that's how many specializations of the method we have.

Cc @stephentoub@jkotas for thoughts.

Contributes to dotnet#79204.
I don't know if this will be considered mergeable, but I wanted to at least try something. For apps that use async a lot (like the Stage2 app we use for Goldilocks), the async infrastructure can cost 10% of the entire executable.
Shuffling a couple things in `GetStateMachineBox` I was able to get 0.23% saving. It's miniscule. In general async is death by a thousand papercuts so I don't see a silver bullet.
@jkotas

Copy link
Copy Markdown
Member

Copying the state machine is costly because they tend to be huge and are a mix of reference fields (need write barriers) and value fields

That's because RyuJIT inlines the box helper for large structs. I think this should be fixed in the JIT.

Outlining of the event tracing helper looks good.

if (taskField is AsyncStateMachineBox<IAsyncStateMachine> weaklyTypedBox)
else if (taskField is AsyncStateMachineBox<IAsyncStateMachine> weaklyTypedBox)
{
// If this is the first await, we won't yet have a state machine, so store it.

@stephentoubstephentoubApr 26, 2024

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.

If we're trying to squeeze water from a stone, this whole block should be extremely cold (only used in debugging scenarios), so if you can come up with a good way to outline it all to a separate helper, that'd be reasonable. Not sure if that'd help much, though, given that stateMachine is generic.

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.

Yeah, it would likely make things worse because it's still the same code, but with extra unwinding/GC tracking data structures because we have a new method.

@MichalStrehovsky

Copy link
Copy Markdown
MemberAuthor

That's because RyuJIT inlines the box helper for large structs. I think this should be fixed in the JIT.

Do you have a heuristic in mind? It seems like what RyuJIT is doing now is strictly better from throughput perspective (we call the right allocation helper, assign all the fields, only do write barriers for GC pointers). Falling back to the regular boxing helper would be slower. Or are you thinking about a object RuntimeHelpers.Box<T>(ref T memory) that would be like a box, but guaranteed not to be expanded into alloc+set individual fields.

@EgorBo

EgorBo commented Apr 26, 2024

Copy link
Copy Markdown
Member

That's because RyuJIT inlines the box helper for large structs. I think this should be fixed in the JIT.

Do you have a heuristic in mind? It seems like what RyuJIT is doing now is strictly better from throughput perspective (we call the right allocation helper, assign all the fields, only do write barriers for GC pointers). Falling back to the regular boxing helper would be slower. Or are you thinking about a object RuntimeHelpers.Box<T>(ref T memory) that would be like a box, but guaranteed not to be expanded into alloc+set individual fields.

I presume Jan means that for a struct with many gc fields (JIT can check that) it's better for VM/Box helper to do a batch move (BulkMoveWithWriteBarrier) instead of individual write barriers

@jkotas

Copy link
Copy Markdown
Member

It seems like what RyuJIT is doing now is strictly better from throughput perspective

I expect that the bulk copy in the box helper starts to win in throughput above certain size. The JIT should probably give up inlining the copy way below this threshold for good code size / throughput tradeoff.

Also, the current implementation of the box is slow since the box helper is almost never used today. Jeremy hit that problem when switching a few places in reflection to use the box helper (WIP #101137).

@EgorBo

Copy link
Copy Markdown
Member

Quick test:

[Benchmark]publicobjectBox()=>ms;MyStructms;structMyStruct{stringa1;stringa2;stringa3;stringa4;stringa5;stringa6;}

Benchmark for current impl vs using CORINFO_HELP_BOX (which is lowered down to memmoveGCRefs basically):

| Method | Toolchain | Mean |
|------- |---------------------------- |----------:|
| Box | \Core_Root_PR\corerun.exe | 5.832 ns |
| Box | \Core_Root_base\corerun.exe | 10.236 ns |

@MichalStrehovsky

Copy link
Copy Markdown
MemberAuthor

I presume Jan means that for a struct with many gc fields (JIT can check that) it's better for VM/Box helper to do a batch move (BulkMoveWithWriteBarrier) instead of individual write barriers

I wonder if this also means the JIT should use the batch move when assigning structs with many GC fields. Looks like there is a CORINFO_HELP_ASSIGN_STRUCT but I can't find any place in the JIT codebase where it would be used.

The method I'm changing here does two struct assignments with these large structs - one as part of the box, and the other as part of the non-boxing path. Looks like it might be beneficial to treat both cases the same in the JIT and if one is using CORINFO_HELP_BOX, the other should be using ASSIGN_STRUCT.

@EgorBo

EgorBo commented Apr 27, 2024

Copy link
Copy Markdown
Member

I wonder if this also means the JIT should use the batch move when assigning structs with many GC fields.

We discussed this in #99096 tldr: there is always a trade-off

E.g. the code we currently generate is interruption-friendly and is more precise from GC's point of view (we update card table for each gc handle in a struct)

@EgorBo

Copy link
Copy Markdown
Member

Fun fact: no-optimization also improves perf! 🙂

image

@jkotas

Copy link
Copy Markdown
Member

is more precise from GC's point of view (we update card table for each gc handle in a struct)

For box helper, this precision loss in card table updates should not be a concern. The object was just allocated and so it is very unlikely that will be any card table updates necessary. (There was even a discussion at one point about what it would take to skip the write barrier calls completely in cases like this.)

@MichalStrehovsky

Copy link
Copy Markdown
MemberAuthor

I made a draft PR to use the regular boxing helper above a certain number of GC fields at #101669. I hope we'll be able to tune the number with the bots/automation that exists in this repo. A struct with 6 fields, all of them GC fields should obviously use the box helper path but I'm less clear on the criteria for others (does mixing GC and non-GC fields make any difference, etc.?).

// generating this extra code until a better solution is implemented.
var box = new AsyncStateMachineBox<TStateMachine>();
// DebugFinalizableAsyncStateMachineBox looks like a small type, but it actually is not because
// it will have a copy of all the slots from its parent. It will add another hundred(s) bytes

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It might be out of scope for this PR, but how about using an unspecialized DebugFinalizableAsyncStateMachineBox<IAsyncStateMachine> for NativeAOT?

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.

Don't know much about this - if it's only used by managed debuggers, it would not be useful for native AOT anyway. It's ifdeffed out so I'm not concerned about it.

@github-actionsgithub-actionsBot mentioned this pull request May 2, 2024
@MichalStrehovsky
MichalStrehovsky merged commit 507abdf into dotnet:mainMay 2, 2024
@MichalStrehovsky
MichalStrehovsky deleted the async branch May 2, 2024 09:23
michaelgsharp pushed a commit to michaelgsharp/runtime that referenced this pull request May 9, 2024
Contributes to dotnet#79204.
I don't know if this will be considered mergeable, but I wanted to at least try something. For apps that use async a lot (like the Stage2 app we use for Goldilocks), the async infrastructure can cost 10% of the entire executable.
Shuffling a couple things in `GetStateMachineBox` I was able to get 0.23% saving. It's miniscule. In general async is death by a thousand papercuts so I don't see a silver bullet.
Ruihan-Yin pushed a commit to Ruihan-Yin/runtime that referenced this pull request May 30, 2024
Contributes to dotnet#79204.
I don't know if this will be considered mergeable, but I wanted to at least try something. For apps that use async a lot (like the Stage2 app we use for Goldilocks), the async infrastructure can cost 10% of the entire executable.
Shuffling a couple things in `GetStateMachineBox` I was able to get 0.23% saving. It's miniscule. In general async is death by a thousand papercuts so I don't see a silver bullet.
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jun 2, 2024
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.

5 participants

@MichalStrehovsky@jkotas@EgorBo@stephentoub@teo-tsirpanis
, '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

Try to reduce cost of Async - #101605

Merged
MichalStrehovsky merged 2 commits into
dotnet:mainfrom
MichalStrehovsky:async
May 2, 2024
Merged

Try to reduce cost of Async#101605
MichalStrehovsky merged 2 commits into
dotnet:mainfrom
MichalStrehovsky:async

Conversation

@MichalStrehovsky

Copy link
Copy Markdown
Member

Contributes to #79204.

I don't know if this will be considered mergeable, but I wanted to at least try something. For apps that use async a lot (like the Stage2 app we use for Goldilocks), the async infrastructure can cost 10% of the entire executable.

Shuffling a couple things in GetStateMachineBox I was able to get 0.23% saving. It's miniscule. In general async is death by a thousand papercuts so I don't see a silver bullet.

Click to expand existing assembly
 TodosApi!System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<System.Threading.Tasks.VoidTaskResult>__GetStateMachineBox<S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>:00000001`409833704156pushr1400000001`4098337257pushrdi00000001`4098337356pushrsi00000001`4098337455pushrbp00000001`4098337553pushrbx00000001`40983376 4883ec20 subrsp,20h00000001`4098337a 488bf1 movrsi, stateMachine (rcx)00000001`4098337d 488bda movrbx, taskField (rdx)00000001`40983380 e8eb68aaff call TodosApi!System.Threading.ExecutionContext__Capture (140429c70)00000001`40983385 488be8 movrbp,rax00000001`40983388 488b3b movrdi, qword ptr [taskField (rbx)]00000001`4098338b 4885ff testrdi,rdi00000001`4098338e 742c je TodosApi!System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<System.Threading.Tasks.VoidTaskResult>__GetStateMachineBox<S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>+0x4c (1409833bc)00000001`40983390 488d0df93f3e00 learcx,[TodosApi!??_7System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1_AsyncStateMachineBox`1<System.Threading.Tasks.VoidTaskResult__S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>@@6B@ (140d67390)]00000001`40983397 48390f cmp qword ptr [rdi],rcx00000001`4098339a 7520jne TodosApi!System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<System.Threading.Tasks.VoidTaskResult>__GetStateMachineBox<S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>+0x4c (1409833bc)00000001`4098339c 48396f10 cmp qword ptr [stronglyTypedBox (rdi)+10h], currentContext (rbp)00000001`409833a0 740c je TodosApi!System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<System.Threading.Tasks.VoidTaskResult>__GetStateMachineBox<S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>+0x3e (1409833ae)00000001`409833a2 488d4f10 learcx,[stronglyTypedBox (rdi)+10h]00000001`409833a6 488bd5 movrdx,rbp00000001`409833a9 e8921a69ff call TodosApi!RhpAssignRefAVLocation (140014e40)00000001`409833ae 488bc7 movrax,rdi00000001`409833b1 4883c420 addrsp,20h00000001`409833b5 5b poprbx00000001`409833b6 5d poprbp00000001`409833b7 5e poprsi00000001`409833b8 5f poprdi00000001`409833b9 415e popr1400000001`409833bb c3 ret00000001`409833bc 4c8b33 movr14, qword ptr [taskField (rbx)]00000001`409833bf 4d85f6 testr14,r1400000001`409833c2 7461je TodosApi!System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<System.Threading.Tasks.VoidTaskResult>__GetStateMachineBox<S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>+0xb5 (140983425)00000001`409833c4 488d0d7d193e00 learcx,[TodosApi!??_7System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1_AsyncStateMachineBox`1<System.Threading.Tasks.VoidTaskResult__System.Runtime.CompilerServices.IAsyncStateMachine>@@6B@ (140d64d48)]00000001`409833cb 49390e cmp qword ptr [r14],rcx00000001`409833ce 7555jne TodosApi!System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<System.Threading.Tasks.VoidTaskResult>__GetStateMachineBox<S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>+0xb5 (140983425)00000001`409833d0 49837e4000 cmp qword ptr [weaklyTypedBox (r14)+40h],000000001`409833d5 7534jne TodosApi!System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<System.Threading.Tasks.VoidTaskResult>__GetStateMachineBox<S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>+0x9b (14098340b)00000001`409833d7 488d0d1a622a00 learcx,[TodosApi!Boxed_S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100::`vftable' (140c295f8)]00000001`409833de e84d1869ff call TodosApi!RhpNewFast (140014c30)00000001`409833e3 488bd0 movrdx,rax00000001`409833e3 488bd0 movrdx,rax00000001`409833e6 488d7a08 leardi,[rdx+8]00000001`409833ea e8411c69ff call TodosApi!RhpByRefAssignRefAVLocation1 (140015030)00000001`409833ef e83c1c69ff call TodosApi!RhpByRefAssignRefAVLocation1 (140015030)00000001`409833f4 48a5 movs qword ptr [rdi], qword ptr [rsi]00000001`409833f6 e8351c69ff call TodosApi!RhpByRefAssignRefAVLocation1 (140015030)00000001`409833fb e8301c69ff call TodosApi!RhpByRefAssignRefAVLocation1 (140015030)00000001`40983400 48a5 movs qword ptr [rdi], qword ptr [rsi]00000001`40983402 498d4e40 learcx,[weaklyTypedBox (r14)+40h]00000001`40983406 e8351a69ff call TodosApi!RhpAssignRefAVLocation (140014e40)00000001`4098340b 498d4e10 learcx,[weaklyTypedBox (r14)+10h]00000001`4098340f 488bd5 movrdx,rbp00000001`40983412 e8291a69ff call TodosApi!RhpAssignRefAVLocation (140014e40)00000001`40983417 498bc6 movrax,r1400000001`4098341a 4883c420 addrsp,20h00000001`4098341e 5b poprbx00000001`4098341f 5d poprbp00000001`40983420 5e poprsi00000001`40983421 5f poprdi00000001`40983422 415e popr1400000001`40983424 c3 ret00000001`40983425 488d0d643f3e00 learcx,[TodosApi!??_7System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1_AsyncStateMachineBox`1<System.Threading.Tasks.VoidTaskResult__S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>@@6B@ (140d67390)]00000001`4098342c e8ff1769ff call TodosApi!RhpNewFast (140014c30)00000001`40983431 4c8bf0 movr14,rax00000001`40983434 41c7463400040002 mov dword ptr [r14+34h],2000400h00000001`4098343c 41814e3400080000 or dword ptr [r14+34h],800h00000001`40983444 488bcb movrcx,rbx00000001`40983447 498bd6 movrdx,r1400000001`4098344a e8611a69ff call TodosApi!RhpCheckedAssignRefAVLocation (140014eb0)00000001`4098344f 498d7e40 leardi,[r14+40h]00000001`40983453 e8d81b69ff call TodosApi!RhpByRefAssignRefAVLocation1 (140015030)00000001`40983458 e8d31b69ff call TodosApi!RhpByRefAssignRefAVLocation1 (140015030)00000001`4098345d 48a5 movs qword ptr [rdi], qword ptr [rsi]00000001`4098345f e8cc1b69ff call TodosApi!RhpByRefAssignRefAVLocation1 (140015030)00000001`40983464 e8c71b69ff call TodosApi!RhpByRefAssignRefAVLocation1 (140015030)00000001`40983469 48a5 movs qword ptr [rdi], qword ptr [rsi]00000001`4098346b 498d4e10 learcx,[r14+10h]00000001`4098346f 488bd5 movrdx,rbp00000001`40983472 e8c91969ff call TodosApi!RhpAssignRefAVLocation (140014e40)00000001`40983477 488d0dea6f1f00 learcx,[TodosApi!?__NONGCSTATICS@System.Threading.Tasks.TplEventSource@@ (140b7a468)]00000001`4098347e 488379f800 cmp qword ptr [rcx-8],000000001`409834837559jne TodosApi!System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<System.Threading.Tasks.VoidTaskResult>__GetStateMachineBox<S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>+0x16e (1409834de)00000001`40983485 488b0d94b0c400 movrcx, qword ptr [TodosApi!?__GCSTATICS@System.Threading.Tasks.TplEventSource@@ (1415ce520)]00000001`4098348c 488b5908 movrbx, qword ptr [rcx+8]00000001`40983490 80bb9d00000000 cmp byte ptr [rbx+9Dh],000000001`409834977437je TodosApi!System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<System.Threading.Tasks.VoidTaskResult>__GetStateMachineBox<S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>+0x160 (1409834d0)00000001`40983499 498bce movrcx,r1400000001`4098349c e83f27abff call TodosApi!System.Threading.Tasks.Task__get_Id (140435be0)00000001`409834a1 8bf0 movesi,eax00000001`409834a3 488d0de68c1a00 learcx,[TodosApi!__RuntimeType_S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100 (140b2c190)]00000001`409834aa e85161a4ff call TodosApi!System.RuntimeType__get_Name (1403c9600)00000001`409834af 488bd0 movrdx,rax00000001`409834b2 488d0dff5c0e00 learcx,[TodosApi!__Str_Async___4E51D638FE527E2B0672DE04B6E5F6B84BA81E64E12AA0136B0691A50E6F80FE (140a691b8)]00000001`409834b9 e89285a4ff call TodosApi!String__Concat_6 (1403cba50)00000001`409834be 4c8bc0 movr8,rax00000001`409834c1 488bcb movrcx,rbx00000001`409834c4 8bd6 movedx,esi00000001`409834c6 4533c9 xorr9d,r9d00000001`409834c9 3909cmp dword ptr [rcx],ecx00000001`409834cb e8608cabff call TodosApi!System.Threading.Tasks.TplEventSource__TraceOperationBegin (14043c130)00000001`409834d0 498bc6 movrax,r1400000001`409834d3 4883c420 addrsp,20h00000001`409834d7 5b poprbx00000001`409834d8 5d poprbp00000001`409834d9 5e poprsi00000001`409834da 5f poprdi00000001`409834db 415e popr1400000001`409834dd c3 ret00000001`409834de e86a0f68ff call TodosApi!__GetGCStaticBase_System.Threading.Tasks.TplEventSource (14000444d)00000001`409834e3 eba0 jmp TodosApi!System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<System.Threading.Tasks.VoidTaskResult>__GetStateMachineBox<S_P_Xml_System_Xml_XmlUtf8RawTextWriter__WriteEntityRefAsync_d__100>+0x115 (140983485)

Addresses following problems:

  • Multiple expensive epilogs
  • Copying the state machine is costly because they tend to be huge and are a mix of reference fields (need write barriers) and value fields. We need to copy twice - one is an assignment, the other is a box. See clusters of RhpByRefAssignRefAVLocation1 - that's the write barrier. I'm making the box take a less efficient path based on comments.
  • Logging is constructing a string that doesn't really need to be in generic code.

This is just a couple dozen bytes in savings per method, but ends up saving 50 kB for the whole app because that's how many specializations of the method we have.

Cc @stephentoub@jkotas for thoughts.

Contributes to dotnet#79204.
I don't know if this will be considered mergeable, but I wanted to at least try something. For apps that use async a lot (like the Stage2 app we use for Goldilocks), the async infrastructure can cost 10% of the entire executable.
Shuffling a couple things in `GetStateMachineBox` I was able to get 0.23% saving. It's miniscule. In general async is death by a thousand papercuts so I don't see a silver bullet.
@jkotas

Copy link
Copy Markdown
Member

Copying the state machine is costly because they tend to be huge and are a mix of reference fields (need write barriers) and value fields

That's because RyuJIT inlines the box helper for large structs. I think this should be fixed in the JIT.

Outlining of the event tracing helper looks good.

if (taskField is AsyncStateMachineBox<IAsyncStateMachine> weaklyTypedBox)
else if (taskField is AsyncStateMachineBox<IAsyncStateMachine> weaklyTypedBox)
{
// If this is the first await, we won't yet have a state machine, so store it.

@stephentoubstephentoubApr 26, 2024

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.

If we're trying to squeeze water from a stone, this whole block should be extremely cold (only used in debugging scenarios), so if you can come up with a good way to outline it all to a separate helper, that'd be reasonable. Not sure if that'd help much, though, given that stateMachine is generic.

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.

Yeah, it would likely make things worse because it's still the same code, but with extra unwinding/GC tracking data structures because we have a new method.

@MichalStrehovsky

Copy link
Copy Markdown
MemberAuthor

That's because RyuJIT inlines the box helper for large structs. I think this should be fixed in the JIT.

Do you have a heuristic in mind? It seems like what RyuJIT is doing now is strictly better from throughput perspective (we call the right allocation helper, assign all the fields, only do write barriers for GC pointers). Falling back to the regular boxing helper would be slower. Or are you thinking about a object RuntimeHelpers.Box<T>(ref T memory) that would be like a box, but guaranteed not to be expanded into alloc+set individual fields.

@EgorBo

EgorBo commented Apr 26, 2024

Copy link
Copy Markdown
Member

That's because RyuJIT inlines the box helper for large structs. I think this should be fixed in the JIT.

Do you have a heuristic in mind? It seems like what RyuJIT is doing now is strictly better from throughput perspective (we call the right allocation helper, assign all the fields, only do write barriers for GC pointers). Falling back to the regular boxing helper would be slower. Or are you thinking about a object RuntimeHelpers.Box<T>(ref T memory) that would be like a box, but guaranteed not to be expanded into alloc+set individual fields.

I presume Jan means that for a struct with many gc fields (JIT can check that) it's better for VM/Box helper to do a batch move (BulkMoveWithWriteBarrier) instead of individual write barriers

@jkotas

Copy link
Copy Markdown
Member

It seems like what RyuJIT is doing now is strictly better from throughput perspective

I expect that the bulk copy in the box helper starts to win in throughput above certain size. The JIT should probably give up inlining the copy way below this threshold for good code size / throughput tradeoff.

Also, the current implementation of the box is slow since the box helper is almost never used today. Jeremy hit that problem when switching a few places in reflection to use the box helper (WIP #101137).

@EgorBo

Copy link
Copy Markdown
Member

Quick test:

[Benchmark]publicobjectBox()=>ms;MyStructms;structMyStruct{stringa1;stringa2;stringa3;stringa4;stringa5;stringa6;}

Benchmark for current impl vs using CORINFO_HELP_BOX (which is lowered down to memmoveGCRefs basically):

| Method | Toolchain | Mean |
|------- |---------------------------- |----------:|
| Box | \Core_Root_PR\corerun.exe | 5.832 ns |
| Box | \Core_Root_base\corerun.exe | 10.236 ns |

@MichalStrehovsky

Copy link
Copy Markdown
MemberAuthor

I presume Jan means that for a struct with many gc fields (JIT can check that) it's better for VM/Box helper to do a batch move (BulkMoveWithWriteBarrier) instead of individual write barriers

I wonder if this also means the JIT should use the batch move when assigning structs with many GC fields. Looks like there is a CORINFO_HELP_ASSIGN_STRUCT but I can't find any place in the JIT codebase where it would be used.

The method I'm changing here does two struct assignments with these large structs - one as part of the box, and the other as part of the non-boxing path. Looks like it might be beneficial to treat both cases the same in the JIT and if one is using CORINFO_HELP_BOX, the other should be using ASSIGN_STRUCT.

@EgorBo

EgorBo commented Apr 27, 2024

Copy link
Copy Markdown
Member

I wonder if this also means the JIT should use the batch move when assigning structs with many GC fields.

We discussed this in #99096 tldr: there is always a trade-off

E.g. the code we currently generate is interruption-friendly and is more precise from GC's point of view (we update card table for each gc handle in a struct)

@EgorBo

Copy link
Copy Markdown
Member

Fun fact: no-optimization also improves perf! 🙂

image

@jkotas

Copy link
Copy Markdown
Member

is more precise from GC's point of view (we update card table for each gc handle in a struct)

For box helper, this precision loss in card table updates should not be a concern. The object was just allocated and so it is very unlikely that will be any card table updates necessary. (There was even a discussion at one point about what it would take to skip the write barrier calls completely in cases like this.)

@MichalStrehovsky

Copy link
Copy Markdown
MemberAuthor

I made a draft PR to use the regular boxing helper above a certain number of GC fields at #101669. I hope we'll be able to tune the number with the bots/automation that exists in this repo. A struct with 6 fields, all of them GC fields should obviously use the box helper path but I'm less clear on the criteria for others (does mixing GC and non-GC fields make any difference, etc.?).

// generating this extra code until a better solution is implemented.
var box = new AsyncStateMachineBox<TStateMachine>();
// DebugFinalizableAsyncStateMachineBox looks like a small type, but it actually is not because
// it will have a copy of all the slots from its parent. It will add another hundred(s) bytes

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It might be out of scope for this PR, but how about using an unspecialized DebugFinalizableAsyncStateMachineBox<IAsyncStateMachine> for NativeAOT?

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.

Don't know much about this - if it's only used by managed debuggers, it would not be useful for native AOT anyway. It's ifdeffed out so I'm not concerned about it.

@github-actionsgithub-actionsBot mentioned this pull request May 2, 2024
@MichalStrehovsky
MichalStrehovsky merged commit 507abdf into dotnet:mainMay 2, 2024
@MichalStrehovsky
MichalStrehovsky deleted the async branch May 2, 2024 09:23
michaelgsharp pushed a commit to michaelgsharp/runtime that referenced this pull request May 9, 2024
Contributes to dotnet#79204.
I don't know if this will be considered mergeable, but I wanted to at least try something. For apps that use async a lot (like the Stage2 app we use for Goldilocks), the async infrastructure can cost 10% of the entire executable.
Shuffling a couple things in `GetStateMachineBox` I was able to get 0.23% saving. It's miniscule. In general async is death by a thousand papercuts so I don't see a silver bullet.
Ruihan-Yin pushed a commit to Ruihan-Yin/runtime that referenced this pull request May 30, 2024
Contributes to dotnet#79204.
I don't know if this will be considered mergeable, but I wanted to at least try something. For apps that use async a lot (like the Stage2 app we use for Goldilocks), the async infrastructure can cost 10% of the entire executable.
Shuffling a couple things in `GetStateMachineBox` I was able to get 0.23% saving. It's miniscule. In general async is death by a thousand papercuts so I don't see a silver bullet.
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jun 2, 2024
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.

5 participants

@MichalStrehovsky@jkotas@EgorBo@stephentoub@teo-tsirpanis