Skip to content

Conversation

@MichalStrehovsky
Copy link
Member

This is doubly inefficient because:

  • The method pointed to by the delegate is guaranteed not runtime-async (so we'd go through up to two thunks), and
  • the variant of Invoke method is no longer the Delegate.Invoke method that can be reported as CORINFO_FLG_DELEGATE_INVOKE to RyuJIT (needs to be invoked as a regular method instead of the efficient delegate invoke sequence).

CoreCLR does this in methodtablebuilder.cs where we consider all methods on delegates as MethodReturnKind::NormalMethod.

Also adding a test because it looks like we don't have one.

Cc @dotnet/ilc-contrib

This would be doubly inefficient because:

* The method pointed to by the delegate is guaranteed not runtime-async (so we'd go through up to two thunks), and
* the variant of `Invoke` method is no longer _the_ `Delegate.Invoke` method that can be reported as `CORINFO_FLG_DELEGATE_INVOKE` to RyuJIT (needs to be invoked as a regular method instead of the efficient delegate invoke sequence).

CoreCLR does this in methodtablebuilder.cs where we consider all methods on delegates as `MethodReturnKind::NormalMethod`.
@dotnet-policy-service
Copy link
Contributor

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

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes an inefficiency where Delegate.Invoke methods were being runtime-awaited, which is problematic because the method pointed to by the delegate is guaranteed not to be runtime-async. The fix adds checks in two compiler locations to prevent async variants from being used for delegate types, and includes a test to validate the fix.

Key changes:

  • Added delegate type check in ILImporter.Scanner to skip async variant generation
  • Added delegate type check in CorInfoImpl to prevent async variant resolution
  • Added comprehensive test covering both compiler-async and runtime-async delegate scenarios

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.

File Description
src/tests/async/delegates/delegates.csproj New test project file for delegate async testing
src/tests/async/delegates/delegates.cs New test validating delegate invocation with both compiler and runtime async methods
src/coreclr/tools/aot/ILCompiler.Compiler/IL/ILImporter.Scanner.cs Added check to skip async variant when method's owning type is a delegate
src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs Added check with explanatory comment to prevent async variant resolution for delegate methods

@jkotas
Copy link
Member

jkotas commented Dec 12, 2025

CoreCLR does this in methodtablebuilder.cs where we consider all methods on delegates as MethodReturnKind::NormalMethod.

Hmm, interesting. It means that ReturnsTaskOrValueTask; in CoreCLR does not 100% match what its name says.

@jkotas
Copy link
Member

jkotas commented Dec 12, 2025

Hmm, interesting. It means that ReturnsTaskOrValueTask; in CoreCLR does not 100% match what its name says.

cc @VSadov

@VSadov
Copy link
Member

VSadov commented Dec 12, 2025

Virtual/interface dispatch is the only kind of indirection through which we can propagate async right now.

Method pointers do not have practical solutions. (exposing async call convention to the user is probably too big of a hammer).

Delegate might be doable in some scenarios. They are just hard as they have all kind of things - argument shuffling, multicast, BeginInvoke, ... etc. Also the most common and interesting case Func<T> has generic return type so technically its Invoke can't be Task-returning.
Propagating async through delegates, if it is possible at all, was left as a challenge for the future.

@VSadov
Copy link
Member

VSadov commented Dec 12, 2025

Hmm, interesting. It means that ReturnsTaskOrValueTask; in CoreCLR does not 100% match what its name says.

cc @VSadov

ReturnsTaskOrValueTask really means "has an async call variant". But HasAsyncCallVariant might be too much of a self-serving name.

I wonder what we do with Task-returning vararg methods... Perhaps should not be classified as ReturnsTaskOrValueTask either.
Let's add a test: #122499

@jakobbotsch
Copy link
Member

Delegate might be doable in some scenarios. They are just hard as they have all kind of things - argument shuffling, multicast, BeginInvoke, ... etc. Also the most common and interesting case Func<T> has generic return type.
Propagating async through delegates, if it is possible at all, was left as a challenge for the future.

I think this is something we will be optimizing via PGO and GDV (and the corresponding "whole-program view" optimizations on the NAOT side).

Co-authored-by: Jan Kotas <jkotas@microsoft.com>
@MichalStrehovsky MichalStrehovsky merged commit 37dd34d into dotnet:main Dec 13, 2025
104 checks passed
@MichalStrehovsky MichalStrehovsky deleted the delegateasync branch December 13, 2025 10:37
@github-actions github-actions bot locked and limited conversation to collaborators Jan 13, 2026
Sign up for free to 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.

4 participants