Remove DelegateInvokeClosedStaticThunk — use InitializeClosedInstance for closed static delegates#126353
Remove DelegateInvokeClosedStaticThunk — use InitializeClosedInstance for closed static delegates#126353
Conversation
…for closed static delegates Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/c9da8a69-d46b-4eef-a862-b5d11ef38256 Co-authored-by: MichalStrehovsky <13110571+MichalStrehovsky@users.noreply.github.com>
|
Tagging subscribers to this area: @agocke, @dotnet/ilc-contrib |
| // Do not change their values without updating the values in the calling convention converter component | ||
| private protected const int MulticastThunk = 0; | ||
| private protected const int ClosedStaticThunk = 1; | ||
| // Index 1 is reserved (was ClosedStaticThunk, now unused) |
There was a problem hiding this comment.
Would it be better to delete this instead?
There was a problem hiding this comment.
Yes, I think copilot was just holding on to the comments around this that warn against touching this.
This PR is just for a conversation I had with @MichalPetryka on Discord. He pointed out CoreCLR doesn't do a thunk for this and lives with the calling convention mismatch. So I'm just wondering if we can live with it too. Will see how the testing goes. Copilot might need to write extra tests too...
There was a problem hiding this comment.
Deleted the reserved-index comments from both Delegate.cs and DelegateInfo.cs in 446f57e.
|
@copilot a test is crashing, apparently ARM64 only: |
…losed static delegates to generic methods Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/95e30f9e-b7c0-4dd8-91e8-0c297db894af Co-authored-by: MichalStrehovsky <13110571+MichalStrehovsky@users.noreply.github.com>
... The crash was because the closed-static path unconditionally used |
|
@copilot the PR description says "At the ABI level, |
No, the claim is not true when there's a return buffer on x64. On x64 (Windows and SysV),
The delegate Invoke body ( The old On ARM64 this isn't an issue because CoreCLR handles the same problem with So the optimization is valid for the no-return-buffer case and for ARM64, but we'd need special handling for static methods with return buffers on x64. Options would be to keep the thunk only for that case, or implement an equivalent of CoreCLR's |
|
Yeah, the point of this would be to remove complexity for an edge case delegate scenario, not add more. Closing. |
Closed delegates to static methods (e.g. extension method with a bound first parameter) unnecessarily used a dedicated
DelegateInvokeClosedStaticThunk+InitializeClosedStaticThunk. At the ABI level,staticMethod(capturedObj, arg1, arg2)andinstanceMethod(this=capturedObj, arg1, arg2)have identical register layouts, so the thunk indirection is wasted work — one extra generated method per delegate type.Description
Delegate.cs— RemoveInitializeClosedStaticThunk, removeClosedStaticThunk = 1constant, drop stale thunk check fromTarget, update the reflectionCreateDelegatehelper to useInitializeClosedInstanceWithoutNullCheck(handles fat/generic function pointers correctly).DelegateCreationInfo.cs— Restructure the static branch so the closed-static path callsInitializeClosedInstancewithThunk = null, exactly mirroring how closed instance delegates are constructed. For generic static methods wheretargetMethod != targetCanonMethod, useInitializeClosedInstanceSlowto correctly handle fat function pointers (function pointer + instantiation argument in a single pointer). Open-static path is unchanged.DelegateThunks.cs— DeleteDelegateInvokeClosedStaticThunkclass entirely.DelegateThunks.Sorting.cs— Delete corresponding sorting partial.DelegateInfo.cs— Remove_closedStaticThunkfield, its construction, its switch arm, andClosedStaticThunk = 1fromDelegateThunkKind. TheGetThunkswitch table retains 6 slots (0–5); slot 1 now falls through toreturnNullLabel/IntPtr.Zero.Note
AI-generated code changes.
Original prompt
Summary
Closed delegates to static methods (e.g. a delegate to a C# extension method with a pre-populated first parameter) currently use a dedicated
DelegateInvokeClosedStaticThunkandInitializeClosedStaticThunk. This is unnecessary — at the ABI level,staticMethod(capturedObject, arg1, arg2)andinstanceMethod(this=capturedObject, arg1, arg2)have identical register/stack layouts, so we can useInitializeClosedInstance/InitializeClosedInstanceSlowinstead, just like closed delegates to instance methods. This eliminates one thunk per delegate type and simplifies the delegate infrastructure.Changes required
1.
src/coreclr/nativeaot/System.Private.CoreLib/src/System/Delegate.csInitializeClosedStaticThunkmethod entirely (lines ~200-207).ClosedStaticThunk = 1constant. Since thunk kind values are used as indices into a switch table in theGetThunkoverride, we cannot renumber the remaining values. Instead, keep the constant value1as unused/reserved. The simplest approach: remove theClosedStaticThunkconstant and leave a comment that index 1 is unused/reserved, OR just remove it entirely since nothing should reference it anymore.CallConverterThunk, and we're removing all usages, we can remove the constant.Targetproperty: Remove the_functionPointer == GetThunk(ClosedStaticThunk)check. After the change, closed static delegates will use_firstParameterto hold the captured object (just like closed instance delegates), soTargetwill naturally return_firstParametervia the final fallthrough path.Delegate.CreateDelegate(MethodTable*, IntPtr, object, bool, bool)(the reflection helper): TheisStatic && !isOpenbranch currently callsGetThunk(Delegate.ClosedStaticThunk)+InitializeClosedStaticThunk. Change it to callInitializeClosedInstanceWithoutNullCheck(thisObject, ldftnResult)instead (same as the!isStatic && !isOpenbranch). This is correct becauseInitializeClosedInstanceWithoutNullCheckhandles both normal and generic (fat) function pointers.2.
src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DelegateCreationInfo.csIn
DelegateCreationInfo.Create, theclosed && targetMethod.Signature.IsStaticbranch currently does:Change this to use
InitializeClosedInstance(no thunk needed):And remove the
invokeThunk/Thunkfor this path — the delegate will be constructed without a thunk, same as a normal closed instance delegate. TheDelegateCreationInforeturned should haveThunk = nullfor this case.Important: The closed-static path currently always passes a thunk. After the change, there is no thunk. Make sure the
DelegateCreationInfoconstructor and the code that consumes it (inDelegateConstructorExpansionor similar) can handle a nullThunkfor static methods. Look at how the closed-instance path works — it setsThunkto null and the init method takes only 2 parameters (object + IntPtr).InitializeClosedInstancealso takes 2 parameters. So the static-closed path should be restructured to match the instance-closed path.3.
src/coreclr/tools/Common/TypeSystem/IL/Stubs/DelegateThunks.csDelegateInvokeClosedStaticThunkclass (thesealed partial class DelegateInvokeClosedStaticThunk : DelegateThunkwith itsEmitIL()andNameproperty).4.
src/coreclr/tools/Common/TypeSystem/IL/Stubs/DelegateThunks.Sorting.csDelegateInvokeClosedStaticThunkpartial class withClassCode => 28195375.5.
src/coreclr/tools/Common/TypeSystem/IL/DelegateInfo.cs_closedStaticThunkfield fromDelegateThunkCollection._closedStaticThunk = new DelegateInvokeClosedStaticThunk(owningDelegate)line from the constructor.case DelegateThunkKind.ClosedStaticThunk:should returnnull.ClosedStaticThunk = 1from theDelegateThunkKindenum. Since the values are used as switch indices in the generatedGetThunkoverride, we should keep the numbering stable. Either: (a) keep the enum value but always return null for it, or (b) remove it entirely if no code references it anymore. SinceDelegateCreationInfowill no longer referenceDelegateThunkKind.ClosedStaticThunk, we can remove the enum value entirely. But theGetThunkMethodOverride.EmitIL()generates a switch based onDelegateThunkCollection.MaxThunkKind, so removing a value in the middle is fine as long as the switch handle...This pull request was created from Copilot chat.