Skip to content

Commit 192bdc3

Browse files
[clr-interp] Runtime async support (#121862)
This PR adds runtime async support to the interpreter Notable changes: 1. Wrap async method bodies in a try/finally block to capture/restore the exec and sync contexts, just like synchronized methods. 2. Add a series of instructions for doing a suspend/resume. This set is currently somewhat longer than ideal, and we could simplify this by having a set of managed helper functions to reduce the number of instructions needed. 3. The continuation is filled in with a copy of the data from the stack. 4. The calling convention is non-traditional for the extra return value. For the continuation argument, instead of representing it on the stack as part of the return value, we represent it as a field on the InterpreterFrame, and update and check that frame as needed. This allows the vast majority of the code in the compiler to ignore the concept of runtime-async. 5. The call stub generator is also modified to pass the extra parameter, but more significantly, all of the stubs which call into the interpreter are setup to now capture the continuation argument and put it into the special return register. 6. Tests have been updated to do less work when running under the interpreter. Some of the existing tests were designed specifically to trigger various rejit thresholds, and while that is a fine thing to test in the JIT, the interpreter is FAR slower, and needs a different approach. TODO before merge ~1. Remove the changes to enable RuntimeAsync by default.~ ~2. Add documentation about how the interpreter async abi works~ ~3. Add documentation about what a code generator needs to do to make runtime async work~ ~4. I'm pretty sure IL peep handling has a bug handling any of the 2 byte opcodes. This needs to be tested.~ --------- Co-authored-by: Jakob Botsch Nielsen <[email protected]>
1 parent f6d570f commit 192bdc3

File tree

63 files changed

+2829
-407
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

63 files changed

+2829
-407
lines changed

docs/design/coreclr/botr/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ Below is a table of contents.
3030
- [Mixed Mode Assemblies](mixed-mode.md)
3131
- [Guide For Porting](guide-for-porting.md)
3232
- [Vectors and Intrinsics](vectors-and-intrinsics.md)
33+
- [Runtime Async Codegen](runtime-async-codegen.md)
3334

3435

3536
It may be possible that this table is not complete. You can get a complete list

docs/design/coreclr/botr/clr-abi.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -678,3 +678,19 @@ MyStruct Test2()
678678
return default;
679679
}
680680
```
681+
682+
# Interpreter ABI details
683+
684+
The interpreter data stack is separately allocated from the normal "thread" stack, and it grows UP. The interpreter execution control stack is allocated on the "thread" stack, as a series of `InterpMethodContextFrame` values that are linked in a singly linked list onto an `InterpreterFrame` which is placed onto the Frame chain of the thread. `InterpMethodContextFrame` structures are always allocated in descending order so that a callee method's associated `InterpMethodContextFrame` is always located lower in memory compared to its caller or the containing `InterpreterFrame`.
685+
686+
The base stack pointer within a method never changes, but when a function is called in the interpreter it will have a stack pointer which is associated with the set of arguments passed. In effect argument passing is done by giving a portion of the temporary args space of the caller function to the callee.
687+
688+
All instructions and GC that address the stack pointer are relative to the current stack pointer, which does not move. This requires that implementations of the localloc instruction actually allocate the memory on the heap, and localloc'd memory is not actually tied to the data stack in any way.
689+
690+
The stack pointer in all interpreter functions is always aligned on a `INTERP_STACK_ALIGNMENT` boundary. Currently this is a 16 byte alignment requirement.
691+
692+
The stack elements are always aligned to at least `INTERP_STACK_SLOT_SIZE` and never more than `INTERP_STACK_ALIGNMENT` Given that today's implementation sets `INTERP_STACK_SLOT_SIZE` to 8 and `INTERP_STACK_ALIGNMENT` to 16, this implies all data on the stack is either aligned at an 8 or 16 byte alignment.
693+
694+
Primitive types smaller than 4 bytes are always zero or sign extended to 4 bytes when on the stack.
695+
696+
When a function is async it will have a continuation return. This return is not done using the data stack, but instead is done by setting the Continuation field in the `InterpreterFrame`. Thunks are responsible for setting/resetting this value as we enter/leave code compiled by the JIT.
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
# Responsibilities of a code generator for implementing the Runtime Async feature
2+
3+
This document describes the behaviors that a code generator must conform to to correctly make the runtime async feature work correctly.
4+
5+
This document is NOT intended to describe the runtime-async feature. That is better described in the runtime-async specification. See (https://github.com/dotnet/runtime/blob/main/docs/design/specs/runtime-async.md).
6+
7+
8+
9+
The general responsibilities of the runtime-async code generator
10+
11+
1. Wrap the body of Task and ValueTask returning functions in try/finally blocks which set/reset the `ExecutionContext` and `SynchronizationContext`.
12+
13+
2. Allow the async thunk logic to work.
14+
15+
3. Generate Async Debug info (Not yet described in this document)f
16+
17+
18+
19+
# Identifying calls to Runtime-Async methods that can be handled by runtime-async
20+
21+
When compiling a call to a method that might be called in the optimized fashion, recognize the following sequence.
22+
23+
```
24+
call[virt] <Method>
25+
[ OPTIONAL ]
26+
{
27+
[ OPTIONAL - Used for ValueTask based ConfigureAwait ]
28+
{
29+
stloc X;
30+
ldloca X
31+
}
32+
ldc.i4.0 / ldc.i4.1
33+
call[virt] <ConfigureAwait> (The virt instruction is used for ConfigureAwait on a Task based runtime async function) NI_System_Threading_Tasks_Task_ConfigureAwait
34+
}
35+
call <Await> One of the functions which matches NI_System_Runtime_CompilerServices_AsyncHelpers_Await
36+
```
37+
38+
A search for this sequence is done if Method is known to be async.
39+
40+
The dispatch to async functions save the `ExecutionContext` on suspension and restore it on resumption via `AsyncHelpers.CaptureExecutionContext` and `AsyncHelpers.RestoreExecutionContext` respectively
41+
42+
If PREFIX_TASK_AWAIT_CONTINUE_ON_CAPTURED_CONTEXT, then continuation mode shall be ContinuationContextHandling::ContinueOnCapturedContext otherwise ContinuationContextHandling::ContinueOnThreadPool.
43+
44+
45+
46+
# Non-optimized pattern
47+
48+
It is also legal for code to have a simple direct usage of `AsyncHelpers.Await`, `AsyncHelpers.AwaitAwaiter`, `AsyncHelpers.UnsafeAwaitAwaiter` or `AsyncHelpers.TransparentAwait`. To support this these functions are marked as async even though they do not return a Task/ValueTask.
49+
50+
Like other async calls the dispatch to these functions will save and restore the execution context on suspension/resumption.
51+
52+
The dispatch to these functions will set continuation mode to ContinuationContextHandling::None
53+
54+
# Calli of an async function
55+
56+
The dispatch to these functions will save and restore the execution context only on async dispatch.
57+
58+
# The System.Runtime.CompilerServices.AsyncHelpers::AsyncSuspend intrinsic
59+
60+
When encountered, triggers the function to suspend immediately, and return the passed in Continuation.
61+
62+
# Saving and restoring of contexts
63+
64+
Capture the execution context before the suspension, and when the function resumes, call `AsyncHelpers.RestoreExecutionContext`. The context should be stored into the Continuation. The context may be captured by calling `AsyncHelpers.CaptureExecutionContext`.
65+
66+
# ABI for async function handling
67+
68+
There is an additional argument which is the Continuation. When calling a function normally, this is always set to 0. When resuming, this is set to the Continuation object. There is also an extra return argument. It is either 0 or a Continuation. If it is a continuation, then the calling function needs to suspend (if it is an async function), or generate a Task/ValueTask (if it is a async function wrapper).
69+
70+
## Suspension path
71+
72+
This is what is used in calls to async functions made from async functions.
73+
74+
```
75+
bool didSuspend = false; // Needed for the context restore
76+
77+
(result, continuation) = call func(NULL /\* Continuation argument \*/, args)
78+
if (continuation != NULL)
79+
{
80+
// Allocate new continuation
81+
// Capture Locals
82+
// Copy resumption details into continuation (Do things like call AsyncHelpers.CaptureContinuationContext or AsyncHelpers.CaptureExecutionContext as needed)
83+
// Chain to continuation returned from called function
84+
// IF in a function which saves the exec and sync contexts, and we haven't yet suspended, restore the old values.
85+
// return.
86+
87+
// Resumption point
88+
89+
// Copy values out of continuation (including captured sync context and execution context locals)
90+
// If the continuation may have an exception, check to see if its there, and if it is, throw it. Do this if CORINFO\_CONTINUATION\_HAS\_EXCEPTION is set.
91+
// If the continuation has a return value, copy it out of the continuation. (CORINFO\_CONTINUATION\_HAS\_RESULT is set)
92+
}
93+
```
94+
95+
## Thunks path
96+
97+
This is what is used in non-async functions when calling an async function. Generally used in the AsyncResumptionStub and in the Task returning thunk.
98+
```
99+
(result, continuation) = call func(NULL /\* Continuation argument \*/, args)
100+
place result onto IL evaluation stack
101+
Place continuation into a local for access using the StubHelpers.AsyncCallContinuation() helper function.
102+
```
103+
104+
Implement an intrinsic for StubHelpers.AsyncCallContinuation() which will load the most recent value stored into the continuation local.
105+
106+
# Behavior of ContinuationContextHandling
107+
108+
This only applies to calls which where ContinuationContextHandling is not ContinuationContextHandling::None.
109+
110+
If set to ContinuationContextHandling::ContinueOnCapturedContext
111+
112+
- The Continuation shall have an allocated data member for the captured context, and the CORINFO_CONTINUATION_HAS_CONTINUATION_CONTEXT flag shall be set on the continuation.
113+
114+
- The Continuation will store the captured synchronization context. This is done by calling `AsyncHelpers.CaptureContinuationContext(ref newContinuation.ContinuationContext, ref newContinuation.Flags)` while filling in the `Continuation`.
115+
116+
If set to ContinuationContextHandling::ContinueOnThreadPool
117+
- The Continuation shall have the CORINFO_CONTINUATION_CONTINUE_ON_THREAD_POOL flag set
118+
119+
# Exception handling behavior
120+
121+
If an async function is called within a try block (In the jit hasTryIndex return true), set the CORINFO\_CONTINUATION\_HAS\_EXCEPTION bit on the Continuation and make it large enough.
122+
123+
# Locals handling
124+
125+
ByRef locals must not be captured. In fact, we should NULL out any locals which are ByRefs or ByRef-like. Currently we do not do this on synchronous execution, but logically possibly we should.
126+
127+
# Saving and restoring the synchronization and execution contexts
128+
129+
The code generator must save/restore the sync and execution contexts around the body of all Task/ValueTask methods when directly called with a null continuation context. The EE communicates when this is necessary with the `CORINFO_ASYNC_SAVE_CONTEXTS` flag returned through `getMethodInfo`.

src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.cs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Diagnostics.CodeAnalysis;
77
using System.Numerics;
88
using System.Reflection;
9+
using System.Runtime.CompilerServices;
910
using System.Runtime.InteropServices;
1011
using System.Runtime.Serialization;
1112
using System.Runtime.Versioning;
@@ -83,7 +84,7 @@ internal enum ContinuationFlags
8384
ContinueOnCapturedTaskScheduler = 64,
8485
}
8586

86-
// Keep in sync with dataAsyncResumeInfo in the JIT
87+
// Keep in sync with CORINFO_AsyncResumeInfo in corinfo.h
8788
internal unsafe struct ResumeInfo
8889
{
8990
public delegate*<Continuation, ref byte, Continuation?> Resume;
@@ -144,6 +145,18 @@ public ref byte GetResultStorageOrNull()
144145

145146
public static partial class AsyncHelpers
146147
{
148+
#if FEATURE_INTERPRETER
149+
[LibraryImport(RuntimeHelpers.QCall, EntryPoint = "AsyncHelpers_ResumeInterpreterContinuation")]
150+
private static partial void AsyncHelpers_ResumeInterpreterContinuation(ObjectHandleOnStack cont, ref byte resultStorage);
151+
152+
internal static Continuation? ResumeInterpreterContinuation(Continuation cont, ref byte resultStorage)
153+
{
154+
ObjectHandleOnStack contHandle = ObjectHandleOnStack.Create(ref cont);
155+
AsyncHelpers_ResumeInterpreterContinuation(contHandle, ref resultStorage);
156+
return cont;
157+
}
158+
#endif
159+
147160
// This is the "magic" method on which other "Await" methods are built.
148161
// Calling this from an Async method returns the continuation to the caller thus
149162
// explicitly initiates suspension.

src/coreclr/inc/corinfo.h

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1856,7 +1856,13 @@ enum { LCL_FINALLY_MARK = 0xFC }; // FC = "Finally Call"
18561856
* when it generates code
18571857
**********************************************************************************/
18581858

1859-
typedef void* CORINFO_MethodPtr; // a generic method pointer
1859+
#ifdef TARGET_64BIT
1860+
typedef uint64_t TARGET_SIZE_T;
1861+
#else
1862+
typedef uint32_t TARGET_SIZE_T;
1863+
#endif
1864+
1865+
typedef TARGET_SIZE_T CORINFO_MethodPtr; // a generic method pointer
18601866

18611867
struct CORINFO_Object
18621868
{
@@ -1929,19 +1935,25 @@ struct CORINFO_RefArray : public CORINFO_Object
19291935
CORINFO_Object* refElems[1]; // actually of variable size;
19301936
};
19311937

1932-
struct CORINFO_RefAny
1933-
{
1934-
void * dataPtr;
1935-
CORINFO_CLASS_HANDLE type;
1936-
};
1937-
19381938
// The jit assumes the CORINFO_VARARGS_HANDLE is a pointer to a subclass of this
19391939
struct CORINFO_VarArgInfo
19401940
{
19411941
unsigned argBytes; // number of bytes the arguments take up.
19421942
// (The CORINFO_VARARGS_HANDLE counts as an arg)
19431943
};
19441944

1945+
// Note: Keep synchronized with AsyncHelpers.ResumeInfo
1946+
// Any changes to this are an R2R breaking change. Update the R2R verion as needed
1947+
struct CORINFO_AsyncResumeInfo
1948+
{
1949+
// delegate*<Continuation, ref byte, Continuation>
1950+
TARGET_SIZE_T Resume;
1951+
// Pointer in main code for diagnostics. See comments on
1952+
// ICorDebugInfo::AsyncSuspensionPoint::DiagnosticNativeOffset and
1953+
// ResumeInfo.DiagnosticIP in SPC.
1954+
TARGET_SIZE_T DiagnosticIP;
1955+
};
1956+
19451957
struct CORINFO_TYPE_LAYOUT_NODE
19461958
{
19471959
// Type handle if this is a SIMD type, i.e. for intrinsic types in

0 commit comments

Comments
 (0)