Skip to content

Commit 0f21477

Browse files
authored
[interp] Implement basic tailcalls (dotnet#118901)
* Add and generate dedicated interpreter opcodes for tail calls * Implement regular and virtual tail calls from interpreted code to interpreted code * Other types of tail calls are not yet implemented
1 parent cda0bf4 commit 0f21477

File tree

4 files changed

+61
-17
lines changed

4 files changed

+61
-17
lines changed

src/coreclr/interpreter/compiler.cpp

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1241,7 +1241,7 @@ InterpMethod* InterpCompiler::CreateInterpMethod()
12411241

12421242
bool unmanagedCallersOnly = corJitFlags.IsSet(CORJIT_FLAGS::CORJIT_FLAG_REVERSE_PINVOKE);
12431243

1244-
InterpMethod *pMethod = new InterpMethod(m_methodHnd, m_totalVarsStackSize, pDataItems, initLocals, unmanagedCallersOnly);
1244+
InterpMethod *pMethod = new InterpMethod(m_methodHnd, m_ILLocalsOffset, m_totalVarsStackSize, pDataItems, initLocals, unmanagedCallersOnly);
12451245

12461246
return pMethod;
12471247
}
@@ -3082,7 +3082,7 @@ void InterpCompiler::EmitCall(CORINFO_RESOLVED_TOKEN* pConstrainedToken, bool re
30823082
}
30833083
else if (isCalli)
30843084
{
3085-
AddIns(INTOP_CALLI);
3085+
AddIns(tailcall ? INTOP_CALLI_TAIL : INTOP_CALLI);
30863086
m_pLastNewIns->data[0] = GetDataItemIndex(calliCookie);
30873087
m_pLastNewIns->SetSVars2(CALL_ARGS_SVAR, callIFunctionPointerVar);
30883088
}
@@ -3100,6 +3100,11 @@ void InterpCompiler::EmitCall(CORINFO_RESOLVED_TOKEN* pConstrainedToken, bool re
31003100
assert(!isPInvoke && !isMarshaledPInvoke);
31013101
AddIns(INTOP_CALLDELEGATE);
31023102
}
3103+
else if (tailcall)
3104+
{
3105+
assert(!isPInvoke && !isMarshaledPInvoke);
3106+
AddIns(INTOP_CALL_TAIL);
3107+
}
31033108
else
31043109
{
31053110
AddIns((isPInvoke && !isMarshaledPInvoke) ? INTOP_CALL_PINVOKE : INTOP_CALL);
@@ -3136,14 +3141,14 @@ void InterpCompiler::EmitCall(CORINFO_RESOLVED_TOKEN* pConstrainedToken, bool re
31363141

31373142
calliCookie = m_compHnd->GetCookieForInterpreterCalliSig(&callInfo.sig);
31383143

3139-
AddIns(INTOP_CALLI);
3144+
AddIns(tailcall ? INTOP_CALLI_TAIL : INTOP_CALLI);
31403145
m_pLastNewIns->data[0] = GetDataItemIndex(calliCookie);
31413146
m_pLastNewIns->SetSVars2(CALL_ARGS_SVAR, codePointerLookupResult);
31423147
break;
31433148
}
31443149
case CORINFO_VIRTUALCALL_VTABLE:
31453150
// Traditional virtual call. In theory we could optimize this to using the vtable
3146-
AddIns(INTOP_CALLVIRT);
3151+
AddIns(tailcall ? INTOP_CALLVIRT_TAIL : INTOP_CALLVIRT);
31473152
m_pLastNewIns->data[0] = GetDataItemIndex(callInfo.hMethod);
31483153
break;
31493154

@@ -3171,13 +3176,13 @@ void InterpCompiler::EmitCall(CORINFO_RESOLVED_TOKEN* pConstrainedToken, bool re
31713176

31723177
calliCookie = m_compHnd->GetCookieForInterpreterCalliSig(&callInfo.sig);
31733178

3174-
AddIns(INTOP_CALLI);
3179+
AddIns(tailcall ? INTOP_CALLI_TAIL : INTOP_CALLI);
31753180
m_pLastNewIns->data[0] = GetDataItemIndex(calliCookie);
31763181
m_pLastNewIns->SetSVars2(CALL_ARGS_SVAR, synthesizedLdvirtftnPtrVar);
31773182
}
31783183
else
31793184
{
3180-
AddIns(INTOP_CALLVIRT);
3185+
AddIns(tailcall ? INTOP_CALLVIRT_TAIL : INTOP_CALLVIRT);
31813186
m_pLastNewIns->data[0] = GetDataItemIndex(callInfo.hMethod);
31823187
}
31833188
break;

src/coreclr/interpreter/interpretershared.h

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,19 +30,20 @@ struct InterpMethod
3030
InterpMethod *self;
3131
#endif
3232
CORINFO_METHOD_HANDLE methodHnd;
33-
int32_t allocaSize;
33+
int32_t argsSize, allocaSize;
3434
void** pDataItems;
3535
// This stub is used for calling the interpreted method from JITted/AOTed code
3636
CallStubHeader *pCallStub;
3737
bool initLocals;
3838
bool unmanagedCallersOnly;
3939

40-
InterpMethod(CORINFO_METHOD_HANDLE methodHnd, int32_t allocaSize, void** pDataItems, bool initLocals, bool unmanagedCallersOnly)
40+
InterpMethod(CORINFO_METHOD_HANDLE methodHnd, int32_t argsSize, int32_t allocaSize, void** pDataItems, bool initLocals, bool unmanagedCallersOnly)
4141
{
4242
#if DEBUG
4343
this->self = this;
4444
#endif
4545
this->methodHnd = methodHnd;
46+
this->argsSize = argsSize;
4647
this->allocaSize = allocaSize;
4748
this->pDataItems = pDataItems;
4849
this->initLocals = initLocals;

src/coreclr/interpreter/intops.def

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,11 @@ OPDEF(INTOP_NEWOBJ, "newobj", 5, 1, 1, InterpOpMethodHandle)
365365
OPDEF(INTOP_NEWOBJ_GENERIC, "newobj.generic", 6, 1, 2, InterpOpMethodHandle)
366366
OPDEF(INTOP_NEWOBJ_VT, "newobj.vt", 5, 1, 1, InterpOpMethodHandle)
367367

368+
// Tail calls
369+
OPDEF(INTOP_CALL_TAIL, "call.tail", 4, 1, 1, InterpOpMethodHandle)
370+
OPDEF(INTOP_CALLI_TAIL, "calli", 5, 1, 2, InterpOpLdPtr)
371+
OPDEF(INTOP_CALLVIRT_TAIL, "callvirt.tail", 4, 1, 1, InterpOpMethodHandle)
372+
368373
// The following helper call instructions exist in 2 variants, one for normal methods, and one for cases where a shared generic lookup is needed.
369374
// In the case where a shared generic lookup is needed an extra argument is passed as an svar, which is a pointer to the generic context.
370375
// If there is a generic context argument it is always the first SVar to the instruction.

src/coreclr/vm/interpexec.cpp

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -543,6 +543,7 @@ void InterpExecMethod(InterpreterFrame *pInterpreterFrame, InterpMethodContextFr
543543
}
544544

545545
int32_t returnOffset, callArgsOffset, methodSlot;
546+
bool isTailcall = false;
546547
MethodDesc* targetMethod;
547548

548549
MAIN_LOOP:
@@ -1895,8 +1896,10 @@ void InterpExecMethod(InterpreterFrame *pInterpreterFrame, InterpMethodContextFr
18951896
break;
18961897
}
18971898

1899+
case INTOP_CALLVIRT_TAIL:
18981900
case INTOP_CALLVIRT:
18991901
{
1902+
isTailcall = (*ip == INTOP_CALLVIRT_TAIL);
19001903
returnOffset = ip[1];
19011904
callArgsOffset = ip[2];
19021905
methodSlot = ip[3];
@@ -1914,8 +1917,10 @@ void InterpExecMethod(InterpreterFrame *pInterpreterFrame, InterpMethodContextFr
19141917
goto CALL_INTERP_METHOD;
19151918
}
19161919

1920+
case INTOP_CALLI_TAIL:
19171921
case INTOP_CALLI:
19181922
{
1923+
isTailcall = (*ip == INTOP_CALLI_TAIL);
19191924
returnOffset = ip[1];
19201925
callArgsOffset = ip[2];
19211926
int32_t calliFunctionPointerVar = ip[3];
@@ -1927,6 +1932,7 @@ void InterpExecMethod(InterpreterFrame *pInterpreterFrame, InterpMethodContextFr
19271932
// Save current execution state for when we return from called method
19281933
pFrame->ip = ip;
19291934

1935+
// Interpreter-FIXME: isTailcall
19301936
InvokeCalliStub(LOCAL_VAR(calliFunctionPointerVar, PCODE), pCallStub, stack + callArgsOffset, stack + returnOffset);
19311937
break;
19321938
}
@@ -1936,6 +1942,7 @@ void InterpExecMethod(InterpreterFrame *pInterpreterFrame, InterpMethodContextFr
19361942
// This opcode handles p/invokes that don't use a managed wrapper for marshaling. These
19371943
// calls are special in that they need an InlinedCallFrame in order for proper EH to happen
19381944

1945+
isTailcall = false;
19391946
returnOffset = ip[1];
19401947
callArgsOffset = ip[2];
19411948
methodSlot = ip[3];
@@ -1971,6 +1978,7 @@ void InterpExecMethod(InterpreterFrame *pInterpreterFrame, InterpMethodContextFr
19711978

19721979
case INTOP_CALLDELEGATE:
19731980
{
1981+
isTailcall = false;
19741982
returnOffset = ip[1];
19751983
callArgsOffset = ip[2];
19761984
methodSlot = ip[3];
@@ -1996,8 +2004,10 @@ void InterpExecMethod(InterpreterFrame *pInterpreterFrame, InterpMethodContextFr
19962004
break;
19972005
}
19982006

2007+
case INTOP_CALL_TAIL:
19992008
case INTOP_CALL:
20002009
{
2010+
isTailcall = (*ip == INTOP_CALL_TAIL);
20012011
returnOffset = ip[1];
20022012
callArgsOffset = ip[2];
20032013
methodSlot = ip[3];
@@ -2032,24 +2042,44 @@ void InterpExecMethod(InterpreterFrame *pInterpreterFrame, InterpMethodContextFr
20322042
if (targetIp == NULL)
20332043
{
20342044
// If we didn't get the interpreter code pointer setup, then this is a method we need to invoke as a compiled method.
2045+
// Interpreter-FIXME: Implement tailcall via helpers, see https://github.com/dotnet/runtime/blob/main/docs/design/features/tailcalls-with-helpers.md
20352046
InvokeCompiledMethod(targetMethod, stack + callArgsOffset, stack + returnOffset, targetMethod->GetMultiCallableAddrOfCode(CORINFO_ACCESS_ANY));
20362047
break;
20372048
}
20382049
}
20392050

2040-
// Allocate child frame.
2051+
if (isTailcall)
20412052
{
2042-
InterpMethodContextFrame *pChildFrame = pFrame->pNext;
2043-
if (!pChildFrame)
2053+
// Move args from callArgsOffset to start of stack frame.
2054+
InterpMethod* pTargetMethod = targetIp->Method;
2055+
assert(pTargetMethod->CheckIntegrity());
2056+
// It is safe to use memcpy because the source and destination are both on the interp stack, not in the GC heap.
2057+
// We need to use the target method's argsSize, not our argsSize, because tail calls (unlike CEE_JMP) can have a
2058+
// different signature from the caller.
2059+
memcpy(pFrame->pStack, stack + callArgsOffset, pTargetMethod->argsSize);
2060+
// Reuse current stack frame. We discard the call insn's returnOffset because it's not important and tail calls are
2061+
// required to be followed by a ret, so we know nothing is going to read from stack[returnOffset] after the call.
2062+
pFrame->ReInit(pFrame->pParent, targetIp, pFrame->pRetVal, pFrame->pStack);
2063+
}
2064+
else
2065+
{
2066+
// Save current execution state for when we return from called method
2067+
pFrame->ip = ip;
2068+
2069+
// Allocate child frame.
20442070
{
2045-
pChildFrame = (InterpMethodContextFrame*)alloca(sizeof(InterpMethodContextFrame));
2046-
pChildFrame->pNext = NULL;
2047-
pFrame->pNext = pChildFrame;
2071+
InterpMethodContextFrame *pChildFrame = pFrame->pNext;
2072+
if (!pChildFrame)
2073+
{
2074+
pChildFrame = (InterpMethodContextFrame*)alloca(sizeof(InterpMethodContextFrame));
2075+
pChildFrame->pNext = NULL;
2076+
pFrame->pNext = pChildFrame;
2077+
}
2078+
pChildFrame->ReInit(pFrame, targetIp, stack + returnOffset, stack + callArgsOffset);
2079+
pFrame = pChildFrame;
20482080
}
2049-
pChildFrame->ReInit(pFrame, targetIp, stack + returnOffset, stack + callArgsOffset);
2050-
pFrame = pChildFrame;
2081+
assert (((size_t)pFrame->pStack % INTERP_STACK_ALIGNMENT) == 0);
20512082
}
2052-
assert (((size_t)pFrame->pStack % INTERP_STACK_ALIGNMENT) == 0);
20532083

20542084
// Set execution state for the new frame
20552085
pMethod = pFrame->startIp->Method;
@@ -2061,6 +2091,7 @@ void InterpExecMethod(InterpreterFrame *pInterpreterFrame, InterpMethodContextFr
20612091
}
20622092
case INTOP_NEWOBJ_GENERIC:
20632093
{
2094+
isTailcall = false;
20642095
returnOffset = ip[1];
20652096
callArgsOffset = ip[2];
20662097
methodSlot = ip[4];
@@ -2079,6 +2110,7 @@ void InterpExecMethod(InterpreterFrame *pInterpreterFrame, InterpMethodContextFr
20792110
}
20802111
case INTOP_NEWOBJ:
20812112
{
2113+
isTailcall = false;
20822114
returnOffset = ip[1];
20832115
callArgsOffset = ip[2];
20842116
methodSlot = ip[3];
@@ -2110,6 +2142,7 @@ void InterpExecMethod(InterpreterFrame *pInterpreterFrame, InterpMethodContextFr
21102142
}
21112143
case INTOP_NEWOBJ_VT:
21122144
{
2145+
isTailcall = false;
21132146
returnOffset = ip[1];
21142147
callArgsOffset = ip[2];
21152148
methodSlot = ip[3];

0 commit comments

Comments
 (0)