Skip to content

Commit

Permalink
Avoid closure capture in ConcurrentDictionary.GetOrAdd`3 (#242)
Browse files Browse the repository at this point in the history
* Rewrite ConcurrentDictionary.GetOrAdd`3 to avoid closure allocation

* Use static modifier on existing ConcurrentDictionary.GetOrAdd`3 calls to prevent accidental capture
  • Loading branch information
MattKotsenas authored Oct 25, 2024
1 parent f408f01 commit 644631a
Show file tree
Hide file tree
Showing 5 changed files with 46 additions and 12 deletions.
4 changes: 2 additions & 2 deletions api_list.include.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@
* `Task CancelAsync(CancellationTokenSource)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.threading.cancellationtokensource.cancelasync)


#### ConcurrentDictionary<TKey,TValue>
#### ConcurrentDictionary<TKey, TValue>

* `TValue GetOrAdd<TKey, TValue, TArg>(ConcurrentDictionary<TKey,TValue>, TKey, Func<TKey, TArg, TValue>, TArg) where TKey : notnull` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.concurrent.concurrentdictionary-2.getoradd#system-collections-concurrent-concurrentdictionary-2-getoradd-1(-0-system-func((-0-0-1))-0))
* `TValue GetOrAdd<TKey, TValue, TArg>(ConcurrentDictionary<TKey, TValue>, TKey, Func<TKey, TArg, TValue>, TArg) where TKey : notnull` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.concurrent.concurrentdictionary-2.getoradd#system-collections-concurrent-concurrentdictionary-2-getoradd-1(-0-system-func((-0-0-1))-0))


#### DateOnly
Expand Down
2 changes: 1 addition & 1 deletion src/Consume/Consume.cs
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ void Type_GetMethod()
void ConcurrentDictionary_Methods()
{
var dict = new ConcurrentDictionary<string, int>();
var value = dict.GetOrAdd("Hello", (_, arg) => arg.Length, "World");
var value = dict.GetOrAdd("Hello", static (_, arg) => arg.Length, "World");
}

void Dictionary_Methods()
Expand Down
8 changes: 4 additions & 4 deletions src/Polyfill/Nullability/NullabilityInfoExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public static bool IsNullable(this MemberInfo info)
}

public static NullabilityInfo GetNullabilityInfo(this FieldInfo info) =>
fieldCache.GetOrAdd(info, inner =>
fieldCache.GetOrAdd(info, static inner =>
{
var context = new NullabilityInfoContext();
return context.Create(inner);
Expand All @@ -68,7 +68,7 @@ public static bool IsNullable(this FieldInfo info)
}

public static NullabilityInfo GetNullabilityInfo(this EventInfo info) =>
eventCache.GetOrAdd(info, inner =>
eventCache.GetOrAdd(info, static inner =>
{
var context = new NullabilityInfoContext();
return context.Create(inner);
Expand All @@ -86,7 +86,7 @@ public static bool IsNullable(this EventInfo info)
public static NullabilityInfo GetNullabilityInfo(this PropertyInfo info) =>
propertyCache.GetOrAdd(
info,
inner =>
static inner =>
{
var context = new NullabilityInfoContext();
return context.Create(inner);
Expand All @@ -102,7 +102,7 @@ public static bool IsNullable(this PropertyInfo info)
}

public static NullabilityInfo GetNullabilityInfo(this ParameterInfo info) =>
parameterCache.GetOrAdd(info, inner =>
parameterCache.GetOrAdd(info, static inner =>
{
var context = new NullabilityInfoContext();
return context.Create(inner);
Expand Down
42 changes: 38 additions & 4 deletions src/Polyfill/Polyfill_ConcurrentDictionary.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,46 @@ static partial class Polyfill
/// (Nothing in Visual Basic).</exception>
/// <exception cref="OverflowException">The dictionary contains too many
/// elements.</exception>
/// <returns>The value for the key. This will be either the existing value for the key if the
/// <returns>The value for the key. This will be either the existing value for the key if the
/// key is already in the dictionary, or the new value for the key as returned by valueFactory
/// if the key was not in the dictionary.</returns>
[Link("https://learn.microsoft.com/en-us/dotnet/api/system.collections.concurrent.concurrentdictionary-2.getoradd#system-collections-concurrent-concurrentdictionary-2-getoradd-1(-0-system-func((-0-0-1))-0)")]
public static TValue GetOrAdd<TKey,TValue, TArg>(this ConcurrentDictionary<TKey,TValue> target, TKey key, Func<TKey, TArg, TValue> valueFactory, TArg factoryArgument)
where TKey : notnull =>
target.GetOrAdd(key, _ => valueFactory(_, factoryArgument));
public static TValue GetOrAdd<TKey, TValue, TArg>(this ConcurrentDictionary<TKey, TValue> target, TKey key, Func<TKey, TArg, TValue> valueFactory, TArg factoryArgument)
where TKey : notnull
{
// Implementation based on https://github.com/dotnet/runtime/issues/13978#issuecomment-69494764.
// Because this API is intended to be used in high performance scenarios where avoiding allocations
// is important, we can't delegate to the existing `GetOrAdd`2`, as that would allocate a closure
// over `factoryArgument`.

if (target is null)
{
throw new ArgumentNullException(nameof(target));
}

if (key is null)
{
throw new ArgumentNullException(nameof(target));
}
if (valueFactory is null)
{
throw new ArgumentNullException(nameof(valueFactory));
}

while (true)
{
TValue value;
if (target.TryGetValue(key, out value))
{
return value;
}

value = valueFactory(key, factoryArgument);
if (target.TryAdd(key, value))
{
return value;
}
}
}
}
#endif
2 changes: 1 addition & 1 deletion src/Tests/PolyfillTests_ConcurrentDictionary.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ public void ConcurrentDictionaryGetOrAddFunc()
{
var dictionary = new ConcurrentDictionary<string, int>();

Func<string, string, int> valueFactory = (key, arg) => arg.Length;
Func<string, string, int> valueFactory = static (key, arg) => arg.Length;

var value = dictionary.GetOrAdd("Hello", valueFactory, "World");

Expand Down

0 comments on commit 644631a

Please sign in to comment.