-
Notifications
You must be signed in to change notification settings - Fork 5.4k
JIT: Refactor async-to-sync call optimization #124798
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -8556,7 +8556,7 @@ void Compiler::impImportBlockCode(BasicBlock* block) | |||||||||||||||
| JITDUMP(" %08X", resolvedToken.token); | ||||||||||||||||
|
|
||||||||||||||||
| eeGetCallInfo(&resolvedToken, (prefixFlags & PREFIX_CONSTRAINED) ? &constrainedResolvedToken : nullptr, | ||||||||||||||||
| combine(CORINFO_CALLINFO_SECURITYCHECKS, CORINFO_CALLINFO_LDFTN), &callInfo); | ||||||||||||||||
| CORINFO_CALLINFO_SECURITYCHECKS | CORINFO_CALLINFO_LDFTN, &callInfo); | ||||||||||||||||
|
|
||||||||||||||||
| // This check really only applies to intrinsic Array.Address methods | ||||||||||||||||
| if (callInfo.sig.callConv & CORINFO_CALLCONV_PARAMTYPE) | ||||||||||||||||
|
|
@@ -8595,8 +8595,7 @@ void Compiler::impImportBlockCode(BasicBlock* block) | |||||||||||||||
| JITDUMP(" %08X", resolvedToken.token); | ||||||||||||||||
|
|
||||||||||||||||
| eeGetCallInfo(&resolvedToken, nullptr /* constraint typeRef */, | ||||||||||||||||
| combine(combine(CORINFO_CALLINFO_SECURITYCHECKS, CORINFO_CALLINFO_LDFTN), | ||||||||||||||||
| CORINFO_CALLINFO_CALLVIRT), | ||||||||||||||||
| CORINFO_CALLINFO_SECURITYCHECKS | CORINFO_CALLINFO_LDFTN | CORINFO_CALLINFO_CALLVIRT, | ||||||||||||||||
| &callInfo); | ||||||||||||||||
|
|
||||||||||||||||
| // This check really only applies to intrinsic Array.Address methods | ||||||||||||||||
|
|
@@ -8729,7 +8728,7 @@ void Compiler::impImportBlockCode(BasicBlock* block) | |||||||||||||||
| _impResolveToken(CORINFO_TOKENKIND_NewObj); | ||||||||||||||||
|
|
||||||||||||||||
| eeGetCallInfo(&resolvedToken, nullptr /* constraint typeRef*/, | ||||||||||||||||
| combine(CORINFO_CALLINFO_SECURITYCHECKS, CORINFO_CALLINFO_ALLOWINSTPARAM), &callInfo); | ||||||||||||||||
| CORINFO_CALLINFO_SECURITYCHECKS | CORINFO_CALLINFO_ALLOWINSTPARAM, &callInfo); | ||||||||||||||||
|
|
||||||||||||||||
| mflags = callInfo.methodFlags; | ||||||||||||||||
|
|
||||||||||||||||
|
|
@@ -8982,16 +8981,8 @@ void Compiler::impImportBlockCode(BasicBlock* block) | |||||||||||||||
|
|
||||||||||||||||
| if (isAwait) | ||||||||||||||||
| { | ||||||||||||||||
| _impResolveToken(opcode == CEE_CALLVIRT ? CORINFO_TOKENKIND_AwaitVirtual | ||||||||||||||||
| : CORINFO_TOKENKIND_Await); | ||||||||||||||||
| if (resolvedToken.hMethod != nullptr) | ||||||||||||||||
| { | ||||||||||||||||
| // There is a runtime async variant that is implicitly awaitable, just call that. | ||||||||||||||||
| // skip the await pattern to the last token. | ||||||||||||||||
| codeAddr = codeAddrAfterMatch; | ||||||||||||||||
| opcodeOffs = awaitOffset; | ||||||||||||||||
| } | ||||||||||||||||
| else | ||||||||||||||||
| _impResolveToken(CORINFO_TOKENKIND_Await); | ||||||||||||||||
| if (resolvedToken.hMethod == nullptr) | ||||||||||||||||
| { | ||||||||||||||||
| // This can happen in cases when the Task-returning method is not a runtime Async | ||||||||||||||||
| // function. For example "T M1<T>(T arg) => arg" when called with a Task argument. | ||||||||||||||||
|
|
@@ -9009,12 +9000,45 @@ void Compiler::impImportBlockCode(BasicBlock* block) | |||||||||||||||
| _impResolveToken(CORINFO_TOKENKIND_Method); | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| CORINFO_CALLINFO_FLAGS flags = CORINFO_CALLINFO_ALLOWINSTPARAM | CORINFO_CALLINFO_SECURITYCHECKS; | ||||||||||||||||
| if (opcode == CEE_CALLVIRT) | ||||||||||||||||
| { | ||||||||||||||||
| flags |= CORINFO_CALLINFO_CALLVIRT; | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| eeGetCallInfo(&resolvedToken, | ||||||||||||||||
| (prefixFlags & PREFIX_CONSTRAINED) ? &constrainedResolvedToken : nullptr, | ||||||||||||||||
| // this is how impImportCall invokes getCallInfo | ||||||||||||||||
| combine(combine(CORINFO_CALLINFO_ALLOWINSTPARAM, CORINFO_CALLINFO_SECURITYCHECKS), | ||||||||||||||||
| (opcode == CEE_CALLVIRT) ? CORINFO_CALLINFO_CALLVIRT : CORINFO_CALLINFO_NONE), | ||||||||||||||||
| (prefixFlags & PREFIX_CONSTRAINED) ? &constrainedResolvedToken : nullptr, flags, | ||||||||||||||||
| &callInfo); | ||||||||||||||||
|
|
||||||||||||||||
| if (isAwait && (callInfo.kind == CORINFO_CALL)) | ||||||||||||||||
| { | ||||||||||||||||
| assert(callInfo.sig.isAsyncCall()); | ||||||||||||||||
| bool isSyncCallThunk; | ||||||||||||||||
| info.compCompHnd->getAsyncOtherVariant(callInfo.hMethod, &isSyncCallThunk); | ||||||||||||||||
|
||||||||||||||||
| info.compCompHnd->getAsyncOtherVariant(callInfo.hMethod, &isSyncCallThunk); | |
| CORINFO_METHOD_HANDLE asyncOtherVariant = | |
| info.compCompHnd->getAsyncOtherVariant(callInfo.hMethod, &isSyncCallThunk); | |
| if ((asyncOtherVariant != nullptr) && !isSyncCallThunk) | |
| { | |
| callInfo.hMethod = asyncOtherVariant; | |
| } |
Copilot
AI
Feb 24, 2026
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.
The variable name isSyncCallThunk is misleading. When getAsyncOtherVariant is called on an async method, it returns the sync (task-returning) variant. The bool parameter indicates whether that returned sync variant is a thunk. Consider renaming to otherVariantIsThunk or syncVariantIsThunk to make the meaning clearer.
| bool isSyncCallThunk; | |
| info.compCompHnd->getAsyncOtherVariant(callInfo.hMethod, &isSyncCallThunk); | |
| if (!isSyncCallThunk) | |
| bool syncVariantIsThunk; | |
| info.compCompHnd->getAsyncOtherVariant(callInfo.hMethod, &syncVariantIsThunk); | |
| if (!syncVariantIsThunk) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1558,6 +1558,27 @@ static CORINFO_RESOLVED_TOKEN CreateResolvedTokenFromMethod(CorInfoImpl jitInter | |
| return null; | ||
| } | ||
|
|
||
| private CORINFO_METHOD_STRUCT_* getAsyncOtherVariant(CORINFO_METHOD_STRUCT_* ftn, ref bool variantIsThunk) | ||
| { | ||
| MethodDesc method = HandleToObject(ftn); | ||
| if (method.IsAsyncVariant()) | ||
|
Comment on lines
+1561
to
+1564
|
||
| { | ||
| method = method.GetTargetOfAsyncVariant(); | ||
| } | ||
| else if (method.Signature.ReturnsTaskOrValueTask()) | ||
| { | ||
| method = method.GetAsyncVariant(); | ||
| } | ||
| else | ||
| { | ||
| variantIsThunk = false; | ||
| return null; | ||
| } | ||
|
|
||
| variantIsThunk = method?.IsAsyncThunk() ?? false; | ||
| return ObjectToHandle(method); | ||
| } | ||
|
|
||
| private CORINFO_CLASS_STRUCT_* getDefaultComparerClass(CORINFO_CLASS_STRUCT_* elemType) | ||
| { | ||
| TypeDesc comparand = HandleToObject(elemType); | ||
|
|
@@ -1778,7 +1799,7 @@ private object GetRuntimeDeterminedObjectForToken(ref CORINFO_RESOLVED_TOKEN pRe | |
| if (pResolvedToken.tokenType == CorInfoTokenKind.CORINFO_TOKENKIND_Newarr) | ||
| result = ((TypeDesc)result).MakeArrayType(); | ||
|
|
||
| if (pResolvedToken.tokenType is CorInfoTokenKind.CORINFO_TOKENKIND_Await or CorInfoTokenKind.CORINFO_TOKENKIND_AwaitVirtual) | ||
| if (pResolvedToken.tokenType is CorInfoTokenKind.CORINFO_TOKENKIND_Await) | ||
| result = _compilation.TypeSystemContext.GetAsyncVariantMethod((MethodDesc)result); | ||
|
|
||
| return result; | ||
|
|
@@ -1868,7 +1889,7 @@ private void resolveToken(ref CORINFO_RESOLVED_TOKEN pResolvedToken) | |
| _compilation.NodeFactory.MetadataManager.GetDependenciesDueToAccess(ref _additionalDependencies, _compilation.NodeFactory, (MethodIL)methodIL, method); | ||
| #endif | ||
|
|
||
| if (pResolvedToken.tokenType is CorInfoTokenKind.CORINFO_TOKENKIND_Await or CorInfoTokenKind.CORINFO_TOKENKIND_AwaitVirtual) | ||
| if (pResolvedToken.tokenType is CorInfoTokenKind.CORINFO_TOKENKIND_Await) | ||
| { | ||
| // in rare cases a method that returns Task is not actually TaskReturning (i.e. returns T). | ||
| // we cannot resolve to an Async variant in such case. | ||
|
|
@@ -1878,20 +1899,6 @@ private void resolveToken(ref CORINFO_RESOLVED_TOKEN pResolvedToken) | |
| // Don't get async variant of Delegate.Invoke method; the pointed to method is not an async variant either. | ||
| allowAsyncVariant = allowAsyncVariant && !method.OwningType.IsDelegate; | ||
|
|
||
| #if !READYTORUN | ||
| if (allowAsyncVariant) | ||
| { | ||
| bool isDirect = pResolvedToken.tokenType == CorInfoTokenKind.CORINFO_TOKENKIND_Await || method.IsCallEffectivelyDirect(); | ||
| if (isDirect && !method.IsAsync) | ||
| { | ||
| // Async variant would be a thunk. Do not resolve direct calls | ||
| // to async thunks. That just creates and JITs unnecessary | ||
| // thunks, and the thunks are harder for the JIT to optimize. | ||
| allowAsyncVariant = false; | ||
| } | ||
| } | ||
| #endif | ||
|
|
||
| method = allowAsyncVariant | ||
| ? _compilation.TypeSystemContext.GetAsyncVariantMethod(method) | ||
| : null; | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -39,6 +39,7 @@ static ICorJitInfoCallbacks() | |||||||||||||||||||||
| s_callbacks.resolveVirtualMethod = &_resolveVirtualMethod; | ||||||||||||||||||||||
| s_callbacks.getUnboxedEntry = &_getUnboxedEntry; | ||||||||||||||||||||||
| s_callbacks.getInstantiatedEntry = &_getInstantiatedEntry; | ||||||||||||||||||||||
| s_callbacks.getAsyncOtherVariant = &_getAsyncOtherVariant; | ||||||||||||||||||||||
| s_callbacks.getDefaultComparerClass = &_getDefaultComparerClass; | ||||||||||||||||||||||
| s_callbacks.getDefaultEqualityComparerClass = &_getDefaultEqualityComparerClass; | ||||||||||||||||||||||
| s_callbacks.getSZArrayHelperEnumeratorClass = &_getSZArrayHelperEnumeratorClass; | ||||||||||||||||||||||
|
|
@@ -220,6 +221,7 @@ static ICorJitInfoCallbacks() | |||||||||||||||||||||
| public delegate* unmanaged<IntPtr, IntPtr*, CORINFO_DEVIRTUALIZATION_INFO*, byte> resolveVirtualMethod; | ||||||||||||||||||||||
| public delegate* unmanaged<IntPtr, IntPtr*, CORINFO_METHOD_STRUCT_*, bool*, CORINFO_METHOD_STRUCT_*> getUnboxedEntry; | ||||||||||||||||||||||
| public delegate* unmanaged<IntPtr, IntPtr*, CORINFO_METHOD_STRUCT_*, CORINFO_METHOD_STRUCT_**, CORINFO_CLASS_STRUCT_**, CORINFO_METHOD_STRUCT_*> getInstantiatedEntry; | ||||||||||||||||||||||
| public delegate* unmanaged<IntPtr, IntPtr*, CORINFO_METHOD_STRUCT_*, bool*, CORINFO_METHOD_STRUCT_*> getAsyncOtherVariant; | ||||||||||||||||||||||
| public delegate* unmanaged<IntPtr, IntPtr*, CORINFO_CLASS_STRUCT_*, CORINFO_CLASS_STRUCT_*> getDefaultComparerClass; | ||||||||||||||||||||||
| public delegate* unmanaged<IntPtr, IntPtr*, CORINFO_CLASS_STRUCT_*, CORINFO_CLASS_STRUCT_*> getDefaultEqualityComparerClass; | ||||||||||||||||||||||
| public delegate* unmanaged<IntPtr, IntPtr*, CORINFO_CLASS_STRUCT_*, CORINFO_CLASS_STRUCT_*> getSZArrayHelperEnumeratorClass; | ||||||||||||||||||||||
|
|
@@ -665,6 +667,21 @@ private static byte _resolveVirtualMethod(IntPtr thisHandle, IntPtr* ppException | |||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| [UnmanagedCallersOnly] | ||||||||||||||||||||||
| private static CORINFO_METHOD_STRUCT_* _getAsyncOtherVariant(IntPtr thisHandle, IntPtr* ppException, CORINFO_METHOD_STRUCT_* ftn, bool* variantIsThunk) | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| var _this = GetThis(thisHandle); | ||||||||||||||||||||||
| try | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| return _this.getAsyncOtherVariant(ftn, ref *variantIsThunk); | ||||||||||||||||||||||
|
||||||||||||||||||||||
| return _this.getAsyncOtherVariant(ftn, ref *variantIsThunk); | |
| bool localVariantIsThunk = variantIsThunk is not null && *variantIsThunk; | |
| CORINFO_METHOD_STRUCT_* result = _this.getAsyncOtherVariant(ftn, ref localVariantIsThunk); | |
| if (variantIsThunk is not null) | |
| { | |
| *variantIsThunk = localVariantIsThunk; | |
| } | |
| return result; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,6 +7,9 @@ | |
| <Platform Name="x64" /> | ||
| <Platform Name="x86" /> | ||
| </Configurations> | ||
| <Project Path="../../../tools/illink/src/ILLink.CodeFix/ILLink.CodeFixProvider.csproj"> | ||
| <BuildType Solution="Checked|*" Project="Debug" /> | ||
| </Project> | ||
|
Comment on lines
+10
to
+12
|
||
| <Project Path="../../../tools/illink/src/ILLink.RoslynAnalyzer/ILLink.RoslynAnalyzer.csproj"> | ||
| <BuildType Solution="Checked|*" Project="Debug" /> | ||
| </Project> | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,6 +7,9 @@ | |
| <Platform Name="x64" /> | ||
| <Platform Name="x86" /> | ||
| </Configurations> | ||
| <Project Path="../../../tools/illink/src/ILLink.CodeFix/ILLink.CodeFixProvider.csproj"> | ||
| <BuildType Solution="Checked|*" Project="Debug" /> | ||
| </Project> | ||
|
Comment on lines
+10
to
+12
|
||
| <Project Path="../../../tools/illink/src/ILLink.RoslynAnalyzer/ILLink.RoslynAnalyzer.csproj"> | ||
| <BuildType Solution="Checked|*" Project="Debug" /> | ||
| </Project> | ||
|
|
||
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.
With CORINFO_TOKENKIND_AwaitVirtual removed, ResolveAsyncCallToken now always resolves using CORINFO_TOKENKIND_Await (even for direct CEE_CALL). Previously the VM could avoid returning an async-thunk variant for direct calls; now the interpreter may end up calling thunks unnecessarily. Consider applying the same direct-call check as the JIT importer (after getCallInfo: if callInfo.kind==CORINFO_CALL and the async variant is a thunk, fall back to resolving the regular method token).