dotnet / runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.
https://docs.microsoft.com/dotnet/core/
MIT License
15.2k stars 4.72k forks source link

Get InvocationList of MulticastDelegate without any allocation #41849

Closed RamType0 closed 9 months ago

RamType0 commented 4 years ago

Background and Motivation

Currently,we have an API to invocation list of MulticastDelegate,it is MulticastDelegate.GetInvocationList().

It allocates array every time we call it. And maybe that's why we have internal HasSingleTarget property in it.

We need handsome,and performant API.

Proposed API

public class Delegate
{
    // Existing API
    // public virtual System.Delegate[] GetInvocationList();

    // Returns whether the delegate has a single target. It is a property to highlight that it is guaranteed 
    // to be a very cheap operation that is important for set of scenarios addressed by this proposal.
    // Alternative names: HasSingleTarget, IsTrueMulticast (inverted condition), HasSingleElementInvocationList
    public bool IsSingle { get; }

    // Returns length of the invocation list. It is a method to highlight that it may not be a very cheap operation.
    // This method is optional part of the proposal for completeness. It is fine to omit it from the approved shape.
    public int GetInvocationListLength();

    // Returns invocation list enumerator
    public static InvocationListEnumerator<TDelegate> EnumerateInvocationList<TDelegate>(TDelegate d) where TDelegate : Delegate;

    // The shape of this enumerator matches existing StringBuilder.ChunkEnumerator and Activity.Enumerator
    // Neither of these existing enumerators implement IEnumerable and IEnumerator. This can be changed. 
    // Q: How are we deciding whether enumerators like this should implement IEnumerable and IEnumerator?
    // Note that implementing these interfaces makes each instantiation of the type more expensive, so it is a tradeoff.
    public struct InvocationListEnumerator<TDelegate> where TDelegate : Delegate
    {
        public TDelegate Current { get; }
        public bool MoveNext();

        // EditorBrowsable to match existing StringBuilder.ChunkEnumerator and Activity.Enumerator
        [EditorBrowsable(EditorBrowsableState.Never)] // Only here to make foreach work
        public InvocationListEnumerator<TDelegate> GetEnumerator() => this;
    }
}

Original rejected proposal

public ReadOnlySpan<Delegate> InvocationList => delegates == null ? MemoryMarshal.CreateSpan(this,1) : delegates;
jkotas commented 10 months ago

However, if there is a value with exposing a property or method for GetInvocationList().Length that would be fine

@terrajobst What would be a good API name for that?

As @weltkante pointed out, special casing the single-cast delegate fast path is fairly common. It would be nice to introduce proper API to replace the solutions based on private reflection:

terrajobst commented 10 months ago

@jkotas

@terrajobst What would be a good API name for that?

The objection to exposing the proposed properties was raised by @GrabYourPitchforks, so I let him speak to the details here.

My understanding was that InvocationCount would return the number of elements the iterator would produce (equivalent to GetInvocationList().Length) and that the Boolean IsSingleInvocation would be equivalent to InvocationCount == 0, so those names seemed reasonable.

In order for me to propose better names is dependent upon understanding the desired semantics.

jkotas commented 10 months ago

My understanding was that InvocationCount would return the number of elements the iterator would produce

Yes, that's correct.

Exposing InvocationCount with assumption that it is a fast O(1) operation locks us into the current internal delegate implementation. This assumption would be broken if we were to change the internal delegate implementation to a different data structure that does not allow fast O(1) length operation. (The internal data structure used by delegates was changed at least once in .NET runtime history.) This lock-in is not a showstopper, I am just pointing out the downside of exposing full InvocationCount when the API use cases only need to check for == 1.

terrajobst commented 10 months ago

Marking the API as I forgot to do that in the meeting 🤦

halter73 commented 10 months ago

I was concerned that Delegate.InvocationCount might be misconstrued to mean the number of times the Delegate has been invoked.

jkotas commented 10 months ago

I was concerned that Delegate.InvocationCount might be misconstrued

Renamed to GetInvocationListLength() for clarity.

jkotas commented 10 months ago

I have updated the proposal at the top with the feedback so far. Any comments?

weltkante commented 10 months ago

I have updated the proposal at the top with the feedback so far. Any comments?

IsSingle is probably meant to be a boolean?

jkotas commented 10 months ago

I think this is ready for next round of API review discussion.

bartonjs commented 9 months ago

Video

public class Delegate
{
    // Existing API
    // public virtual System.Delegate[] GetInvocationList();

    // Returns whether the delegate has a single target. It is a property to highlight that it is guaranteed 
    // to be a very cheap operation that is important for set of scenarios addressed by this proposal.
    public bool HasSingleTarget { get; }

    // Returns invocation list enumerator
    public static InvocationListEnumerator<TDelegate> EnumerateInvocationList<TDelegate>(TDelegate d) where TDelegate : Delegate;

    // The shape of this enumerator matches existing StringBuilder.ChunkEnumerator and Activity.Enumerator
    // Neither of these existing enumerators implement IEnumerable and IEnumerator. This can be changed. 
    // Q: How are we deciding whether enumerators like this should implement IEnumerable and IEnumerator?
    // Note that implementing these interfaces makes each instantiation of the type more expensive, so it is a tradeoff.
    public struct InvocationListEnumerator<TDelegate> where TDelegate : Delegate
    {
        public TDelegate Current { get; }
        public bool MoveNext();

        // EditorBrowsable to match existing StringBuilder.ChunkEnumerator and Activity.Enumerator
        [EditorBrowsable(EditorBrowsableState.Never)] // Only here to make foreach work
        public InvocationListEnumerator<TDelegate> GetEnumerator() => this;
    }
}
weltkante commented 8 months ago

I've wanted to try out the new APIs but they don't seem to appear in the daily builds, am I missing something? Sorry if there's known delays between PRs and daily builds, didn't see anything being mentioned.

jkotas commented 8 months ago

Yes, .NET SDK runs behind dotnet/runtime. You can check https://github.com/dotnet/installer/blob/main/eng/Version.Details.xml#L30 to see the dotnet/runtime commit that is in the SDK.

jkotas commented 8 months ago

Also https://github.com/dotnet/runtime/blob/main/docs/project/dogfooding.md#advanced-scenario---using-a-daily-build-of-microsoftnetcoreapp has instructions for how to get daily builds of dotnet/runtime without waiting for the bits to flow to .NET SDK.