diff --git a/src/coreclr/jit/importer.cpp b/src/coreclr/jit/importer.cpp index 1b811f91084775..4ad152303a3975 100644 --- a/src/coreclr/jit/importer.cpp +++ b/src/coreclr/jit/importer.cpp @@ -4602,7 +4602,8 @@ bool Compiler::impIsImplicitTailCallCandidate( #endif // !FEATURE_TAILCALL_OPT_SHARED_RETURN // must be call+ret or call+pop+ret - if (!impIsTailCallILPattern(false, opcode, codeAddrOfNextOpcode, codeEnd, isRecursive)) + if (!impIsTailCallILPattern(false, opcode, codeAddrOfNextOpcode, codeEnd, isRecursive) && + ((prefixFlags & PREFIX_IS_ASYNC_VERSION_TAIL_AWAIT) == 0)) { return false; } @@ -9331,6 +9332,7 @@ void Compiler::impImportBlockCode(BasicBlock* block) impAppendTree(gtUnusedValNode(val), CHECK_SPILL_ALL, impCurStmtDI); } + prefixFlags &= ~PREFIX_TAILCALL; goto RET; } @@ -11784,6 +11786,16 @@ bool Compiler::impWrapTopOfStackInAwait() if (impInlineRoot()->compIsAsyncVersion()) { asyncInfo->IsTailAwait = !compIsForInlining() || impInlineInfo->iciCall->GetAsyncInfo().IsTailAwait; + +#if FEATURE_TAILCALL_OPT + // We intentionally do not consult with the EE and canTailCall because + // this is us introducing a call as an implementation detail and not a + // user-introduced call. + if (asyncInfo->IsTailAwait && opts.compTailCallOpt && opts.OptimizationEnabled()) + { + awaitCall->gtCallMoreFlags |= GTF_CALL_M_IMPLICIT_TAILCALL; + } +#endif } else { diff --git a/src/coreclr/jit/importercalls.cpp b/src/coreclr/jit/importercalls.cpp index c2700e9ff29237..9a801f46f10959 100644 --- a/src/coreclr/jit/importercalls.cpp +++ b/src/coreclr/jit/importercalls.cpp @@ -66,7 +66,6 @@ var_types Compiler::impImportCall(OPCODE opcode, // to see any imperative security. // Reverse P/Invokes need a call to CORINFO_HELP_JIT_REVERSE_PINVOKE_EXIT // at the end, so tailcalls should be disabled. - // Async methods need to restore contexts, so tailcalls should be disabled. if (info.compFlags & CORINFO_FLG_SYNCH) { canTailCall = false; @@ -77,11 +76,6 @@ var_types Compiler::impImportCall(OPCODE opcode, canTailCall = false; szCanTailCallFailReason = "Caller is Reverse P/Invoke"; } - else if (compIsAsync()) - { - canTailCall = false; - szCanTailCallFailReason = "Caller is async method"; - } #if !FEATURE_FIXED_OUT_ARGS else if (info.compIsVarArgs) { @@ -1141,6 +1135,18 @@ var_types Compiler::impImportCall(OPCODE opcode, BADCODE("Stack should be empty after tailcall"); } + // Async methods need to restore contexts, so in general tailcalls + // should be disabled. The exception is a tail await: for those the JIT + // directly returns the callee's continuation to the caller and no + // context needs to be restored, so the async call can be turned into a + // real tail call. Any other tail call candidate in an async method + // must be disqualified. + if (canTailCall && compIsAsync() && (!call->AsCall()->IsAsync() || !call->AsCall()->GetAsyncInfo().IsTailAwait)) + { + canTailCall = false; + szCanTailCallFailReason = "Caller is async method and call is not a tail await"; + } + // For opportunistic tailcalls we allow implicit widening, i.e. tailcalls from int32 -> int16, since the // managed calling convention dictates that the callee widens the value. For explicit tailcalls or async // functions we don't want to require this detail of the calling convention to bubble up to helper