From 42ec5c24e1fbb1a8c883c90475936aa40caf18b6 Mon Sep 17 00:00:00 2001 From: vsadov <8218165+VSadov@users.noreply.github.com> Date: Wed, 15 Jan 2025 18:27:05 -0800 Subject: [PATCH 1/9] T RuntimeHelpers.Await(Task) --- src/coreclr/jit/importercalls.cpp | 11 ++- src/coreclr/jit/namedintrinsiclist.h | 1 + .../src/CompatibilitySuppressions.xml | 6 ++ src/coreclr/vm/corelib.h | 1 + src/coreclr/vm/metasig.h | 1 + src/coreclr/vm/threadsuspend.cpp | 4 +- .../CompilerServices/RuntimeHelpers.cs | 69 +++++++++++++++++++ .../System.Runtime/ref/System.Runtime.cs | 1 + src/tests/async/varying-yields-await.csproj | 9 +++ src/tests/async/varying-yields.cs | 21 +++++- 10 files changed, 119 insertions(+), 5 deletions(-) create mode 100644 src/tests/async/varying-yields-await.csproj diff --git a/src/coreclr/jit/importercalls.cpp b/src/coreclr/jit/importercalls.cpp index 1bc7423a44bd..ec755aacb025 100644 --- a/src/coreclr/jit/importercalls.cpp +++ b/src/coreclr/jit/importercalls.cpp @@ -528,7 +528,8 @@ var_types Compiler::impImportCall(OPCODE opcode, // calls in JIT generated state machines only. if (compIsAsync2() && ((ni == NI_System_Runtime_CompilerServices_RuntimeHelpers_AwaitAwaiterFromRuntimeAsync) || - (ni == NI_System_Runtime_CompilerServices_RuntimeHelpers_UnsafeAwaitAwaiterFromRuntimeAsync))) + (ni == NI_System_Runtime_CompilerServices_RuntimeHelpers_UnsafeAwaitAwaiterFromRuntimeAsync) || + (ni == NI_System_Runtime_CompilerServices_RuntimeHelpers_Await))) { assert((call != nullptr) && call->OperIs(GT_CALL)); call->AsCall()->gtIsAsyncCall = true; @@ -3376,7 +3377,8 @@ GenTree* Compiler::impIntrinsic(CORINFO_CLASS_HANDLE clsHnd, } if ((ni == NI_System_Runtime_CompilerServices_RuntimeHelpers_AwaitAwaiterFromRuntimeAsync) || - (ni == NI_System_Runtime_CompilerServices_RuntimeHelpers_UnsafeAwaitAwaiterFromRuntimeAsync)) + (ni == NI_System_Runtime_CompilerServices_RuntimeHelpers_UnsafeAwaitAwaiterFromRuntimeAsync) || + (ni == NI_System_Runtime_CompilerServices_RuntimeHelpers_Await)) { // These are marked intrinsics simply to mark the call node as async, // which the caller will do. Make sure we keep pIntrinsicName assigned @@ -10840,6 +10842,11 @@ NamedIntrinsic Compiler::lookupNamedIntrinsic(CORINFO_METHOD_HANDLE method) result = NI_System_Runtime_CompilerServices_RuntimeHelpers_UnsafeAwaitAwaiterFromRuntimeAsync; } + else if (strcmp(methodName, "Await") == 0) + { + result = + NI_System_Runtime_CompilerServices_RuntimeHelpers_Await; + } else if (strcmp(methodName, "SuspendAsync2") == 0) { result = NI_System_Runtime_CompilerServices_RuntimeHelpers_SuspendAsync2; diff --git a/src/coreclr/jit/namedintrinsiclist.h b/src/coreclr/jit/namedintrinsiclist.h index 3b64215ea79a..54b8225d6f4a 100644 --- a/src/coreclr/jit/namedintrinsiclist.h +++ b/src/coreclr/jit/namedintrinsiclist.h @@ -120,6 +120,7 @@ enum NamedIntrinsic : unsigned short NI_System_Runtime_CompilerServices_RuntimeHelpers_GetMethodTable, NI_System_Runtime_CompilerServices_RuntimeHelpers_AwaitAwaiterFromRuntimeAsync, NI_System_Runtime_CompilerServices_RuntimeHelpers_UnsafeAwaitAwaiterFromRuntimeAsync, + NI_System_Runtime_CompilerServices_RuntimeHelpers_Await, NI_System_Runtime_CompilerServices_RuntimeHelpers_SuspendAsync2, NI_System_Runtime_CompilerServices_RuntimeHelpers_get_RuntimeAsyncViaJitGeneratedStateMachines, diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/CompatibilitySuppressions.xml b/src/coreclr/nativeaot/System.Private.CoreLib/src/CompatibilitySuppressions.xml index 4897d4b5ffe1..99abece3c820 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/CompatibilitySuppressions.xml +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/CompatibilitySuppressions.xml @@ -857,6 +857,12 @@ CP0002 M:System.Threading.Lock.#ctor(System.Boolean) + + CP0002 + M:System.Runtime.CompilerServices.RuntimeHelpers.Await``1(System.Threading.Tasks.Task{``0}) + ref/net10.0/System.Private.CoreLib.dll + lib/net10.0/System.Private.CoreLib.dll + CP0002 M:System.Runtime.CompilerServices.RuntimeHelpers.AwaitAwaiterFromRuntimeAsync``1(``0) diff --git a/src/coreclr/vm/corelib.h b/src/coreclr/vm/corelib.h index abea233a0744..e9537ff001ec 100644 --- a/src/coreclr/vm/corelib.h +++ b/src/coreclr/vm/corelib.h @@ -685,6 +685,7 @@ DEFINE_METHOD(RUNTIME_HELPERS, FINALIZE_VALUETASK_RETURNING_THUNK, Finalize DEFINE_METHOD(RUNTIME_HELPERS, FINALIZE_VALUETASK_RETURNING_THUNK_1, FinalizeValueTaskReturningThunk, GM_Continuation_RetValueTaskOfT) DEFINE_METHOD(RUNTIME_HELPERS, UNSAFE_AWAIT_AWAITER_FROM_RUNTIME_ASYNC_1, UnsafeAwaitAwaiterFromRuntimeAsync, GM_T_RetVoid) DEFINE_METHOD(RUNTIME_HELPERS, AWAIT_AWAITER_FROM_RUNTIME_ASYNC_1, AwaitAwaiterFromRuntimeAsync, GM_T_RetVoid) +DEFINE_METHOD(RUNTIME_HELPERS, AWAIT_1, Await, GM_TaskOfT_RetT) DEFINE_CLASS(SPAN_HELPERS, System, SpanHelpers) DEFINE_METHOD(SPAN_HELPERS, MEMSET, Fill, SM_RefByte_Byte_UIntPtr_RetVoid) diff --git a/src/coreclr/vm/metasig.h b/src/coreclr/vm/metasig.h index 7b6ac5cf69e4..a4f0a9231863 100644 --- a/src/coreclr/vm/metasig.h +++ b/src/coreclr/vm/metasig.h @@ -571,6 +571,7 @@ DEFINE_METASIG_T(IM(RetValueTask, _, g(VALUETASK))) DEFINE_METASIG_T(GM(Exception_RetTaskOfT, IMAGE_CEE_CS_CALLCONV_DEFAULT, 1, C(EXCEPTION), GI(C(TASK_1), 1, M(0)))) DEFINE_METASIG_T(GM(T_RetTaskOfT, IMAGE_CEE_CS_CALLCONV_DEFAULT, 1, M(0), GI(C(TASK_1), 1, M(0)))) +DEFINE_METASIG_T(GM(TaskOfT_RetT, IMAGE_CEE_CS_CALLCONV_DEFAULT, 1, GI(C(TASK_1), 1, M(0)), M(0))) DEFINE_METASIG_T(GM(Exception_RetValueTaskOfT, IMAGE_CEE_CS_CALLCONV_DEFAULT, 1, C(EXCEPTION), GI(g(VALUETASK_1), 1, M(0)))) DEFINE_METASIG_T(GM(T_RetValueTaskOfT, IMAGE_CEE_CS_CALLCONV_DEFAULT, 1, M(0), GI(g(VALUETASK_1), 1, M(0)))) diff --git a/src/coreclr/vm/threadsuspend.cpp b/src/coreclr/vm/threadsuspend.cpp index 6aec5a3ef491..1bc08bf81b8a 100644 --- a/src/coreclr/vm/threadsuspend.cpp +++ b/src/coreclr/vm/threadsuspend.cpp @@ -4915,7 +4915,9 @@ bool IsSpecialCaseAsyncRet(MethodDesc* pMD) // causing loading to happen? Also, can we just mark them as async2 in SPC, // or force them to be fully interruptible? LPCUTF8 name = pMD->GetName(); - return strcmp(name, "UnsafeAwaitAwaiterFromRuntimeAsync") == 0 || strcmp(name, "AwaitAwaiterFromRuntimeAsync") == 0; + return strcmp(name, "UnsafeAwaitAwaiterFromRuntimeAsync") == 0 || + strcmp(name, "AwaitAwaiterFromRuntimeAsync") == 0 || + strcmp(name, "Await") == 0; } static bool GetReturnAddressHijackInfo(EECodeInfo *pCodeInfo, ReturnKind *pReturnKind, bool* hasAsyncRet) diff --git a/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/RuntimeHelpers.cs b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/RuntimeHelpers.cs index f5dfe8afc578..d49f3a0fe344 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/RuntimeHelpers.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/RuntimeHelpers.cs @@ -208,6 +208,75 @@ public static void UnsafeAwaitAwaiterFromRuntimeAsync(TAwaiter awaiter SuspendAsync2(sentinelContinuation); return; } + + // TODO: should this be called "AwaitFromRuntimeAsync" ? (i.e. same as above, but no "Awaiter") + // + // Marked intrinsic since this needs to be + // recognizes as an async2 call. + [Intrinsic] + [BypassReadyToRun] + [MethodImpl(MethodImplOptions.NoInlining)] + public static unsafe T Await(Task task) + { + // TODO: handle complete tasks more efficiently. + //if (!task.IsCompleted) + //{ + ref RuntimeAsyncAwaitState state = ref t_runtimeAsyncAwaitState; + Continuation? sentinelContinuation = state.SentinelContinuation; + if (sentinelContinuation == null) + state.SentinelContinuation = sentinelContinuation = new Continuation(); + + Continuation myContinuation = new Continuation(); + myContinuation.GCData = new object[] { task }; + myContinuation.Resume = &AwaitHelper.Resume; + + state.Notifier = task.GetAwaiter(); + sentinelContinuation.Next = myContinuation; + + // RETURN {default(T), myContinuation} + // + SuspendAsync2(myContinuation); + + // unreachable + return task.ResultOnSuccess; + //} + //else + //{ + // // RETURN {task.Result, null} + // // + // T result = task.Result; + // ReturnAsync2(Unsafe.AsPointer(ref result)); + // + // // unreachable + // return result; + //} + } + + internal static class AwaitHelper + { + public static Continuation? Resume(Continuation continuation) + { + Task task = (Task)continuation.GCData![0]!; + Continuation next = continuation.Next!; + + if (IsReferenceOrContainsReferences()) + { + next.GCData![0] = task.Result; + } + else + { + int retIndex = + (next.Flags & CorInfoContinuationFlags.CORINFO_CONTINUATION_OSR_IL_OFFSET_IN_DATA) != 0 ? + 4 : + 0; + + // TODO: WriteUnaligned? + Unsafe.As(ref next.Data![retIndex]) = task.Result; + } + + return null; + } + } #endif } } diff --git a/src/libraries/System.Runtime/ref/System.Runtime.cs b/src/libraries/System.Runtime/ref/System.Runtime.cs index 0cd7d5b7fce5..d58f86cd27a3 100644 --- a/src/libraries/System.Runtime/ref/System.Runtime.cs +++ b/src/libraries/System.Runtime/ref/System.Runtime.cs @@ -13708,6 +13708,7 @@ public static void RunModuleConstructor(System.ModuleHandle module) { } public delegate void TryCode(object? userData); public static void UnsafeAwaitAwaiterFromRuntimeAsync(TAwaiter awaiter) where TAwaiter : ICriticalNotifyCompletion { } public static void AwaitAwaiterFromRuntimeAsync(TAwaiter awaiter) where TAwaiter : INotifyCompletion { } + public static T Await(System.Threading.Tasks.Task task) { throw null; } } public sealed partial class RuntimeWrappedException : System.Exception { diff --git a/src/tests/async/varying-yields-await.csproj b/src/tests/async/varying-yields-await.csproj new file mode 100644 index 000000000000..a36e587b0764 --- /dev/null +++ b/src/tests/async/varying-yields-await.csproj @@ -0,0 +1,9 @@ + + + True + AWAIT;$(DefineConstants) + + + + + diff --git a/src/tests/async/varying-yields.cs b/src/tests/async/varying-yields.cs index 6edc30e8118d..50de5a4920fb 100644 --- a/src/tests/async/varying-yields.cs +++ b/src/tests/async/varying-yields.cs @@ -4,6 +4,8 @@ //#define ASYNC1_TASK //#define ASYNC1_VALUETASK +#pragma warning disable 4014, 1998 + using System; using System.Collections.Generic; using System.Diagnostics; @@ -91,9 +93,19 @@ async2 Task double liveState3 = _yieldProbability; if (depth == 0) +#if AWAIT + return RuntimeHelpers.Await(Loop()); +#else return await Loop(); +#endif + + long result = +#if AWAIT + RuntimeHelpers.Await(Run(depth - 1)); +#else + await Run(depth - 1); +#endif - long result = await Run(depth - 1); Sink = (int)liveState1 + (int)liveState2 + (int)(1 / liveState3) + depth; return result; } @@ -117,7 +129,12 @@ async2 Task { for (int i = 0; i < 20; i++) { - numIters += await DoYields(); + numIters += +#if AWAIT + RuntimeHelpers.Await(DoYields()); +#else + await DoYields(); +#endif } } From 76ca9dad9f023c18603ea1f339b483326637424a Mon Sep 17 00:00:00 2001 From: vsadov <8218165+VSadov@users.noreply.github.com> Date: Fri, 17 Jan 2025 23:06:47 -0800 Subject: [PATCH 2/9] state machine version of Await and friends --- src/coreclr/jit/importercalls.cpp | 6 +- src/coreclr/vm/method.hpp | 18 +++-- src/coreclr/vm/methodtablebuilder.cpp | 13 +++- .../CompilerServices/RuntimeHelpers.cs | 70 +++---------------- 4 files changed, 38 insertions(+), 69 deletions(-) diff --git a/src/coreclr/jit/importercalls.cpp b/src/coreclr/jit/importercalls.cpp index ec755aacb025..7725b32dac51 100644 --- a/src/coreclr/jit/importercalls.cpp +++ b/src/coreclr/jit/importercalls.cpp @@ -914,7 +914,7 @@ var_types Compiler::impImportCall(OPCODE opcode, impPopCallArgs(sig, call->AsCall()); // Extra args - if ((instParam != nullptr) || sig->isAsyncCall() || (varArgsCookie != nullptr)) + if ((instParam != nullptr) || call->AsCall()->IsAsync2() || (varArgsCookie != nullptr)) { if (Target::g_tgtArgOrder == Target::ARG_ORDER_R2L) { @@ -924,7 +924,7 @@ var_types Compiler::impImportCall(OPCODE opcode, .WellKnown(WellKnownArg::VarArgsCookie)); } - if (sig->isAsyncCall()) + if (call->AsCall()->IsAsync2()) { call->AsCall()->gtArgs.PushFront(this, NewCallArg::Primitive(gtNewNull(), TYP_REF) .WellKnown(WellKnownArg::AsyncContinuation)); @@ -944,7 +944,7 @@ var_types Compiler::impImportCall(OPCODE opcode, NewCallArg::Primitive(instParam).WellKnown(WellKnownArg::InstParam)); } - if (sig->isAsyncCall()) + if (call->AsCall()->IsAsync2()) { call->AsCall()->gtArgs.PushBack(this, NewCallArg::Primitive(gtNewNull(), TYP_REF) .WellKnown(WellKnownArg::AsyncContinuation)); diff --git a/src/coreclr/vm/method.hpp b/src/coreclr/vm/method.hpp index 49168a9b734a..2ebee8a097de 100644 --- a/src/coreclr/vm/method.hpp +++ b/src/coreclr/vm/method.hpp @@ -70,7 +70,12 @@ enum class AsyncMethodKind AsyncImplHelper, // Synthetic Async2 method that forwards to the NotAsync Task-returning method - AsyncThunkHelper + AsyncThunkHelper, + + // Actual IL method that is explicitly declared as Async2 and thus compiled into a state machine. + // Such methods do not get Async thunks and can only be called from another Async2 method using Async2 call convention. + // This is used in a few infrastructure methods like `Await` + AsyncImplExplicit, }; struct AsyncMethodData @@ -1829,10 +1834,13 @@ class MethodDesc // CONSIDER: We probably need a better name for the concept, but it is hard to beat shortness of "async2" inline bool IsAsync2Method() const { - // Right now the only Async2 methods that exist are synthetic helpers. - // It is possible to declare an Async2 method directly in IL/Metadata, - // but we do not have a scenario for that. - return IsAsyncHelperMethod(); + LIMITED_METHOD_DAC_CONTRACT; + if (!HasAsyncMethodData()) + return false; + auto asyncKind = GetAddrOfAsyncMethodData()->kind; + return asyncKind == AsyncMethodKind::AsyncThunkHelper || + asyncKind == AsyncMethodKind::AsyncImplHelper || + asyncKind == AsyncMethodKind::AsyncImplExplicit; } inline bool IsStructMethodOperatingOnCopy() diff --git a/src/coreclr/vm/methodtablebuilder.cpp b/src/coreclr/vm/methodtablebuilder.cpp index f864e625aa8a..c1765dfd3d2e 100644 --- a/src/coreclr/vm/methodtablebuilder.cpp +++ b/src/coreclr/vm/methodtablebuilder.cpp @@ -3605,7 +3605,18 @@ MethodTableBuilder::EnumerateClassMethods() else { _ASSERTE(IsAsyncTaskMethodNormal(asyncMethodType)); - pNewMethod->SetAsyncMethodKind(AsyncMethodKind::NotAsync); + + if (IsMiAsync(dwImplFlags)) + { + // TODO: VS must validate that only a few special methods can do this. + // the possibility is useful, but should not become a general + // feature by accident. + pNewMethod->SetAsyncMethodKind(AsyncMethodKind::AsyncImplExplicit); + } + else + { + pNewMethod->SetAsyncMethodKind(AsyncMethodKind::NotAsync); + } } pDeclaredMethod = pNewMethod; diff --git a/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/RuntimeHelpers.cs b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/RuntimeHelpers.cs index d49f3a0fe344..0c946d16aca2 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/RuntimeHelpers.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/RuntimeHelpers.cs @@ -178,8 +178,8 @@ public static unsafe ReadOnlySpan CreateSpan(RuntimeFieldHandle fldHandle) #if !NATIVEAOT [Intrinsic] - [MethodImpl(MethodImplOptions.NoInlining)] [BypassReadyToRun] + [MethodImpl(MethodImplOptions.NoInlining | (MethodImplOptions)0x0400)] // NoInlining | Async public static void AwaitAwaiterFromRuntimeAsync(TAwaiter awaiter) where TAwaiter : INotifyCompletion { ref RuntimeAsyncAwaitState state = ref t_runtimeAsyncAwaitState; @@ -196,7 +196,7 @@ public static void AwaitAwaiterFromRuntimeAsync(TAwaiter awaiter) wher // recognizes as an async2 call. [Intrinsic] [BypassReadyToRun] - [MethodImpl(MethodImplOptions.NoInlining)] + [MethodImpl(MethodImplOptions.NoInlining | (MethodImplOptions)0x0400)] // NoInlining | Async public static void UnsafeAwaitAwaiterFromRuntimeAsync(TAwaiter awaiter) where TAwaiter : ICriticalNotifyCompletion { ref RuntimeAsyncAwaitState state = ref t_runtimeAsyncAwaitState; @@ -215,68 +215,18 @@ public static void UnsafeAwaitAwaiterFromRuntimeAsync(TAwaiter awaiter // recognizes as an async2 call. [Intrinsic] [BypassReadyToRun] - [MethodImpl(MethodImplOptions.NoInlining)] - public static unsafe T Await(Task task) + [MethodImpl(MethodImplOptions.NoInlining | (MethodImplOptions)0x0400)] // NoInlining | Async + public static T Await(Task task) { - // TODO: handle complete tasks more efficiently. - //if (!task.IsCompleted) - //{ - ref RuntimeAsyncAwaitState state = ref t_runtimeAsyncAwaitState; - Continuation? sentinelContinuation = state.SentinelContinuation; - if (sentinelContinuation == null) - state.SentinelContinuation = sentinelContinuation = new Continuation(); - - Continuation myContinuation = new Continuation(); - myContinuation.GCData = new object[] { task }; - myContinuation.Resume = &AwaitHelper.Resume; - - state.Notifier = task.GetAwaiter(); - sentinelContinuation.Next = myContinuation; - - // RETURN {default(T), myContinuation} - // - SuspendAsync2(myContinuation); - - // unreachable - return task.ResultOnSuccess; - //} - //else - //{ - // // RETURN {task.Result, null} - // // - // T result = task.Result; - // ReturnAsync2(Unsafe.AsPointer(ref result)); - // - // // unreachable - // return result; - //} - } - - internal static class AwaitHelper - { - public static Continuation? Resume(Continuation continuation) + TaskAwaiter awaiter = task.GetAwaiter(); + if (!awaiter.IsCompleted) { - Task task = (Task)continuation.GCData![0]!; - Continuation next = continuation.Next!; - - if (IsReferenceOrContainsReferences()) - { - next.GCData![0] = task.Result; - } - else - { - int retIndex = - (next.Flags & CorInfoContinuationFlags.CORINFO_CONTINUATION_OSR_IL_OFFSET_IN_DATA) != 0 ? - 4 : - 0; - - // TODO: WriteUnaligned? - Unsafe.As(ref next.Data![retIndex]) = task.Result; - } - - return null; + UnsafeAwaitAwaiterFromRuntimeAsync(awaiter); } + + return awaiter.GetResult(); } + #endif } } From ed94235ca3caa2ce29b7b1f099a00f2d93d623e2 Mon Sep 17 00:00:00 2001 From: vsadov <8218165+VSadov@users.noreply.github.com> Date: Tue, 21 Jan 2025 16:27:51 -0800 Subject: [PATCH 3/9] bump roslyn ref --- buildroslynnugets.cmd | 4 ++-- eng/Versions.props | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/buildroslynnugets.cmd b/buildroslynnugets.cmd index 281ddee98868..bc51f7ce2d7d 100644 --- a/buildroslynnugets.cmd +++ b/buildroslynnugets.cmd @@ -1,7 +1,7 @@ setlocal ENABLEEXTENSIONS pushd %~dp0 -set ASYNC_ROSLYN_COMMIT=10a5611cb10cd64876a2638664f0740255197e1b -set ASYNC_SUFFIX=async-11 +set ASYNC_ROSLYN_COMMIT=c1d47ed4bdbd28137fce8fbb350771677aa4cf15 +set ASYNC_SUFFIX=async-12 set ASYNC_ROSLYN_BRANCH=demos/async2-experiment1 cd .. diff --git a/eng/Versions.props b/eng/Versions.props index 21c3ead510a7..3fb08863235a 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -44,9 +44,9 @@ Any tools that contribute to the design-time experience should use the MicrosoftCodeAnalysisVersion_LatestVS property above to ensure they do not break the local dev experience. --> - 4.13.0-async-11 - 4.13.0-async-11 - 4.13.0-async-11 + 4.13.0-async-12 + 4.13.0-async-12 + 4.13.0-async-12