Skip to content

Commit

Permalink
Avoid runtime "atomic write" check in ConcurrentDictionary for shared…
Browse files Browse the repository at this point in the history
… generics (#77005)

ConcurrentDictionary caches an s_isValueWriteAtomic in a static
readonly, but this trick does not work for shared generics. Extract the
static readonly field to a class only dependent on TValue to handle
more cases, and also add a reference type check up front to get a lot
of the shared generics cases.
  • Loading branch information
jakobbotsch authored Oct 18, 2022
1 parent 3f600a1 commit e8fa4a7
Showing 1 changed file with 51 additions and 41 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -54,45 +54,6 @@ public class ConcurrentDictionary<TKey, TValue> : IDictionary<TKey, TValue>, IDi
/// </remarks>
private const int MaxLockNumber = 1024;

/// <summary>Whether TValue is a type that can be written atomically (i.e., with no danger of torn reads).</summary>
private static readonly bool s_isValueWriteAtomic = IsValueWriteAtomic();

/// <summary>Determines whether type TValue can be written atomically.</summary>
private static bool IsValueWriteAtomic()
{
// Section 12.6.6 of ECMA CLI explains which types can be read and written atomically without
// the risk of tearing. See https://www.ecma-international.org/publications/files/ECMA-ST/ECMA-335.pdf

if (!typeof(TValue).IsValueType ||
typeof(TValue) == typeof(IntPtr) ||
typeof(TValue) == typeof(UIntPtr))
{
return true;
}

switch (Type.GetTypeCode(typeof(TValue)))
{
case TypeCode.Boolean:
case TypeCode.Byte:
case TypeCode.Char:
case TypeCode.Int16:
case TypeCode.Int32:
case TypeCode.SByte:
case TypeCode.Single:
case TypeCode.UInt16:
case TypeCode.UInt32:
return true;

case TypeCode.Double:
case TypeCode.Int64:
case TypeCode.UInt64:
return IntPtr.Size == 8;

default:
return false;
}
}

/// <summary>
/// Initializes a new instance of the <see cref="ConcurrentDictionary{TKey,TValue}"/>
/// class that is empty, has the default concurrency level, has the default initial capacity, and
Expand Down Expand Up @@ -594,7 +555,11 @@ nullableHashcode is null ||
{
if (valueComparer.Equals(node._value, comparisonValue))
{
if (s_isValueWriteAtomic)
// Do the reference type check up front to handle many cases of shared generics.
// If TValue is a value type then the field's value here can be baked in. Otherwise,
// for the remaining shared generic cases the field access here would disqualify inlining,
// so the following check cannot be factored out of TryAddInternal/TryUpdateInternal.
if (!typeof(TValue).IsValueType || ConcurrentDictionaryTypeProps<TValue>.IsWriteAtomic)
{
node._value = newValue;
}
Expand Down Expand Up @@ -933,7 +898,11 @@ nullableHashcode is null ||
// be written atomically, since lock-free reads may be happening concurrently.
if (updateIfExists)
{
if (s_isValueWriteAtomic)
// Do the reference type check up front to handle many cases of shared generics.
// If TValue is a value type then the field's value here can be baked in. Otherwise,
// for the remaining shared generic cases the field access here would disqualify inlining,
// so the following check cannot be factored out of TryAddInternal/TryUpdateInternal.
if (!typeof(TValue).IsValueType || ConcurrentDictionaryTypeProps<TValue>.IsWriteAtomic)
{
node._value = value;
}
Expand Down Expand Up @@ -2269,6 +2238,47 @@ private sealed class DictionaryEnumerator : IDictionaryEnumerator
}
}

internal static class ConcurrentDictionaryTypeProps<T>
{
/// <summary>Whether T's type can be written atomically (i.e., with no danger of torn reads).</summary>
internal static readonly bool IsWriteAtomic = IsWriteAtomicPrivate();

private static bool IsWriteAtomicPrivate()
{
// Section 12.6.6 of ECMA CLI explains which types can be read and written atomically without
// the risk of tearing. See https://www.ecma-international.org/publications/files/ECMA-ST/ECMA-335.pdf

if (!typeof(T).IsValueType ||
typeof(T) == typeof(IntPtr) ||
typeof(T) == typeof(UIntPtr))
{
return true;
}

switch (Type.GetTypeCode(typeof(T)))
{
case TypeCode.Boolean:
case TypeCode.Byte:
case TypeCode.Char:
case TypeCode.Int16:
case TypeCode.Int32:
case TypeCode.SByte:
case TypeCode.Single:
case TypeCode.UInt16:
case TypeCode.UInt32:
return true;

case TypeCode.Double:
case TypeCode.Int64:
case TypeCode.UInt64:
return IntPtr.Size == 8;

default:
return false;
}
}
}

internal sealed class IDictionaryDebugView<TKey, TValue> where TKey : notnull
{
private readonly IDictionary<TKey, TValue> _dictionary;
Expand Down

0 comments on commit e8fa4a7

Please sign in to comment.