Skip to content

Commit ac61528

Browse files
Avoid generic virtual dispatch for frozen collections alternate lookup (#108732)
* Avoid generic virtual dispatch for frozen collections alternate lookup * Various stylistic changes addressing PR feedback * Tidy up some more xmldocs --------- Co-authored-by: Stephen Toub <[email protected]>
1 parent 1da2ea4 commit ac61528

File tree

36 files changed

+458
-96
lines changed

36 files changed

+458
-96
lines changed

src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/DefaultFrozenDictionary.AlternateLookup.cs

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,33 @@ namespace System.Collections.Frozen
1010
internal sealed partial class DefaultFrozenDictionary<TKey, TValue>
1111
{
1212
/// <inheritdoc/>
13-
private protected override ref readonly TValue GetValueRefOrNullRefCore<TAlternateKey>(TAlternateKey key)
13+
private protected override AlternateLookupDelegate<TAlternateKey> GetAlternateLookupDelegate<TAlternateKey>()
14+
=> AlternateLookupDelegateHolder<TAlternateKey>.Instance;
15+
16+
private static class AlternateLookupDelegateHolder<TAlternateKey>
17+
where TAlternateKey : notnull
18+
#if NET9_0_OR_GREATER
19+
#pragma warning disable SA1001 // Commas should be spaced correctly
20+
, allows ref struct
21+
#pragma warning restore SA1001
22+
#endif
23+
{
24+
/// <summary>
25+
/// Invokes <see cref="GetValueRefOrNullRefCoreAlternate{TAlternateKey}(TAlternateKey)"/>
26+
/// on instances known to be of type <see cref="DefaultFrozenDictionary{TKey, TValue}"/>.
27+
/// </summary>
28+
public static readonly AlternateLookupDelegate<TAlternateKey> Instance = (dictionary, key)
29+
=> ref ((DefaultFrozenDictionary<TKey, TValue>)dictionary).GetValueRefOrNullRefCoreAlternate(key);
30+
}
31+
32+
/// <inheritdoc cref="GetValueRefOrNullRefCore(TKey)" />
33+
private ref readonly TValue GetValueRefOrNullRefCoreAlternate<TAlternateKey>(TAlternateKey key)
34+
where TAlternateKey : notnull
35+
#if NET9_0_OR_GREATER
36+
#pragma warning disable SA1001 // Commas should be spaced correctly
37+
, allows ref struct
38+
#pragma warning restore SA1001
39+
#endif
1440
{
1541
IAlternateEqualityComparer<TAlternateKey, TKey> comparer = GetAlternateEqualityComparer<TAlternateKey>();
1642

src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/DefaultFrozenSet.AlternateLookup.cs

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,32 @@ namespace System.Collections.Frozen
77
{
88
internal sealed partial class DefaultFrozenSet<T>
99
{
10-
/// <inheritdoc />
11-
private protected override int FindItemIndex<TAlternate>(TAlternate item)
10+
/// <inheritdoc/>
11+
private protected override AlternateLookupDelegate<TAlternate> GetAlternateLookupDelegate<TAlternate>()
12+
=> AlternateLookupDelegateHolder<TAlternate>.Instance;
13+
14+
private static class AlternateLookupDelegateHolder<TAlternate>
15+
#if NET9_0_OR_GREATER
16+
#pragma warning disable SA1001 // Commas should be spaced correctly
17+
where TAlternate : allows ref struct
18+
#pragma warning restore SA1001
19+
#endif
20+
{
21+
/// <summary>
22+
/// Invokes <see cref="FindItemIndexAlternate{TAlternate}(TAlternate)"/>
23+
/// on instances known to be of type <see cref="DefaultFrozenSet{TValue}"/>.
24+
/// </summary>
25+
public static readonly AlternateLookupDelegate<TAlternate> Instance = (set, item)
26+
=> ((DefaultFrozenSet<T>)set).FindItemIndexAlternate(item);
27+
}
28+
29+
/// <inheritdoc cref="FindItemIndex(T)" />
30+
private int FindItemIndexAlternate<TAlternate>(TAlternate item)
31+
#if NET9_0_OR_GREATER
32+
#pragma warning disable SA1001 // Commas should be spaced correctly
33+
where TAlternate : allows ref struct
34+
#pragma warning restore SA1001
35+
#endif
1236
{
1337
IAlternateEqualityComparer<TAlternate, T> comparer = GetAlternateEqualityComparer<TAlternate>();
1438

src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/FrozenDictionary.AlternateLookup.cs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ public bool TryGetAlternateLookup<TAlternateKey>(out AlternateLookup<TAlternateK
5353
if (Comparer is IAlternateEqualityComparer<TAlternateKey, TKey> &&
5454
(typeof(TKey) != typeof(string) || typeof(TAlternateKey) == typeof(ReadOnlySpan<char>)))
5555
{
56-
lookup = new AlternateLookup<TAlternateKey>(this);
56+
lookup = new AlternateLookup<TAlternateKey>(this, GetAlternateLookupDelegate<TAlternateKey>());
5757
return true;
5858
}
5959

@@ -76,12 +76,16 @@ private protected IAlternateEqualityComparer<TAlternateKey, TKey> GetAlternateEq
7676
/// <typeparam name="TAlternateKey">The alternate type of a key for performing lookups.</typeparam>
7777
public readonly struct AlternateLookup<TAlternateKey> where TAlternateKey : notnull, allows ref struct
7878
{
79+
private readonly AlternateLookupDelegate<TAlternateKey> _alternateLookupDelegate;
80+
7981
/// <summary>Initialize the instance. The dictionary must have already been verified to have a compatible comparer.</summary>
80-
internal AlternateLookup(FrozenDictionary<TKey, TValue> dictionary)
82+
internal AlternateLookup(FrozenDictionary<TKey, TValue> dictionary, AlternateLookupDelegate<TAlternateKey> alternateLookupDelegate)
8183
{
8284
Debug.Assert(dictionary is not null);
8385
Debug.Assert(dictionary.Comparer is IAlternateEqualityComparer<TAlternateKey, TKey>);
86+
Debug.Assert(alternateLookupDelegate is not null);
8487
Dictionary = dictionary;
88+
_alternateLookupDelegate = alternateLookupDelegate;
8589
}
8690

8791
/// <summary>Gets the <see cref="FrozenDictionary{TKey, TValue}"/> against which this instance performs operations.</summary>
@@ -99,7 +103,7 @@ public TValue this[TAlternateKey key]
99103
{
100104
get
101105
{
102-
ref readonly TValue valueRef = ref Dictionary.GetValueRefOrNullRefCore(key);
106+
ref readonly TValue valueRef = ref _alternateLookupDelegate(Dictionary, key);
103107
if (Unsafe.IsNullRef(in valueRef))
104108
{
105109
ThrowHelper.ThrowKeyNotFoundException();
@@ -114,7 +118,7 @@ public TValue this[TAlternateKey key]
114118
/// <returns><see langword="true"/> if the key is in the dictionary; otherwise, <see langword="false"/>.</returns>
115119
/// <exception cref="ArgumentNullException"><paramref name="key"/> is <see langword="null"/>.</exception>
116120
public bool ContainsKey(TAlternateKey key) =>
117-
!Unsafe.IsNullRef(in Dictionary.GetValueRefOrNullRefCore(key));
121+
!Unsafe.IsNullRef(in _alternateLookupDelegate(Dictionary, key));
118122

119123
/// <summary>Gets the value associated with the specified alternate key.</summary>
120124
/// <param name="key">The alternate key of the value to get.</param>
@@ -126,7 +130,7 @@ public bool ContainsKey(TAlternateKey key) =>
126130
/// <exception cref="ArgumentNullException"><paramref name="key"/> is <see langword="null"/>.</exception>
127131
public bool TryGetValue(TAlternateKey key, [MaybeNullWhen(false)] out TValue value)
128132
{
129-
ref readonly TValue valueRef = ref Dictionary.GetValueRefOrNullRefCore(key);
133+
ref readonly TValue valueRef = ref _alternateLookupDelegate(Dictionary, key);
130134

131135
if (!Unsafe.IsNullRef(in valueRef))
132136
{

src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/FrozenDictionary.cs

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -449,20 +449,20 @@ public ref readonly TValue GetValueRefOrNullRef(TKey key)
449449
/// <inheritdoc cref="GetValueRefOrNullRef" />
450450
private protected abstract ref readonly TValue GetValueRefOrNullRefCore(TKey key);
451451

452-
/// <inheritdoc cref="GetValueRefOrNullRef" />
452+
/// <summary>
453+
/// Retrieves a delegate which calls a method equivalent to <see cref="GetValueRefOrNullRef(TKey)"/>
454+
/// for the <typeparamref name="TAlternateKey"/>.
455+
/// </summary>
453456
/// <remarks>
454457
/// This is virtual rather than abstract because only some implementations need to support this, e.g. implementations that
455458
/// are only ever used with the default comparer won't ever hit code paths that use this, at least not
456459
/// until/if we make `EqualityComparer{string}.Default` implement `IAlternateEqualityComparer{ReadOnlySpan{char}, string}`.
457460
///
458-
/// This unfortunately needs to be a generic virtual method, but the only other known option involves having a dedicated
459-
/// class instance such that the generic can be baked into that, where the methods on it are still virtual but don't have
460-
/// extra generic methods. But for most implementations, either a) that class would need to be allocated as part of
461-
/// TryGetAlternateLookup, which would be more expensive for use cases where someone needs a lookup for just a few operations,
462-
/// or b) a dictionary of those instances would need to be maintained, which just replaces the runtime's dictionary for a GVM
463-
/// with a custom one here.
461+
/// Generic Virtual method invocation is slower than delegate invocation and could negate
462+
/// much of the benefit of using Alternate Keys. By retrieving the delegate up-front when
463+
/// the lookup is created, we only pay for generic virtual method invocation once.
464464
/// </remarks>
465-
private protected virtual ref readonly TValue GetValueRefOrNullRefCore<TAlternateKey>(TAlternateKey key)
465+
private protected virtual AlternateLookupDelegate<TAlternateKey> GetAlternateLookupDelegate<TAlternateKey>()
466466
where TAlternateKey : notnull
467467
#if NET9_0_OR_GREATER
468468
#pragma warning disable SA1001 // Commas should be spaced correctly
@@ -473,7 +473,34 @@ private protected virtual ref readonly TValue GetValueRefOrNullRefCore<TAlternat
473473
, allows ref struct
474474
#pragma warning restore SA1001
475475
#endif
476-
=> ref Unsafe.NullRef<TValue>();
476+
=> AlternateLookupDelegateHolder<TAlternateKey>.ReturnsNullRef;
477+
478+
/// <summary>
479+
/// Invokes a method equivalent to <see cref="GetValueRefOrNullRef(TKey)"/>
480+
/// for the <typeparamref name="TAlternateKey"/>.
481+
/// </summary>
482+
internal delegate ref readonly TValue AlternateLookupDelegate<TAlternateKey>(FrozenDictionary<TKey, TValue> dictionary, TAlternateKey key)
483+
where TAlternateKey : notnull
484+
#if NET9_0_OR_GREATER
485+
#pragma warning disable SA1001 // Commas should be spaced correctly
486+
, allows ref struct
487+
#pragma warning restore SA1001
488+
#endif
489+
;
490+
491+
/// <summary>
492+
/// Holds an implementation of <see cref="AlternateLookupDelegate{TAlternateKey}"/> which always returns a null ref.
493+
/// </summary>
494+
private static class AlternateLookupDelegateHolder<TAlternateKey>
495+
where TAlternateKey : notnull
496+
#if NET9_0_OR_GREATER
497+
#pragma warning disable SA1001 // Commas should be spaced correctly
498+
, allows ref struct
499+
#pragma warning restore SA1001
500+
#endif
501+
{
502+
public static readonly AlternateLookupDelegate<TAlternateKey> ReturnsNullRef = (_, _) => ref Unsafe.NullRef<TValue>();
503+
}
477504

478505
/// <summary>Gets a reference to the value associated with the specified key.</summary>
479506
/// <param name="key">The key of the value to get.</param>

src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/FrozenSet.AlternateLookup.cs

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ public bool TryGetAlternateLookup<TAlternate>(out AlternateLookup<TAlternate> lo
5353
if (Comparer is IAlternateEqualityComparer<TAlternate, T> &&
5454
(typeof(T) != typeof(string) || typeof(TAlternate) == typeof(ReadOnlySpan<char>)))
5555
{
56-
lookup = new AlternateLookup<TAlternate>(this);
56+
lookup = new AlternateLookup<TAlternate>(this, GetAlternateLookupDelegate<TAlternate>());
5757
return true;
5858
}
5959

@@ -63,10 +63,10 @@ public bool TryGetAlternateLookup<TAlternate>(out AlternateLookup<TAlternate> lo
6363

6464
/// <summary>Gets the <see cref="Comparer"/> as an <see cref="IAlternateEqualityComparer{TAlternate, T}"/>.</summary>
6565
/// <remarks>This must only be used when it's already been proven that the comparer implements the target interface.</remarks>
66-
private protected IAlternateEqualityComparer<TAlternateKey, T> GetAlternateEqualityComparer<TAlternateKey>() where TAlternateKey : allows ref struct
66+
private protected IAlternateEqualityComparer<TAlternate, T> GetAlternateEqualityComparer<TAlternate>() where TAlternate : allows ref struct
6767
{
68-
Debug.Assert(Comparer is IAlternateEqualityComparer<TAlternateKey, T>, "Must have already been verified");
69-
return Unsafe.As<IAlternateEqualityComparer<TAlternateKey, T>>(Comparer);
68+
Debug.Assert(Comparer is IAlternateEqualityComparer<TAlternate, T>, "Must have already been verified");
69+
return Unsafe.As<IAlternateEqualityComparer<TAlternate, T>>(Comparer);
7070
}
7171

7272
/// <summary>
@@ -76,12 +76,16 @@ private protected IAlternateEqualityComparer<TAlternateKey, T> GetAlternateEqual
7676
/// <typeparam name="TAlternate">The alternate type of a key for performing lookups.</typeparam>
7777
public readonly struct AlternateLookup<TAlternate> where TAlternate : allows ref struct
7878
{
79+
private readonly AlternateLookupDelegate<TAlternate> _alternateLookupDelegate;
80+
7981
/// <summary>Initialize the instance. The set must have already been verified to have a compatible comparer.</summary>
80-
internal AlternateLookup(FrozenSet<T> set)
82+
internal AlternateLookup(FrozenSet<T> set, AlternateLookupDelegate<TAlternate> alternateLookupDelegate)
8183
{
8284
Debug.Assert(set is not null);
8385
Debug.Assert(set.Comparer is IAlternateEqualityComparer<TAlternate, T>);
86+
Debug.Assert(alternateLookupDelegate is not null);
8487
Set = set;
88+
_alternateLookupDelegate = alternateLookupDelegate;
8589
}
8690

8791
/// <summary>Gets the <see cref="FrozenSet{T}"/> against which this instance performs operations.</summary>
@@ -90,15 +94,15 @@ internal AlternateLookup(FrozenSet<T> set)
9094
/// <summary>Determines whether a set contains the specified element.</summary>
9195
/// <param name="item">The element to locate in the set.</param>
9296
/// <returns>true if the set contains the specified element; otherwise, false.</returns>
93-
public bool Contains(TAlternate item) => Set.FindItemIndex(item) >= 0;
97+
public bool Contains(TAlternate item) => _alternateLookupDelegate(Set, item) >= 0;
9498

9599
/// <summary>Searches the set for a given value and returns the equal value it finds, if any.</summary>
96100
/// <param name="equalValue">The value to search for.</param>
97101
/// <param name="actualValue">The value from the set that the search found, or the default value of <typeparamref name="T"/> when the search yielded no match.</param>
98102
/// <returns>A value indicating whether the search was successful.</returns>
99103
public bool TryGetValue(TAlternate equalValue, [MaybeNullWhen(false)] out T actualValue)
100104
{
101-
int index = Set.FindItemIndex(equalValue);
105+
int index = _alternateLookupDelegate(Set, equalValue);
102106
if (index >= 0)
103107
{
104108
actualValue = Set.Items[index];

src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/FrozenSet.cs

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -329,18 +329,56 @@ public bool TryGetValue(T equalValue, [MaybeNullWhen(false)] out T actualValue)
329329
/// <returns>The index of the value, or -1 if not found.</returns>
330330
private protected abstract int FindItemIndex(T item);
331331

332-
/// <summary>Finds the index of a specific value in a set.</summary>
333-
/// <param name="item">The value to lookup.</param>
334-
/// <returns>The index of the value, or -1 if not found.</returns>
335-
private protected virtual int FindItemIndex<TAlternate>(TAlternate item)
332+
/// <summary>
333+
/// Retrieves a delegate which calls a method equivalent to <see cref="FindItemIndex"/>
334+
/// for the <typeparamref name="TAlternate"/>.
335+
/// </summary>
336+
/// <remarks>
337+
/// This is virtual rather than abstract because only some implementations need to support this, e.g. implementations that
338+
/// are only ever used with the default comparer won't ever hit code paths that use this, at least not
339+
/// until/if we make `EqualityComparer{string}.Default` implement `IAlternateEqualityComparer{ReadOnlySpan{char}, string}`.
340+
///
341+
/// Generic Virtual method invocation is slower than delegate invocation and could negate
342+
/// much of the benefit of using Alternate Keys. By retrieving the delegate up-front when
343+
/// the lookup is created, we only pay for generic virtual method invocation once.
344+
/// </remarks>
345+
private protected virtual AlternateLookupDelegate<TAlternate> GetAlternateLookupDelegate<TAlternate>()
336346
#if NET9_0_OR_GREATER
347+
#pragma warning disable SA1001 // Commas should be spaced correctly
337348
// This method will only ever be used on .NET 9+. However, because of how everything is structured,
338349
// and to avoid a proliferation of conditional files for many of the derived types (in particular
339350
// for the OrdinalString* implementations), we still build this method into all builds, even though
340351
// it'll be unused. But we can't use the allows ref struct constraint downlevel, hence the #if.
341352
where TAlternate : allows ref struct
353+
#pragma warning restore SA1001
354+
#endif
355+
=> AlternateLookupDelegateHolder<TAlternate>.ReturnsNullRef;
356+
357+
/// <summary>
358+
/// Invokes a method equivalent to <see cref="FindItemIndex"/>
359+
/// for the <typeparamref name="TAlternate"/>.
360+
/// </summary>
361+
internal delegate int AlternateLookupDelegate<TAlternate>(FrozenSet<T> set, TAlternate key)
362+
#if NET9_0_OR_GREATER
363+
#pragma warning disable SA1001 // Commas should be spaced correctly
364+
where TAlternate : allows ref struct
365+
#pragma warning restore SA1001
342366
#endif
343-
=> -1;
367+
;
368+
369+
/// <summary>
370+
/// Holds an implementation of <see cref="AlternateLookupDelegate{TAlternate}"/> which always returns -1.
371+
/// </summary>
372+
private static class AlternateLookupDelegateHolder<TAlternate>
373+
#if NET9_0_OR_GREATER
374+
#pragma warning disable SA1001 // Commas should be spaced correctly
375+
where TAlternate : allows ref struct
376+
#pragma warning restore SA1001
377+
#endif
378+
{
379+
public static readonly AlternateLookupDelegate<TAlternate> ReturnsNullRef = (_, _) => -1;
380+
}
381+
344382

345383
/// <summary>Returns an enumerator that iterates through the set.</summary>
346384
/// <returns>An enumerator that iterates through the set.</returns>

src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/Int32/Int32FrozenDictionary.AlternateLookup.cs

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,33 @@ namespace System.Collections.Frozen
1111
internal sealed partial class Int32FrozenDictionary<TValue>
1212
{
1313
/// <inheritdoc/>
14-
private protected override ref readonly TValue GetValueRefOrNullRefCore<TAlternateKey>(TAlternateKey key)
14+
private protected override AlternateLookupDelegate<TAlternateKey> GetAlternateLookupDelegate<TAlternateKey>()
15+
=> AlternateLookupDelegateHolder<TAlternateKey>.Instance;
16+
17+
private static class AlternateLookupDelegateHolder<TAlternateKey>
18+
where TAlternateKey : notnull
19+
#if NET9_0_OR_GREATER
20+
#pragma warning disable SA1001 // Commas should be spaced correctly
21+
, allows ref struct
22+
#pragma warning restore SA1001
23+
#endif
24+
{
25+
/// <summary>
26+
/// Invokes <see cref="GetValueRefOrNullRefCoreAlternate{TAlternate}(TAlternate)"/>
27+
/// on instances known to be of type <see cref="Int32FrozenDictionary{TValue}"/>.
28+
/// </summary>
29+
public static readonly AlternateLookupDelegate<TAlternateKey> Instance = (dictionary, key)
30+
=> ref ((Int32FrozenDictionary<TValue>)dictionary).GetValueRefOrNullRefCoreAlternate(key);
31+
}
32+
33+
/// <inheritdoc cref="GetValueRefOrNullRefCore(int)" />
34+
private ref readonly TValue GetValueRefOrNullRefCoreAlternate<TAlternateKey>(TAlternateKey key)
35+
where TAlternateKey : notnull
36+
#if NET9_0_OR_GREATER
37+
#pragma warning disable SA1001 // Commas should be spaced correctly
38+
, allows ref struct
39+
#pragma warning restore SA1001
40+
#endif
1541
{
1642
IAlternateEqualityComparer<TAlternateKey, int> comparer = GetAlternateEqualityComparer<TAlternateKey>();
1743

0 commit comments

Comments
 (0)