-
Notifications
You must be signed in to change notification settings - Fork 206
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement a change in IL API to use RuntimeHelpers.Await<T>(Task<T>) and similar helpers. #2951
base: feature/async2-experiment
Are you sure you want to change the base?
Conversation
AssertEqual("B", strings.B); | ||
AssertEqual("C", strings.C); | ||
AssertEqual("D", strings.D); | ||
// TODO: need to fix this |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jakobbotsch the change stresses calling via thunks and possibly introduced some scenarios that tests did not cover before. Remarkably, nearly everything works fine!! However, here I saw an assert and turned off one scenario.
Not sure if this is something wrong with IL or something on the JIT side.
(the other disabled case is with thunks for async methods in structs).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've hit that before when encoding method/type spec tokens incorrectly. Can you verify that the tokens being encoded when we construct the IL for the variants look fine?
It would be nice to start on this work to see how it would look before we make the switch. Note that most of the work will be VM work -- teaching |
The optimization would need to detect the following pattern
and turn it into
For that there should be a way to:
Is this correct? for
for
|
There are a few ways to do this, but maybe the most straightforward will be to do it as a direct IL pattern match at the point where we call runtimelab/src/coreclr/jit/importer.cpp Lines 8952 to 8957 in b077e29
This would be changed to first look ahead for another There are some other details to work out, like properly setting up for opportunistic tailcalls when the
I would skip
|
Yes. I included the arguments in the example to show that they do not need to change. I was thinking of looking back at previous instruction once we see an Await intrinsic, and if previous instruction was a call that we can optimize, replace it with a call to async method. The rest makes sense. Thanks! |
Implemented the JIT optimization as discussed above. |
…benchmarking purposes)
The impact of the optimization is quite noticeable (as expected):
178 ms is definitely an improvement over 1173 ms. |
src/coreclr/vm/jitinterface.cpp
Outdated
if (flags & CORINFO_CALLINFO_RUNTIMEASYNC_VARIANT) | ||
{ | ||
_ASSERTE(!pMD->IsAsync2Method()); | ||
pMD = pMD->GetAsyncOtherVariant(); | ||
pResolvedToken->hMethod = (CORINFO_METHOD_HANDLE)pMD; | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice, that's much simpler than I was expecting.
pResolvedToken
is in-only, so we should make a copy of it here and change that one instead. If necessary you can update it from the callInfo
on the JIT side, but I'm somewhat worried we end up with a token whose fields are internally inconsistent.
Can you make sure we have tests for some of the hard cases? GVMs, interface calls, virtual class calls and constrained calls come to mind. I was expecting shared generics to require more work as well since other fields of the token are used below for those (see ComputeRuntimeLookupForSharedGenericToken
). Can you double check why it works out? Is the method spec/type spec ok to reuse as-is from the token?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another way to ensure the resolved token consistency could be to pass the new flag not to the eeGetCallInfo
, but to the impResolveToken
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've moved the MethodDesc shimming to the level of impResolveToken
. That seems nicer as it allows eeGetCallInfo
to stay unchanged.
@@ -586,6 +586,8 @@ OPT_CONFIG_INTEGER(JitDoIfConversion, "JitDoIfConversion", 1) | |||
OPT_CONFIG_INTEGER(JitDoOptimizeMaskConversions, "JitDoOptimizeMaskConversions", 1) // Perform optimization of mask | |||
// conversions | |||
|
|||
RELEASE_CONFIG_INTEGER(JitOptimizeAwait, "JitOptimizeAwait", 1) // Perform optimization of Await intrinsics |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wouldn't add a release knob for this.
} | ||
|
||
awaiter.GetResult(); | ||
return; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return; |
} | ||
|
||
awaiter.GetResult(); | ||
return; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return; |
This is the actual implementation of what was proposed in dotnet/runtime#110420 and prototyped in #2941
Basically, this changes
await
marker to be just a call via a specialAwait
helper.When user writes inside a runtime async method
C# compiler emits an equivalent of
The
T Await<T>(Task<T> arg)
method is a special intrinsic method that performs asynchronous awaiting of theTask<int>
.NOTE: There is no
sync-over-async
here,Await
can optionally suspend/resume the current stack of calls and when theTask<int>
is complete, unwraps it and returnsint
.Also, the JIT is familiar with the pattern and can further optimize it into call-with-continuation invocation of the runtime-async entry point for
ReturnsTaskOfInt()
.As a result, if
ReturnsTaskOfInt
is another runtime-async method, we skip intermediate promise types (Task/ValueTask) entirely, which is the main reason for the performance edge of runtime async over the classic async.