Skip to content

Proposal: ref struct ConditionalWeakTable<TKey, TValue>.GetEnumerator() #53296

Closed
@Sergio0694

Description

@Sergio0694

Background and Motivation

The ConditionalWeakTable<TKey, TValue> is an extremely useful type that allows developers to leverage GC dependent handles (which are not directly available), along with some other nice features on top (eg. auto trimming, thread-safety by default, etc.). There is currently one drawback though when trying to use this type in code that's trying to be as fast and memory efficient as possible, which is that it is not possible to enumerate key-value pairs in a ConditionalWeakTable<TKey, TValue> table without incurring in unwanted allocations and extra overhead. Specifically:

  • The GetEnumerator implementation returns a reference type enumerator, incurring in one allocation
  • The enumerator type also has a finalizer, which makes the allocation slower even though it's not actually needed in most cases (given that when using foreach, Dispose() is always called anyway, which suppresses the finalizer).

Proposed API

This proposal is to expose a value-based enumerator, allowing developers to enumerate key-value pairs in ConditionalWeakTable<TKey, TValue> instances without incurring in any allocations at all, and with also some nice performance improvements on top (which are just a natural consequence of moving to a value-based enumerator in this case and removing the two performance penalties mentioned before).

The proposed API is this one:

namespace System.Runtime.CompilerServices
{
    public sealed class ConditionalWeakTable<TKey, TValue> : IEnumerable<KeyValuePair<TKey, TValue>>
        where TKey : class
        where TValue : class?
    {
+       public Enumerator GetEnumerator();
        
+       public ref struct Enumerator
+       {
+           public KeyValuePair<TKey, TValue> Current { get; }
+
+           public bool MoveNext();
+           public void Dispose();
+       }
    }
}

This is perfectly safe when used through the foreach pattern, given that the C# compiler will still generate the same calls as when using a normal iterator, with the same try/finally blocks and the final Dispose() call (which is needed here to decrement the internal reference count for the ConditionalWeakTable<TKey, TValue> instance).

I'd be happy to contribute this feature myself in case the proposal was approved 🙌

Usage Examples

We're using ConditionalWeakTable<TKey, TValue> in the WeakReferenceMessenger type in the MVVM Toolkit. Currently this type incurs in allocations when broadcasting messages solely for the Enumerator type being instantiated every single time. As a test, I've hacked the DependentHandle type in a benchmark project (by defining it in a dummy System.Private.CoreLib project and using [IgnoreAccessChecksTo] and PrivateAssets="All" so that the MVVM Toolkit would load the true one from CoreCLR instead, see here, and then I copied the ConditionalWeakTable<TKey, TValue> source to it while adding the new API mentioned above to it. I then proceeded to run the same benchmark (see here) across the original implementation of WeakReferenceMessenger, and one using this modified ConditionalWeakTable<TKey, TValue> implementation:

Method Mean Error StdDev Ratio Allocated
Original 21.79 ms 0.296 ms 0.277 ms 1.00 192,029 B
Improved 18.66 ms 0.216 ms 0.191 ms 0.86 -

✅ Completely allocation free broadcast
✅ About 15% faster too while at it, which doesn't hurt

I reckon there might be many other scenarios where library authors are using ConditionalWeakTable<TKey, TValue> as well where the ability to iterate key-value pairs without incurring in extra allocations would be very welcome as well.

Alternative Designs

While this would still be perfectly safe under normal usage, I understand that it's still technically slightly less safe than the normal enumerator, given that developers could in theory (but also, why?) do weird stuff like copying the enumerator, use it manually outside of a foreach block, etc. To that I have two possible answers:

  1. The ConditionalWeakTable<TKey, TValue> is already in the System.Runtime.CompilerServices namespace, which is intrinsically considered "unsafe" and "use at your own risk" in general, so it's assumed that developers using this type (or any of the others in this namespace in general) would know what they're doing. Because of this, I think adding this new API directly on ConditionalWeakTable<TKey, TValue> would be fine.
  2. In case this proposed API was still considered not safe enough to add to ConditionalWeakTable<TKey, TValue> directly, an alternative proposal could be to just move the API to CollectionMarshal instead. This would be clunkier (the enumerator type would have to either be moved too or just not be usable directly from ConditionalWeakTable<TKey, TValue>, and it'd also need a new GetEnumerator() method on it just returning this, in order to be usable in a foreach block), but in turn it'd entirely separate this slightly more unsafe API into this new special-purposed type (which also contains a bunch of other unsafe APIs for other collection types).

I'd personally argue for 1) given the above (and especially because it'd make the new enumerator much less verbose to use), but at the end of the day I'd be happy with either as long as the core functionality is present, which is true in both cases 😄

Risks

Very low, as mentioned above. It'd be a niche API on an already niche type, in a niche namespace that's already being used to host all sorts of weird and unsafe APIs. Developers ever finding this should already be very much aware of how to use it properly (especially given that we would also include detailed XML docs to call out all the various potential pitfalls for incorrect usage).

Additional context

For reference, as mentioned above, we could use this API to completely remove memory allocations in our WeakReferenceMessenger implementation in the MVVM Toolkit (part of the Windows Community Toolkit). We particularly focused on speed and efficiency in our messenger types, which are currently far ahead of all other available MVVM libraries (eg. Prism, Caliburn, etc.). Here's some benchmarks with our current implementations:

BenchmarkDotNet=v0.13.0, OS=Windows 10.0.19042.1023 (20H2/October2020Update)
AMD Ryzen 7 2700X, 1 CPU, 16 logical and 8 physical cores
.NET SDK=6.0.100-preview.4.21255.9
  [Host]     : .NET 5.0.6 (5.0.621.22011), X64 RyuJIT
  DefaultJob : .NET 5.0.6 (5.0.621.22011), X64 RyuJIT
Method Mean Error Ratio Gen 0 Allocated
MVVMToolkitStrong 6.428 ms 0.0108 ms 0.08 - -
MVVMToolkitWeak 20.883 ms 0.3085 ms 0.25 31.2500 192,009 B
MVVMLight 82.511 ms 1.5461 ms 1.00 10857.1429 45,920,081 B
Prism 216.294 ms 2.1281 ms 2.62 18000.0000 76,096,445 B
CaliburnMicro 465.634 ms 2.7784 ms 5.65 95000.0000 397,825,336 B

This proposed API would allow us to both completely eliminate allocations in the broadcast method for the weak messenger, as well as reducing the performance gap between our two messenger types (cc. @michael-hawker, Windows Community Toolkit lead).

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions