-
Notifications
You must be signed in to change notification settings - Fork 5.4k
Add support for runtime async in the scanner #121622
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
Changes from all commits
51e48d2
dc0ae97
f130302
492267e
608ecea
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 |
|---|---|---|
|
|
@@ -69,6 +69,8 @@ public enum ImportState : byte | |
| private DependencyList _dependencies; | ||
| private BasicBlock _lateBasicBlocks; | ||
|
|
||
| private bool _asyncDependenciesReported; | ||
|
|
||
| private sealed class ExceptionRegion | ||
| { | ||
| public ILExceptionRegion ILRegion; | ||
|
|
@@ -177,6 +179,14 @@ public ILImporter(ILScanner compilation, MethodDesc method, MethodIL methodIL = | |
| } | ||
| } | ||
|
|
||
| if (_canonMethod.IsAsyncCall()) | ||
| { | ||
| const string reason = "Async state machine"; | ||
| DefType asyncHelpers = _compilation.TypeSystemContext.SystemModule.GetKnownType("System.Runtime.CompilerServices"u8, "AsyncHelpers"u8); | ||
| _dependencies.Add(_factory.MethodEntrypoint(asyncHelpers.GetKnownMethod("CaptureContexts"u8, null)), reason); | ||
| _dependencies.Add(_factory.MethodEntrypoint(asyncHelpers.GetKnownMethod("RestoreContexts"u8, null)), reason); | ||
| } | ||
|
|
||
| FindBasicBlocks(); | ||
| ImportBasicBlocks(); | ||
|
|
||
|
|
@@ -310,6 +320,99 @@ private IMethodNode GetMethodEntrypoint(MethodDesc method) | |
| return _factory.MethodEntrypointOrTentativeMethod(method); | ||
| } | ||
|
|
||
| // Check if a method call starts a task await pattern that can be | ||
| // optimized for runtime async. | ||
| // Roughly corresponds to impMatchTaskAwaitPattern in RyuJIT codebase | ||
| private bool MatchTaskAwaitPattern() | ||
| { | ||
| // We look for the following code patterns in runtime async methods: | ||
| // | ||
| // call[virt] <Method> | ||
| // [ OPTIONAL ] | ||
| // { | ||
| // [ OPTIONAL ] | ||
| // { | ||
| // stloc X; | ||
| // ldloca X | ||
| // } | ||
| // ldc.i4.0 / ldc.i4.1 | ||
| // call[virt] <ConfigureAwait> | ||
| // } | ||
| // call <Await> | ||
|
|
||
| // Find where this basic block ends | ||
| int nextBBOffset = _currentOffset; | ||
| while (nextBBOffset < _basicBlocks.Length && _basicBlocks[nextBBOffset] == null) | ||
| nextBBOffset++; | ||
|
|
||
| // Create ILReader for what's left in the basic block | ||
| var reader = new ILReader(new ReadOnlySpan<byte>(_ilBytes, _currentOffset, nextBBOffset - _currentOffset)); | ||
|
|
||
| if (!reader.HasNext) | ||
| return false; | ||
|
|
||
| ILOpcode opcode; | ||
|
|
||
| // If we can read at least two call tokens + an ldc, this could be ConfigureAwait | ||
| // so check for that. | ||
| if (reader.Size > 2 * (1 + sizeof(int))) | ||
| { | ||
| opcode = reader.ReadILOpcode(); | ||
|
|
||
| // ConfigureAwait on a ValueTask will start with stloc/ldloca. | ||
| int stlocNum = opcode switch | ||
| { | ||
| >= ILOpcode.stloc_0 and <= ILOpcode.stloc_3 => opcode - ILOpcode.stloc_0, | ||
| ILOpcode.stloc => reader.ReadILUInt16(), | ||
| ILOpcode.stloc_s => reader.ReadILByte(), | ||
| _ => -1, | ||
| }; | ||
|
|
||
| // if it was a stloc, check for matching ldloca | ||
| if (stlocNum != -1) | ||
| { | ||
| opcode = reader.ReadILOpcode(); | ||
| int ldlocaNum = opcode switch | ||
| { | ||
| ILOpcode.ldloca_s => reader.ReadILByte(), | ||
| ILOpcode.ldloca => reader.ReadILUInt16(), | ||
| _ => -1, | ||
| }; | ||
|
|
||
| if (stlocNum != ldlocaNum) | ||
| return false; | ||
|
|
||
| opcode = reader.ReadILOpcode(); | ||
| } | ||
|
|
||
| if (opcode is (not ILOpcode.ldc_i4_0) and (not ILOpcode.ldc_i4_1)) | ||
| { | ||
| if (stlocNum != -1) | ||
| { | ||
| // we had stloc/ldloca, we must see ConfigAwait | ||
| return false; | ||
| } | ||
|
|
||
| goto checkForAwait; | ||
| } | ||
|
|
||
| opcode = reader.ReadILOpcode(); | ||
| if (opcode is (not ILOpcode.call) and (not ILOpcode.callvirt) | ||
| || !IsTaskConfigureAwait((MethodDesc)_methodIL.GetObject(reader.ReadILToken())) | ||
| || !reader.HasNext) | ||
| { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| opcode = reader.ReadILOpcode(); | ||
|
|
||
| checkForAwait: | ||
|
|
||
| return opcode == ILOpcode.call | ||
| && IsAsyncHelpersAwait((MethodDesc)_methodIL.GetObject(reader.ReadILToken())); | ||
| } | ||
|
|
||
| private void ImportCall(ILOpcode opcode, int token) | ||
| { | ||
| // We get both the canonical and runtime determined form - JitInterface mostly operates | ||
|
|
@@ -346,6 +449,40 @@ private void ImportCall(ILOpcode opcode, int token) | |
| Debug.Assert(false); break; | ||
| } | ||
|
|
||
| // Are we scanning a call within a state machine? | ||
| if (opcode is ILOpcode.call or ILOpcode.callvirt | ||
| && _canonMethod.IsAsyncCall()) | ||
| { | ||
| // Add dependencies on infra to do suspend/resume. We only need to do this once per method scanned. | ||
| if (!_asyncDependenciesReported && method.IsAsync) | ||
| { | ||
| _asyncDependenciesReported = true; | ||
|
|
||
| const string asyncReason = "Async state machine"; | ||
|
|
||
| var resumptionStub = new AsyncResumptionStub(_canonMethod, _compilation.TypeSystemContext.GeneratedAssembly.GetGlobalModuleType()); | ||
| _dependencies.Add(_compilation.NodeFactory.MethodEntrypoint(resumptionStub), asyncReason); | ||
|
|
||
| _dependencies.Add(_factory.ConstructedTypeSymbol(_compilation.TypeSystemContext.ContinuationType), asyncReason); | ||
|
|
||
| DefType asyncHelpers = _compilation.TypeSystemContext.SystemModule.GetKnownType("System.Runtime.CompilerServices"u8, "AsyncHelpers"u8); | ||
|
|
||
| _dependencies.Add(_factory.MethodEntrypoint(asyncHelpers.GetKnownMethod("AllocContinuation"u8, null)), asyncReason); | ||
| _dependencies.Add(_factory.MethodEntrypoint(asyncHelpers.GetKnownMethod("CaptureExecutionContext"u8, null)), asyncReason); | ||
| _dependencies.Add(_factory.MethodEntrypoint(asyncHelpers.GetKnownMethod("RestoreExecutionContext"u8, null)), asyncReason); | ||
| _dependencies.Add(_factory.MethodEntrypoint(asyncHelpers.GetKnownMethod("CaptureContinuationContext"u8, null)), asyncReason); | ||
| } | ||
|
|
||
| // If this is the task await pattern, we're actually going to call the variant | ||
| // so switch our focus to the variant. | ||
| if (method.GetTypicalMethodDefinition().Signature.ReturnsTaskOrValueTask() | ||
| && MatchTaskAwaitPattern()) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In the JIT case the call to There are ways how the optimization can be defeated (store the call result in a field, await the field), so "unoptimized" scenarios are possible, but may not be covered by regular tests as we tend not to do such things intentionally. Thus disabling the optimization is an extra test coverage that can be useful sometimes. Just something to consider.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also note that the The pattern match may need to be reordered with respect to the above && method.IsAsync
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. An interesting mental exercise is what happens to: int x = await await ReturnsTaskOfTaskOfInt();In the inner await, the task may represent asynchrony in the
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
We don't run the scanner unless we're optimizing. The scanner can and should assume the optimization will happen (we also special case various intrinsics as mustExpand when compiling for native AOT). If the optimization doesn't happen when the method is NoOptimization (can't tell - looks like it still does), we could gate it on that, but otherwise we don't want to assume this optimization doesn't happen when building whole program view. Assuming it might not happen means we'd waste virtual slots because we'd need to assume both variant slots are always used when scanning (one of the objectives of scanning is to eliminate unused virtual slots). We really do care about working set.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Like I wrote on Teams, we don't have coverage of unoptimized async codegen in the src/tests/async tree because all the tests do
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The await transform based on the IL pattern match happens even in debug codegen. I do not think we would ever change that. The ability to disable it is a JIT debug/checked only option based on an environment variable. I do not think ILC needs to try to support it. (IIUC this would be a problem since the scanner would underestimate the set.) Down the road it is likely we will build more cases where we transform to direct calls to async variants. For example,
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I did not mean a flag for hooking it up to The only reason to disable the optimization are:
Same can be achieved by simply commenting out the call to pattern matcher in the source and rebuilding. A knob could be slightly more conveninent. This was just a mild suggestion. If it does not fit into workflow with NativeAOT codebase, it is completely ok to not have a switch. |
||
| { | ||
| runtimeDeterminedMethod = _factory.TypeSystemContext.GetAsyncVariantMethod(runtimeDeterminedMethod); | ||
| method = _factory.TypeSystemContext.GetAsyncVariantMethod(method); | ||
| } | ||
| } | ||
|
|
||
| if (opcode == ILOpcode.newobj) | ||
| { | ||
| TypeDesc owningType = runtimeDeterminedMethod.OwningType; | ||
|
|
@@ -1550,6 +1687,42 @@ private static bool IsMemoryMarshalGetArrayDataReference(MethodDesc method) | |
| return false; | ||
| } | ||
|
|
||
| private static bool IsAsyncHelpersAwait(MethodDesc method) | ||
| { | ||
| if (method.IsIntrinsic && method.Name.SequenceEqual("Await"u8)) | ||
| { | ||
| MetadataType owningType = method.OwningType as MetadataType; | ||
| if (owningType != null) | ||
| { | ||
| return owningType.Module == method.Context.SystemModule | ||
| && owningType.Name.SequenceEqual("AsyncHelpers"u8) | ||
| && owningType.Namespace.SequenceEqual("System.Runtime.CompilerServices"u8); | ||
| } | ||
| } | ||
|
|
||
| return false; | ||
| } | ||
|
|
||
| private static bool IsTaskConfigureAwait(MethodDesc method) | ||
| { | ||
| if (method.IsIntrinsic && method.Name.SequenceEqual("ConfigureAwait"u8)) | ||
| { | ||
| MetadataType owningType = method.OwningType as MetadataType; | ||
| if (owningType != null) | ||
| { | ||
| ReadOnlySpan<byte> typeName = owningType.Name; | ||
| return owningType.Module == method.Context.SystemModule | ||
| && owningType.Namespace.SequenceEqual("System.Threading.Tasks"u8) | ||
| && (typeName.SequenceEqual("Task"u8) | ||
| || typeName.SequenceEqual("Task`1"u8) | ||
| || typeName.SequenceEqual("ValueTask"u8) | ||
| || typeName.SequenceEqual("ValueTask`1"u8)); | ||
| } | ||
| } | ||
|
|
||
| return false; | ||
| } | ||
|
|
||
| private DefType GetWellKnownType(WellKnownType wellKnownType) | ||
| { | ||
| return _compilation.TypeSystemContext.GetWellKnownType(wellKnownType); | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.