New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Get InvocationList of MulticastDelegate without any allocation #41849
Comments
I couldn't figure out the best area label to add to this issue. If you have write-permissions please help me learn by adding exactly one area label. |
This doesn't really describe the motivation, just the desired solution. Where do you see this API being used? Do you anticipate those scenarios seeing a measurable perf benefit? That would help justify the work needed for this API. Thanks! |
I have used this API to handle exception from each invoked delegate. |
Would syntax like the following work for you instead? I don't think there's a reliable way to create a one-element SomeDelegate myDelegate = GetDelegate();
foreach (Delegate innerDelegate in myDelegate)
{
var castDelegate = (SomeDelegate)innerDelegate;
// use 'castDelegate' here
} You should also spend some time prototyping this to make sure that the performance is what you expect. In your own application, make sure that the cost of this allocation really is measurable. You can use |
Ah,I just mistaken that we need ref Delegate for CreateSpan,but not Delegate. public ReadOnlySpan<Delegate> InvocationList => delegates == this ? MemoryMarshal.CreateSpan<Delegate>(ref delegates,1) : Unsafe.As<Delegate[]>( delegates); |
That's probably not a viable solution. There's a lot of code in the runtime - both managed and unmanaged - that has knowledge about the exact layout of delegate instances and what all the different instance fields represent. It would be very risky (and possibly also a negative perf hit) to update all of those call sites. That's not a good tradeoff for a niche API. |
Aren't you confusing it with field of Delegate? |
See here for some examples. You can follow the call graphs backward and see that there's a decent amount of code which relies on this field containing a restricted set of possible values. |
Oh,maybe I was seeing mono's one. |
Such syntax (as long as it is not allocating) would actually be very nice for when you'd have to invoke each delegate within a try catch block to ensure each gets called. So e.g. SomeDelegate myDelegate = GetDelegate();
foreach (Delegate innerDelegate in myDelegate)
{
try
{
// invoke single delegate target
((SomeDelegate)innerDelegate).Invoke()
}
catch (Exception exception)
{
//Do something with that exception
}
} As of right now we (Example) just call GetInvocationList() which always allocates. (It shows up already in measurements, although not yet high up in the list. Still working on reducing others first) |
I have just created benchmark project.
BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19041.508 (2004/?/20H1)
Intel Core i9-9900K CPU 3.60GHz (Coffee Lake), 1 CPU, 16 logical and 8 physical cores
.NET Core SDK=5.0.100
[Host] : .NET Core 5.0.0 (CoreCLR 5.0.20.51904, CoreFX 5.0.20.51904), X64 RyuJIT
DefaultJob : .NET Core 5.0.0 (CoreCLR 5.0.20.51904, CoreFX 5.0.20.51904), X64 RyuJIT
|
@RamType0 The benchmark code makes incorrect assumptions about the layout of that field, and it makes incorrect assumptions about how the C# language treats reachability of objects passed as in parameters. This could lead to reliability problems at runtime. I'll re-up my comment from #41849 (comment). We could probably add an enumerator to allow this scenario if it's really that important, but creating a |
This caused just by MemoryMarshal.CreateReadOnlySpan. public static ref readonly T UnsafeGetInvocationListByRefAndCount<T>(in T d,out int count)
where T : MyMulticastDelegate
{
var _invocationList = d._invocationList;
if (_invocationList != null)
{
var invocationList = Unsafe.As<object[]>(_invocationList);
count = (int)d._invocationCount;
return ref Unsafe.As<object, T>(ref MemoryMarshal.GetArrayDataReference(invocationList));
}
else
{
count = 1;
return ref d;
}
} So, it is not a UnsafeGetInvocationList specific problem.
It is better than current API. |
In which use case (where you'd use this API) would a ROS actually be more beneficial than an enumerator? @GrabYourPitchforks In your example you've set the type of the loop variable to |
@bollhals If it's an instance method, the type would be |
Exactly what is slow about it? |
using |
@GrabYourPitchforks BTW, MyGetInvocationList is seems to be better than CoreCLRGetInvocationList without any API changes. |
If im not mistaken real reason was performance of list with large structs. Eg imagine a list with million Matrix4x4 structs. Each such matrix holds 16 floats making it pretty bulky and each such struct had to be copied every time a value was retrieved via foreach in our case meaning potentially 1 milion copies of relatively large struct. This can definitely tank performance. Class based cases are largely unaffected unless u wanted to swap references itself during iteration |
At least, the posted benchmark uses int for element, which is smaller than "reference". |
Being skeptical of a post made more than 10 years ago, I went and did a proper benchmark. BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19041.572 (2004/?/20H1)
Intel Core i7-10610U CPU 1.80GHz, 1 CPU, 8 logical and 4 physical cores
.NET Core SDK=5.0.100
[Host] : .NET Core 3.1.9 (CoreCLR 4.700.20.47201, CoreFX 4.700.20.47203), X64 RyuJIT
.NET Core 3.1 : .NET Core 3.1.9 (CoreCLR 4.700.20.47201, CoreFX 4.700.20.47203), X64 RyuJIT
.NET Core 5.0 : .NET Core 5.0.0 (CoreCLR 5.0.20.51904, CoreFX 5.0.20.51904), X64 RyuJIT
So I concede that lists are slower than a raw array, however I also don't think this difference is the end of the world, provided you're not literally on fire on a burning hot path. Now I'm very curious on where this difference comes from, since I was assuming it could get much closer to array performance than this. |
foreach on an array is desugared by the C# compiler into simple for loop. foreach on a list needs to go through its struct Enumerator, does version checks to make sure the list hasn't changed between iterations, etc. |
@Joe4evr
"foreach" for span or array is converted to "for". It is also seems that IntList is significant faster than ClassList. |
The CollectionsMarshal class was added to allow efficient bulk operations over the list, such as bulk moving data or bulk modification of data. It wasn't added because of any perceived deficiency in List's enumerator. We're getting very off-track here. If the request is specifically for projection of a MulticastDelegate as a ROS over its inner targets, then that is not feasible (as previously mentioned) and this issue should be closed. This issue provided several alternative designs, but if we don't have a consumer for those alternative APIs then this work will never rise above the cut line for any release. |
@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:
|
The objection to exposing the proposed properties was raised by @GrabYourPitchforks, so I let him speak to the details here. My understanding was that In order for me to propose better names is dependent upon understanding the desired semantics. |
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. |
Marking the API as I forgot to do that in the meeting 🤦 |
I was concerned that |
Renamed to |
I have updated the proposal at the top with the feedback so far. Any comments? |
|
I think this is ready for next round of API review discussion. |
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;
}
} |
) * Add APIs for allocation-free delegate invocation list inspection Fixes #41849 * Apply suggestions from code review Co-authored-by: Stephen Toub <stoub@microsoft.com> * Update src/libraries/System.Private.CoreLib/src/System/Delegate.cs * Return empty enumerator for null * Perf tweaks --------- Co-authored-by: Stephen Toub <stoub@microsoft.com>
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. |
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. |
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. |
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
Original rejected proposal
The text was updated successfully, but these errors were encountered: