Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 34 additions & 70 deletions src/coreclr/tools/Common/TypeSystem/IL/Stubs/DelegateThunks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -310,109 +310,73 @@ public override MethodIL EmitIL()
ILEmitter emitter = new ILEmitter();
ILCodeStream codeStream = emitter.NewCodeStream();

TypeDesc delegateWrapperType = ((MetadataType)SystemDelegateType).GetKnownNestedType("Wrapper"u8);
ArrayType invocationListArrayType = delegateWrapperType.MakeArrayType();

ILLocalVariable delegateArrayLocal = emitter.NewLocal(invocationListArrayType);
ILLocalVariable invocationCountLocal = emitter.NewLocal(Context.GetWellKnownType(WellKnownType.Int32));
ILLocalVariable iteratorLocal = emitter.NewLocal(Context.GetWellKnownType(WellKnownType.Int32));
ILLocalVariable startLocal = emitter.NewLocal(_delegateInfo.Type.MakeByRefType());
ILLocalVariable endLocal = emitter.NewLocal(_delegateInfo.Type.MakeByRefType());

ILLocalVariable returnValueLocal = 0;
if (!Signature.ReturnType.IsVoid)
{
returnValueLocal = emitter.NewLocal(Signature.ReturnType);
}

// Fill in delegateArrayLocal
// Delegate.Wrapper[] delegateArrayLocal = (Delegate.Wrapper[])this._helperObject

// ldarg.0 (this pointer)
// ldfld Delegate._helperObject
// castclass Delegate.Wrapper[] (omitted - generate unsafe cast assuming the delegate is well-formed)
// stloc delegateArrayLocal
// Load start and end refs
codeStream.EmitLdArg(0);
codeStream.Emit(ILOpcode.ldfld, emitter.NewToken(HelperObjectField));
// codeStream.Emit(ILOpcode.castclass, emitter.NewToken(invocationListArrayType));
codeStream.EmitStLoc(delegateArrayLocal);

// Fill in invocationCountLocal
// int invocationCountLocal = this._extraFunctionPointerOrData
// Get start ref
TypeDesc signatureVariable = Context.GetSignatureVariable(0, method: true);
MethodSignature signature = new(MethodSignatureFlags.Static, 1, signatureVariable.MakeByRefType(), [signatureVariable.MakeArrayType()]);
TypeDesc wrapper = ((MetadataType)SystemDelegateType).GetKnownNestedType("Wrapper"u8);
MethodDesc gadr = Context.GetCoreLibEntryPoint("System.Runtime.InteropServices"u8, "MemoryMarshal"u8, "GetArrayDataReference"u8, signature).MakeInstantiatedMethod(wrapper);
codeStream.Emit(ILOpcode.call, emitter.NewToken(gadr));
codeStream.EmitStLoc(startLocal);

// ldarg.0 (this pointer)
// ldfld Delegate._extraFunctionPointerOrData
// stloc invocationCountLocal
// Get end ref from adding count
codeStream.EmitLdLoc(startLocal);
codeStream.EmitLdArg(0);
codeStream.Emit(ILOpcode.ldfld, emitter.NewToken(ExtraFunctionPointerOrDataField));
codeStream.EmitStLoc(invocationCountLocal);

// Fill in iteratorLocal
// int iteratorLocal = 0;

// ldc.0
// stloc iteratorLocal
codeStream.EmitLdc(0);
codeStream.EmitStLoc(iteratorLocal);
codeStream.EmitLdc(Context.Target.PointerSize);
codeStream.Emit(ILOpcode.mul);
codeStream.Emit(ILOpcode.add);
codeStream.EmitStLoc(endLocal);

// Loop across every element of the array.
ILCodeLabel startOfLoopLabel = emitter.NewCodeLabel();
// Label_nextDelegate:
codeStream.EmitLabel(startOfLoopLabel);

// Implement as do/while loop. We only have this stub in play if we're in the multicast situation
// Find the delegate to call
// delegateArrayLocal[iteratorLocal].Value

// ldloc delegateArrayLocal
// ldloc iteratorLocal
// ldelema Delegate.Wrapper
// ldfld Delegate.Wrapper.Value
codeStream.EmitLdLoc(delegateArrayLocal);
codeStream.EmitLdLoc(iteratorLocal);
codeStream.Emit(ILOpcode.ldelema, emitter.NewToken(delegateWrapperType));
codeStream.Emit(ILOpcode.ldfld, emitter.NewToken(delegateWrapperType.GetKnownField("Value"u8)));

// Call the delegate
// delegateArrayLocal[iteratorLocal].Value(...)

// ldarg 1, n
// callvirt DelegateType.Invoke(...)
// IF there is a return value
// stloc returnValueLocal
// Load the delegate
codeStream.EmitLdLoc(startLocal);
codeStream.Emit(ILOpcode.ldind_ref);

// Load the arguments
for (int i = 0; i < Signature.Length; i++)
{
codeStream.EmitLdArg(i + 1);
}

codeStream.Emit(ILOpcode.callvirt, emitter.NewToken(_delegateInfo.InvokeMethod.InstantiateAsOpen()));
// Call the delegate
codeStream.Emit(ILOpcode.call, emitter.NewToken(_delegateInfo.InvokeMethod.InstantiateAsOpen()));

// Save return value.
if (returnValueLocal != 0)
codeStream.EmitStLoc(returnValueLocal);

// Increment iteratorLocal
// ++iteratorLocal;

// ldloc iteratorLocal
// ldc.i4.1
// add
// stloc iteratorLocal
codeStream.EmitLdLoc(iteratorLocal);
codeStream.EmitLdc(1);
// Increment the ref
codeStream.EmitLdLoc(startLocal);
codeStream.EmitLdc(Context.Target.PointerSize);
codeStream.Emit(ILOpcode.add);
codeStream.EmitStLoc(iteratorLocal);

// Check to see if the loop is done
codeStream.EmitLdLoc(invocationCountLocal);
codeStream.EmitLdLoc(iteratorLocal);
codeStream.Emit(ILOpcode.bne_un, startOfLoopLabel);
codeStream.EmitStLoc(startLocal);

// Return to caller. If the delegate has a return value, be certain to return that.
// return returnValueLocal;
// Compare start ref with end. If less than branch to nextDelegate
codeStream.EmitLdLoc(startLocal);
codeStream.EmitLdLoc(endLocal);
codeStream.Emit(ILOpcode.blt_un, startOfLoopLabel);

// ldloc returnValueLocal
// ret
// Load the return value, return value from the last delegate call is returned
if (returnValueLocal != 0)
codeStream.EmitLdLoc(returnValueLocal);

// Return
codeStream.Emit(ILOpcode.ret);

return emitter.Link(this);
Expand Down
102 changes: 62 additions & 40 deletions src/coreclr/vm/comdelegate.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2038,84 +2038,106 @@ extern "C" PCODE QCALLTYPE Delegate_GetMulticastInvokeSlow(MethodTable* pDelegat

ILCodeStream *pCode = sl.NewCodeStream(ILStubLinker::kDispatch);

DWORD dwLoopCounterNum = pCode->NewLocal(ELEMENT_TYPE_I4);
LocalDesc refLocalDesc(TypeHandle(pDelegateMT).MakeByRef());
DWORD dwStartNum = pCode->NewLocal(refLocalDesc);
DWORD dwEndNum = pCode->NewLocal(refLocalDesc);

DWORD dwReturnValNum = -1;
if (fReturnVal)
dwReturnValNum = pCode->NewLocal(sig.GetRetTypeHandleNT());

ILCodeLabel *nextDelegate = pCode->NewCodeLabel();
// Load start and end refs
pCode->EmitLoadThis();
pCode->EmitLDFLD(pCode->GetToken(CoreLibBinder::GetField(FIELD__MULTICAST_DELEGATE__INVOCATION_LIST)));

// initialize counter
pCode->EmitLDC(0);
pCode->EmitSTLOC(dwLoopCounterNum);
// Get start ref
MethodDesc* gadr = CoreLibBinder::GetMethod(METHOD__MEMORY_MARSHAL__GET_ARRAY_DATA_REFERENCE);
TypeHandle th(CoreLibBinder::GetClass(CLASS__OBJECT));
gadr = MethodDesc::FindOrCreateAssociatedMethodDesc(gadr, gadr->GetMethodTable(), FALSE, Instantiation(&th, 1), TRUE);

// Create signature for the MethodSpec. See ECMA-335 - II.23.2.15
SigBuilder sigBuilder;
sigBuilder.AppendByte(IMAGE_CEE_CS_CALLCONV_GENERICINST);
sigBuilder.AppendData(1);
sigBuilder.AppendElementType(ELEMENT_TYPE_MVAR);
sigBuilder.AppendData(0);
uint32_t sigLen;
PCCOR_SIGNATURE gadrSig = (PCCOR_SIGNATURE)sigBuilder.GetSignature((DWORD*)&sigLen);
mdToken methodSpecSigToken = pCode->GetSigToken(gadrSig, sigLen);

pCode->EmitCALL(pCode->GetToken(gadr, mdTokenNil, methodSpecSigToken), 1, 1);
pCode->EmitSTLOC(dwStartNum);

// Get end ref from adding count
pCode->EmitLDLOC(dwStartNum);
pCode->EmitLoadThis();
pCode->EmitLDFLD(pCode->GetToken(CoreLibBinder::GetField(FIELD__MULTICAST_DELEGATE__INVOCATION_COUNT)));
pCode->EmitLDC(TARGET_POINTER_SIZE);
pCode->EmitMUL();
pCode->EmitADD();
pCode->EmitSTLOC(dwEndNum);

//Label_nextDelegate:
ILCodeLabel *nextDelegate = pCode->NewCodeLabel();
// Label_nextDelegate:
pCode->EmitLabel(nextDelegate);

#ifdef DEBUGGING_SUPPORTED
ILCodeLabel *invokeTraceHelper = pCode->NewCodeLabel();
ILCodeLabel *debuggerCheckEnd = pCode->NewCodeLabel();

// Call MulticastDebuggerTraceHelper only if we have a controller subscribing to the event
pCode->EmitLDC((DWORD_PTR)&g_multicastDelegateTraceActiveCount);
pCode->EmitCONV_I();
pCode->EmitLDIND_I4();
// g_multicastDelegateTraceActiveCount != 0
// g_multicastDelegateTraceActiveCount == 0
pCode->EmitLDC(0);
pCode->EmitCEQ();
pCode->EmitBRFALSE(invokeTraceHelper);

pCode->EmitLabel(debuggerCheckEnd);
#endif // DEBUGGING_SUPPORTED

ILCodeLabel *realLoopStart = pCode->NewCodeLabel();
pCode->EmitBEQ(realLoopStart);

@huoyaoyuan huoyaoyuan Jul 5, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't look optimal for branch predicting (forward conditional jump as hot path). The original logic was constructed for optimizing this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't look optimal for branch predicting (forward conditional jump as hot path). The original logic was constructed for optimizing this.

The original IL was unusual and confusing the JIT causing even worse performance. Additionally, it was treated as hot there too. cc @tannergooding since we discussed this on Discord.

It seems the only way to solve this here would be an internal Assume.Likely/Unlikely.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like I suggested on discord, if we don't actually need multicast to lightup the debugger support at any point in the loop; then cloning is likely your best bet here.

You can rewrite the IL to be better and not the asm default to likely and still be understandable as normal control flow, but its quite a bit harder.

I'm not sure the extra churn is worth it though; the current stuff seems plenty good enough.


// Load next delegate from array using LoopCounter as index
// Call debugger tracing
pCode->EmitLoadThis();
pCode->EmitLDLOC(dwStartNum);
pCode->EmitLoadThis();
pCode->EmitLDFLD(pCode->GetToken(CoreLibBinder::GetField(FIELD__MULTICAST_DELEGATE__INVOCATION_LIST)));
pCode->EmitLDLOC(dwLoopCounterNum);
pCode->EmitLDELEM_REF();
pCode->EmitCALL(pCode->GetToken(gadr, mdTokenNil, methodSpecSigToken), 1, 1);
pCode->EmitSUB();
pCode->EmitCALL(METHOD__STUBHELPERS__MULTICAST_DEBUGGER_TRACE_HELPER, 2, 0);

// Label_realLoopStart:
pCode->EmitLabel(realLoopStart);
#endif // DEBUGGING_SUPPORTED

// Load the delegate
pCode->EmitLDLOC(dwStartNum);
pCode->EmitLDIND_REF();

// Load the arguments
for (UINT paramCount = 0; paramCount < sig.NumFixedArgs(); paramCount++)
pCode->EmitLDARG(paramCount);

// call the delegate
// Call the delegate
pCode->EmitCALL(pCode->GetToken(pMD), sig.NumFixedArgs(), fReturnVal);

// Save return value.
if (fReturnVal)
pCode->EmitSTLOC(dwReturnValNum);

// increment counter
pCode->EmitLDLOC(dwLoopCounterNum);
pCode->EmitLDC(1);
// Increment the ref
pCode->EmitLDLOC(dwStartNum);
pCode->EmitLDC(TARGET_POINTER_SIZE);
pCode->EmitADD();
pCode->EmitSTLOC(dwLoopCounterNum);
pCode->EmitSTLOC(dwStartNum);

// compare LoopCounter with InvocationCount. If less then branch to nextDelegate
pCode->EmitLDLOC(dwLoopCounterNum);
pCode->EmitLoadThis();
pCode->EmitLDFLD(pCode->GetToken(CoreLibBinder::GetField(FIELD__MULTICAST_DELEGATE__INVOCATION_COUNT)));
// Compare start ref with end. If less than branch to nextDelegate
pCode->EmitLDLOC(dwStartNum);
pCode->EmitLDLOC(dwEndNum);
pCode->EmitBLT(nextDelegate);

// load the return value. return value from the last delegate call is returned
// Load the return value, return value from the last delegate call is returned
if (fReturnVal)
pCode->EmitLDLOC(dwReturnValNum);

// return
// Return
pCode->EmitRET();

#ifdef DEBUGGING_SUPPORTED
// Emit debugging support at the end of the method for better perf
pCode->EmitLabel(invokeTraceHelper);

pCode->EmitLoadThis();
pCode->EmitLDLOC(dwLoopCounterNum);
pCode->EmitCALL(METHOD__STUBHELPERS__MULTICAST_DEBUGGER_TRACE_HELPER, 2, 0);

pCode->EmitBR(debuggerCheckEnd);
#endif // DEBUGGING_SUPPORTED

PCCOR_SIGNATURE pSig;
DWORD cbSig;
pMD->GetSig(&pSig, &cbSig);
Expand Down
1 change: 1 addition & 0 deletions src/coreclr/vm/corelib.h
Original file line number Diff line number Diff line change
Expand Up @@ -821,6 +821,7 @@ DEFINE_METHOD(UNSAFE, UNBOX, Unbox, NoSig)
DEFINE_METHOD(UNSAFE, WRITE, Write, NoSig)

DEFINE_CLASS(MEMORY_MARSHAL, Interop, MemoryMarshal)
DEFINE_METHOD(MEMORY_MARSHAL, GET_ARRAY_DATA_REFERENCE, GetArrayDataReference, GM_ArrT_RetRefT)
DEFINE_METHOD(MEMORY_MARSHAL, GET_ARRAY_DATA_REFERENCE_MDARRAY, GetArrayDataReference, SM_Array_RetRefByte)

DEFINE_CLASS(INTERLOCKED, Threading, Interlocked)
Expand Down
1 change: 1 addition & 0 deletions src/coreclr/vm/metasig.h
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,7 @@ DEFINE_METASIG(GM(RefByte_T_RetVoid, IMAGE_CEE_CS_CALLCONV_DEFAULT, 1, r(b) M(0)
DEFINE_METASIG(GM(PtrVoid_RetT, IMAGE_CEE_CS_CALLCONV_DEFAULT, 1, P(v), M(0)))
DEFINE_METASIG(GM(PtrVoid_T_RetVoid, IMAGE_CEE_CS_CALLCONV_DEFAULT, 1, P(v) M(0), v))

DEFINE_METASIG(GM(ArrT_RetRefT, IMAGE_CEE_CS_CALLCONV_DEFAULT, 1, a(M(0)), r(M(0))))
DEFINE_METASIG(GM(RefT_RetRefT, IMAGE_CEE_CS_CALLCONV_DEFAULT, 1, r(M(0)), r(M(0))))
DEFINE_METASIG(GM(RefTFrom_RetRefTTo, IMAGE_CEE_CS_CALLCONV_DEFAULT, 2, r(M(0)), r(M(1))))
DEFINE_METASIG(GM(Obj_RetT, IMAGE_CEE_CS_CALLCONV_DEFAULT, 1, j, M(0)))
Expand Down
Loading