Skip to content

Optimize and simplify delegate layout#99200

Open
MichalPetryka wants to merge 99 commits into
dotnet:mainfrom
MichalPetryka:immutable-delegates
Open

Optimize and simplify delegate layout#99200
MichalPetryka wants to merge 99 commits into
dotnet:mainfrom
MichalPetryka:immutable-delegates

Conversation

@MichalPetryka

@MichalPetryka MichalPetryka commented Mar 3, 2024

Copy link
Copy Markdown
Contributor

First attempt at making delegate GC fields immutable in CoreCLR so that they can be allocated on the NonGC heap.

I've checked it with a simple app and corerun locally with a delegate from an unloadable ALC and it seemed to not crash, assert nor unload the ALC from under the delegate, however I couldn't actually find any runtime tests that would verify delegates from unloadable ALCs work so the CI coverage might be missing.

One small point of concern is that this might make delegate equality checks slower since they rely on checking the methods in the last "slow path" check, which is however always hit for different delegates AFAIR.

Contributes to #85014.

cc @jkotas

@ghost ghost added the area-VM-coreclr label Mar 3, 2024
@MichalPetryka

MichalPetryka commented Mar 4, 2024

Copy link
Copy Markdown
Contributor Author

I'm not sure what's up with the failures here, tests that are failing on the CI seem to pass on my machine.
EDIT: I was testing with R2R disabled locally since VS kept complaining about being unable to load PDBs for it.

Comment thread src/coreclr/vm/object.cpp Outdated
MichalPetryka and others added 3 commits March 4, 2024 16:40
Co-authored-by: Jan Kotas <jkotas@microsoft.com>
@MichalPetryka MichalPetryka marked this pull request as ready for review March 4, 2024 23:28
@AndyAyersMS

Copy link
Copy Markdown
Member

/azp run runtime-coreclr gcstress0x3-gcstress0xc

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@jkotas

jkotas commented Mar 5, 2024

Copy link
Copy Markdown
Member

Could you please collect some perf numbers to give us an idea about the improvements and regressions in the affected areas? We may want to do some optimizations to mitigate the regressions.

@jkotas

jkotas commented Mar 5, 2024

Copy link
Copy Markdown
Member

One small point of concern is that this might make delegate equality checks slower since they rely on checking the methods in the last "slow path" check, which is however always hit for different delegates AFAIR.

The existing code tries to compare MethodInfos as a cheap fast path. Most delegates do not have cached MethodInfo, so this fast path is hit rarely - but it is very cheap, so it is still worth it.

This cheap fast path is not cheap anymore with this change. It may be best to delete the fast path that is trying to compare the MethodInfos and potentially optimize Delegate_InternalEqualMethodHandles instead.

@MichalPetryka

MichalPetryka commented Mar 5, 2024

Copy link
Copy Markdown
Contributor Author

Could you please collect some perf numbers to give us an idea about the improvements and regressions in the affected areas? We may want to do some optimizations to mitigate the regressions.

I think the thing that'd need benchmarking here are equality checks and maybe the impact of collectible delegates being stored in the CWT on the GC, the rest of things shouldn't be performance sensitive enough to matter I think? I'm not fully sure what'd be the proper way for benchmarking the latter.

This cheap fast path is not cheap anymore with this change. It may be best to delete the fast path that is trying to compare the MethodInfos and potentially optimize Delegate_InternalEqualMethodHandles instead.

I am going to benchmark the impact of the equality change tomorrow, I feel like if the impact won't be big, potential optimizations to it can be done later.

@jkotas

jkotas commented Mar 5, 2024

Copy link
Copy Markdown
Member

I'm not fully sure what'd be the proper way for benchmarking the latter.

Write a small program that loads an assembly as collectible and calls a method in it. The method in collectible assembly can create delegates in a loop. (If you would like to do it under benchmarknet, it works too - but it is probably more work.)

Comment thread src/coreclr/System.Private.CoreLib/src/System/Delegate.CoreCLR.cs Outdated
Comment thread src/coreclr/System.Private.CoreLib/src/System/Delegate.CoreCLR.cs
Comment thread src/coreclr/vm/comdelegate.cpp Outdated
Moved UnmanagedMarker constant declaration to the top of the struct.
Comment thread src/coreclr/System.Private.CoreLib/src/System/Delegate.CoreCLR.cs Outdated
Simplified GetMethodDesc by removing unnecessary variable and conditional.
Comment thread src/coreclr/vm/comdelegate.cpp Outdated
static MethodDesc* GetMethodDescForOpenVirtualDelegate(OBJECTREF orDelegate);
static BOOL IsTrueMulticastDelegate(OBJECTREF delegate);
static MethodDesc* GetMethodDesc(OBJECTREF obj);
static MethodDesc* GetCachedMethodDesc(DELEGATEREF delegate);

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.

Suggested change
static MethodDesc* GetCachedMethodDesc(DELEGATEREF delegate);
static MethodDesc* GetMethodDescForOpenVirtualDelegate(DELEGATEREF delegate);

If you apply my other feedback, this can stay with the more specific name

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.

I don't think the old name makes sense anymore since it will return results for non open virtual delegates.

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.

The implementation can assert that it is only used for open virtual delegates.

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.

The implementation can assert that it is only used for open virtual delegates.

I don't think there's an easy way to do that anymore? We'd need to check if the method is virtual and that the delegate is open which need a lot of checks.

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.

Right, we would have to assert that the aux pointer points into VSD heap. I am not sure how hard is it to put the assert together. Asserting is optional to enforce the precondition. We have many methods in the VM that document the preconditions without asserting them.

It is better to name cross-component calls based on their external contract, not based on the implementation details. It makes the code easier to understand, and it makes refactoring easier. For example, GetMethodDescForOpenVirtualDelegate name is likely to survive changes in implementation details. GetCachedMethodDesc name not so much.

Co-authored-by: Jan Kotas <jkotas@microsoft.com>
@jkotas

jkotas commented Jul 11, 2026

Copy link
Copy Markdown
Member
/__w/1/s/src/coreclr/System.Private.CoreLib/src/System/Delegate.CoreCLR.cs(336,25): error CS0128: A local variable or function named 'method' is already defined in this scope [/__w/1/s/src/coreclr/System.Private.CoreLib/System.Private.CoreLib.csproj]
/__w/1/s/src/coreclr/System.Private.CoreLib/src/System/Delegate.CoreCLR.cs(340,20): error CS0266: Cannot implicitly convert type 'System.IRuntimeMethodInfo' to 'System.Reflection.MethodInfo'. An explicit conversion exists (are you missing a cast?) [/__w/1/s/src/coreclr/System.Private.CoreLib/System.Private.CoreLib.csproj]
/__w/1/s/src/coreclr/System.Private.CoreLib/src/System/Delegate.CoreCLR.cs(336,25): error IDE0059: Unnecessary assignment of a value to 'method' (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0059) [/__w/1/s/src/coreclr/System.Private.CoreLib/System.Private.CoreLib.csproj]

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 22 out of 22 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (1)

src/native/managed/cdac/tests/UnitTests/ObjectTests.cs:450

  • GetDelegateInfo now has explicit multicast detection via Delegate.HelperObject being an array, but the unit tests no longer exercise that path (the previous "Multicast" test was repurposed to "Unmanaged"). Please add a multicast test that sets HelperObject to an array object and verifies DelegateType.Unknown (and zeroed Target/MethodPtr) for multicast delegates.
    [Theory]
    [ClassData(typeof(MockTarget.StdArch))]
    public void GetDelegateInfo_Unmanaged(MockTarget.Architecture arch)
    {
        const ulong TestMethodTable = 0x00000000_10000200;
        const ulong TestMethodPtr = 0x00000000_aaaa0000;
        const long TestExtraData = -1;
        TargetPointer delegateAddress = default;

        IObject contract = CreateObjectContract(
            arch,
            objectBuilder =>
            {
                delegateAddress = objectBuilder.AddDelegateObject(
                    methodTable: TestMethodTable,
                    target: 0,
                    methodPtr: TestMethodPtr,
                    methodPtrAux: 0,
                    extraData: TestExtraData);
            });

        DelegateInfo info = contract.GetDelegateInfo(delegateAddress);

        Assert.Equal(DelegateType.Unknown, info.DelegateType);
        Assert.Equal(0ul, info.TargetObject.Value);
        Assert.Equal(0ul, info.TargetMethodPtr.Value);
    }

Comment on lines +229 to +231
// both delegates are open
if (_methodPtrAux == other._methodPtrAux)
return true;

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.

@jkotas was this a preexisting bug?

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.

Yes, it looks like it.

I think it is only a problem for FEATURE_CACHED_INTERFACE_DISPATCH that is only used in production on iOS.

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.

We should add a test that hits the issue before fixing it.

Comment on lines 1217 to +1225
// A null invocationList can be one of the following:
// - Instance closed, Instance open non-virt, Instance open virtual, Static closed, Static opened, Unmanaged FtnPtr
// - Instance open virtual is complex and we need to figure out what to do (TODO).
// For the others the logic is the following:
// if _methodPtrAux is 0 the target is in _methodPtr, otherwise the taret is _methodPtrAux
// - Instance closed
// - Instance open non-virt
// - Instance open virtual
// - Static closed
// - Static opened
// - Instance open virtual
// - Unmanaged FtnPtr
// if _methodPtrAux is 0 the target is in _methodPtr, otherwise the target is _methodPtrAux
Comment on lines +92 to +96
Contract Constants:
| Name | Type | Purpose | Value |
| --- | --- | --- | --- |
| `UnmanagedMarker` | nint | Sentinel value for detecting unmanaged pointer delegates. | `-1` |


MethodTable* methodTable = RuntimeHelpers.GetMethodTable(this);
#if FEATURE_TYPEEQUIVALENCE
if (methodTable->HasTypeEquivalence)

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.

I have started PR to add a test that hits this path: #130542 . The test should fail against main now, and it should pass once this PR is merged.

@jkotas

jkotas commented Jul 12, 2026

Copy link
Copy Markdown
Member

LGTM once the remaining feedback is addressed

@MichalPetryka

Copy link
Copy Markdown
Contributor Author

LGTM once the remaining feedback is addressed

I'll get to it tomorrow

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants