-
Notifications
You must be signed in to change notification settings - Fork 4.8k
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
New Type System.Runtime.CompilerServices.DependentHandle<TPrimary, TSecondary>
#19459
Comments
What is the point of having both |
No real reason. The internal type has |
API review: Random notes:
|
Pretty much it's useful for cases where you could use Primary uses cases are caches or linking classes that you can't modify (pretty much the uses cases for System.Runtime.CompilerServices.ConditionalWeakTable<TKey, TValue>`) |
I might add that As far as use-case is concerned, this is very important when implementing libraries for weak event handling or weak delegates. To work around the BCL not exposing DependentHandle all our applications are currently using reflection to get hold of the internal API (similar to what was linked in the original post) because ConditionalWeakTable is too much overhead for us. I'd welcome it if that was not necessary. |
Continuing the conversation here from #53296. As per @jkotas' comment (here):
As suggested by @tannergooding, if we could get a comment in this issue as well to sum up why it'd be worth it to expose this type, and whether or not the proposal here is fine or would need any changes, then we could hopefully get this issue marked as ready to review so we can get it looked at in one of the upcoming API review streams, which would be awesome 😄 |
@jkotas, do you think there is additional work/discussion required here with regards to the proposed API? If the signature looks alright as is (either exactly matching the internal or being "type-safe" as proposed) and there is no expected GC work, this seems relatively simple to mark Notably it does differ from the internal signature, which is: internal struct DependentHandle
{
public DependentHandle(object primary, object? secondary);
public bool IsAllocated { get; }
public object? GetPrimary();
public object? GetPrimaryAndSecondary(out object? secondary);
public void SetPrimary(object? primary);
public void SetSecondary(object? secondary);
public void Free();
} |
We need to hammer out the details. Here are some questions on my mind:
|
I don't have a particular preference here. I think given the use-case and the fact that related types, like
👍
I'm fine with
Makes sense to me. |
For reference, this would be the proposed API surface taking into account all of @jkotas' and @tannergooding's comments: namespace System.Runtime.InteropServices
{
public struct DependentHandle : IDisposable
{
public DependentHandle(object? primary, object? secondary);
public bool IsAllocated { get; }
public object? GetPrimary();
public object? GetPrimaryAndSecondary(out object? secondary);
public void SetPrimary(object? primary);
public void SetSecondary(object? secondary);
public void Dispose();
}
} I'll also add that personally I agree with all of that too: the type doesn't really have to be generic and people can always cast or unsafe-cast if needed, and this way the type will have less overhead and complexity, and potentially be more flexible as well. Same about EDIT: added the namespace to be |
Just some quick replies on the OG proposal questions
The genericness was mostly a personal thing (I hate having to use
Agreed. API was simply before C# 8. This is also why I converted the API to a
Sums up what my wrapper did.
Was mostly a debugging helper (Makes it very quick to see if |
I had a similar proposal #30759 (naming it ConditionalWeakReference instead of exposing the internal name, but thats just naming and doesn't matter) Considering that both proposals are about exactly the same thing under a different name, and I was being told that it going into .NET 6 was not an option without reworking the GC, I'm surprised this suddenly gets so much support. Happy if this gets accepted, in this case you probably can close the other proposal. Also note that the implementation of DependentHandle in the GC has a bug if the values of a |
From the comments I see in #12255 and specifically this commment, that doesn't really seem like a bug per se to me, rather than just an implementation detail causing this admittedly unintuitive behavior in that very specific case. That said, @jkotas said here that that's not really a priority and not worth losing performance in |
could someone please explain what scenarios would be made better by implementing IDisposable for this? I've seen DH scanning show up in CPU profiles quite often with our 1st party workloads (not surprising as scanning DHs has never been optimized ; and DHs are only scanned during the stop-the-world phase of a background GC). also because we haven't gone and optimized this, we also haven't added much instrumentation to help easily show how long the pauses are caused by DH so you will need to rely on CPU profiles (I'm fixing this part in .NET 6 though, since I've seen this causing perf problems). in the doc for this, if it goes in, the perf aspect should absolutely be emphasized. so do your perf measurement diligently if you anticipate to use these at large scale (I've seen over and over again that folks tested something locally and concluded it's ok then got surprised by prod perf because their prod was so much more stressful than local). |
FWIW I've occasionally had to add methods like C++/CLI prohibits calling But, C++/CLI is finally able to fade away, especially with all the new goodies in C# 7, 8, 9 (spans, function pointers, etc.) and things like I've only done this for a few "low-level" supporting types that were used in my interop/glue code (e.g. Direct2D et. al. wrappers). And, you can just use a helper method over in C# to enable C++/CLI code to call |
For ConditionalWeakTable its an implementation detail, since its intended to be used as a static value, yet it still has a Finalizer which disposes the DependentHandles - in that context it can be considered a bug, because the Finalizer will never be called even if the entire object graph is no longer accessible. Once you expose DependentHandle it will become more of a problem when people store it in other objects with shorter lifetimes than a static field. From a correctness perspective, using a Finalizer to dispose the DependentHandle is not a valid implementation given the current GC behavior unless you have full control over the entire object graph reachable from the DependentHandle. I'm not arguing against exposing DependentHandle (I'm already using DependentHandle myself via reflection) - but if its exposed it should be documented that Disposing in a Finalizer will not work reliably (i.e. never finalizes if cycles are present). Otherwise people will test their implementation in simple scenarios and it looks like it works just fine, but in real-life scenarios when cycles start to appear it suddenly leaks. |
Replying to @Maoni0:
I'm sure @jkotas will be able to elaborate more on this, but my understanding from the previous conversation is that in particular the way
We should definitely ensure the docs for Replying to @weltkante:
From what I can see in the linked issue (here), and also from some tests I run locally, it isn't actually the case that the issue is caused by cycles per se, rather than primary objects in dependent handles remaining alive, and therefore keeping the entire dependent object graph alive. That is to say, the repro from that issue is: object key = new object();
while (true) {
var table = new ConditionalWeakTable<object, Tuple<object, byte[]>>();
table.Add(key, new Tuple<object, byte[]>(table, new byte[1000000]));
GC.Collect();
} Which causes a leak, yes, but only because the key is still alive. I can definitely agree that might be unintuitive given that the CWT instances have been thrown away and that the behavior is a result of the dependent handles only being freed from the CWT finalizer (which is an implementation detail, sure), which is not running here since object key = new object();
for (int i = 0; i < 10_000; i++)
{
var table = new ConditionalWeakTable<object, Tuple<object, byte[]>>();
table.Add(key, new Tuple<object, byte[]>(table, new byte[1000000]));
GC.Collect();
}
key = null;
GC.Collect(); Then it'll allocate about 10GB of garbage, then properly collect everything just fine. I'm just saying, just for clarity, that we should not refer to this behavior as a bug, as it really isn't - Likewise, reference cycles are actually handled too, eg. if you try this: while (true)
{
var foo = new { Table = new ConditionalWeakTable<object, Tuple<object, byte[]>>() };
foo.Table.Add(foo, new Tuple<object, byte[]>(foo, new byte[1000000]));
GC.Collect();
} There will be no memory leaks either, and all the object graphs created for each iteration will be collected as well just fine 🙂
|
I agree with @KevinCathcart this is not an ephemeron so we shouldn't call it so. as far as Disposable goes, my intention was obviously not to say that it's not popular because it doesn't exist, it's that it's not a common scenario that one would want to use |
I can definitely think of a few cases in interop where being able to use void DoCallback(void* state, void(*callback)(void*)); If the developer in C# wants to use this API with the least overhead, they would define a P/Invoke to it as follows: [DllImport("nativelib")]
public static unsafe extern void DoCallback(IntPtr state, delegate*<IntPtr, void> callback); And the callback: [UnmanagedCallersOnly]
public static void Callback(IntPtr state)
{
GCHandle handle = GCHandle.FromIntPtr(state);
object state = handle.Target;
} Then they could use it as follows: using (GCHandle handle = GCHandle.Alloc(state))
{
DoCallback(handle, &Callback);
} This style is common enough that the example given for using |
@jkoritzinsky I agree it's a bit more convenient to do it the |
It is not that straightforward. You have to typically write it in try finally to ensure that the handle is disposed even when the exception thrown - the example in the GCHandle docs is not 100% production quality code because of it can leak. |
@jkotas Now that we've updated the API proposal and settled on one that from what I can see the team thinks is fine, what do you consider the risk be to ship this in .NET 6? In other words, could we get this added to the .NET 6 milestone at this point? 🙂 For some more context (following our conversation from my previous proposal #53296): I'm planning to add the .NET 6 target to the Windows Community Toolkit, and at the same time I'm working on a new major version of the MVVM Toolkit we're shipping as part of it (don't have an exact timeline yet but likely by the end of the year). Having access to
With the cutoff date for new features into .NET 6 being about July 17th and getting close and there being a number of other blocking issues already, just wanted to make sure this proposal was being considered for this next release once the schedule for API review gets past these next blocking issues that are there. Thanks! 😄 cc. @michael-hawker |
I do not see this a particularly risky. As you have said, we are getting close to cut off, so the problem is the band-width. It is up to @dotnet/area-system-runtime-compilerservices to decide whether they have band-width to deal with this. |
The MVVM Toolkit messenger class is pretty amazing. It's super simple to use, but really powerful and easy to use anywhere. It's been really amazing to see @Sergio0694's work on optimizing it's performance and it's really a big differentiation for the library for these APIs. To say we have zero allocations would be a game changer for highlighting this differentiation for the Windows Community Toolkit. |
namespace System.Runtime
{
public struct DependentHandle
{
public DependentHandle(object? target, object? dependent);
public bool IsAllocated { get; }
public object? Target { get; set; }
public object? Dependent { get; set; }
(object? Target, object? Dependent) TargetAndDependent { get; }
public void Free();
}
} |
Modulo the
|
VS debugger inspection affects GC behavior in number of subtle ways. It is by design and I do not see it as a problem. I do not have a strong opinion on the method vs. properties. It is purely about ones artistic preferences as mentioned during the API review.
@bartonjs I was not able to follow your argument against IDisposable / Dispose. We have recent prior art such as |
It is very straightforward to make the Dispose implementation idempotent. Dispose will do nothing if the internal handle that this wraps is null, and set the internal handle pointer to null otherwise. It will behave same way as |
As mentioned above, I'd be happy to pick this up and work on a PR (especially given that we're relatively tight on time), and we can always address/incorporate further feedbacks as they come. Agreed with @jkotas that making Just to recap, the currently proposed API surface (with updates) would be this then: namespace System.Runtime
{
public struct DependentHandle : IDisposable
{
public DependentHandle(object? target, object? dependent);
public bool IsAllocated { get; }
public object? Target { get; set; }
public object? Dependent { get; set; }
public (object? Target, object? Dependent) GetTargetAndDependent();
public void Dispose();
}
} I've added Pinging @tannergooding as he's been driving this during API review as well, feel free to assign the issue to me if it's alright 🙂 |
It makes it thread-safe, not generally safe. |
Right, I meant it'd make it safe with respect to single or multi-threaded disposals to the same instance. If the value was copied in multiple places (including boxing, etc.) then it'd still fall apart on multiple disposals, sure. Not sure if that'd be a concern in this case considering the fact it's a very specialized type? Happy to change the implementation there with any feedbacks 🙂 |
That's a concern that I have, yes. Especially because I don't have a gut feeling for what happens when multiple value-copies get disposed. If it's bad, like a double-Free, then manually calling Free is better (to me) -- because then it's a clear "stop, and think about ownership here". If that's actually guaranteed safe, then my concern goes away. |
Background and Motivation
There is an internal struct
System.Runtime.CompilerServices.DependentHandle
that is currently only used bySystem.Runtime.CompilerServices.ConditionalWeakTable<TKey, TValue>
. The struct allows for a dependency between two objects to be created by a third type without modifying the first two classes, and only using up a pointer's worth of memory in the third type.I have personally find use for such a class, in my Weak Delegates system for example. While in there I had to use slightly convoluted tricks to make it work I believe a more proper implementation included in the BCL would be useful.
Proposed API
Open Questions
Should
DependentHandle
implementIDisposable
? If so, we should expose the same on GCHandleShould the constructor be private and have a static
Alloc
method to match GCHandle?Should
TargetAndDependent
be a property or a method?The text was updated successfully, but these errors were encountered: