diff --git a/src/mscorlib/shared/System/Collections/Generic/Dictionary.cs b/src/mscorlib/shared/System/Collections/Generic/Dictionary.cs index 4f342752d837..9198a53dc165 100644 --- a/src/mscorlib/shared/System/Collections/Generic/Dictionary.cs +++ b/src/mscorlib/shared/System/Collections/Generic/Dictionary.cs @@ -56,6 +56,9 @@ private struct Entry private ValueCollection _values; private object _syncRoot; + uint _magic; + int _shift; + // constants for serialization private const string VersionName = "Version"; // Do not rename (binary serialization) private const string HashSizeName = "HashSize"; // Do not rename (binary serialization). Must save buckets.Length @@ -72,10 +75,14 @@ public Dictionary(int capacity, IEqualityComparer comparer) { if (capacity < 0) ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.capacity); if (capacity > 0) Initialize(capacity); - _comparer = comparer ?? EqualityComparer.Default; + if (comparer != EqualityComparer.Default) + { + _comparer = comparer; + } - if (_comparer == EqualityComparer.Default) + if (typeof(TKey) == typeof(string) && _comparer == null) { + // To start, move off default comparer for string which is randomised _comparer = (IEqualityComparer)NonRandomizedStringEqualityComparer.Default; } } @@ -139,13 +146,7 @@ protected Dictionary(SerializationInfo info, StreamingContext context) HashHelpers.SerializationInfoTable.Add(this, info); } - public IEqualityComparer Comparer - { - get - { - return _comparer; - } - } + public IEqualityComparer Comparer => _comparer ?? EqualityComparer.Default; public int Count { @@ -210,21 +211,25 @@ public TValue this[TKey key] { get { - int i = FindEntry(key); + int i = _comparer == null ? FindEntryDefaultComparer(key) : FindEntry(key); if (i >= 0) return _entries[i].value; ThrowHelper.ThrowKeyNotFoundException(key); return default(TValue); } set { - bool modified = TryInsert(key, value, InsertionBehavior.OverwriteExisting); + bool modified = _comparer == null ? + TryInsertDefaultComparer(key, value, InsertionBehavior.OverwriteExisting) : + TryInsert(key, value, InsertionBehavior.OverwriteExisting); Debug.Assert(modified); } } public void Add(TKey key, TValue value) { - bool modified = TryInsert(key, value, InsertionBehavior.ThrowOnExisting); + bool modified = _comparer == null ? + TryInsertDefaultComparer(key, value, InsertionBehavior.ThrowOnExisting) : + TryInsert(key, value, InsertionBehavior.ThrowOnExisting); Debug.Assert(modified); // If there was an existing key and the Add failed, an exception will already have been thrown. } @@ -235,7 +240,7 @@ void ICollection>.Add(KeyValuePair keyV bool ICollection>.Contains(KeyValuePair keyValuePair) { - int i = FindEntry(keyValuePair.Key); + int i = _comparer == null ? FindEntryDefaultComparer(keyValuePair.Key) : FindEntry(keyValuePair.Key); if (i >= 0 && EqualityComparer.Default.Equals(_entries[i].value, keyValuePair.Value)) { return true; @@ -245,7 +250,7 @@ bool ICollection>.Contains(KeyValuePair bool ICollection>.Remove(KeyValuePair keyValuePair) { - int i = FindEntry(keyValuePair.Key); + int i = _comparer == null ? FindEntryDefaultComparer(keyValuePair.Key) : FindEntry(keyValuePair.Key); if (i >= 0 && EqualityComparer.Default.Equals(_entries[i].value, keyValuePair.Value)) { Remove(keyValuePair.Key); @@ -275,7 +280,7 @@ public void Clear() public bool ContainsKey(TKey key) { - return FindEntry(key) >= 0; + return (_comparer == null ? FindEntryDefaultComparer(key) : FindEntry(key)) >= 0; } public bool ContainsValue(TValue value) @@ -289,10 +294,9 @@ public bool ContainsValue(TValue value) } else { - EqualityComparer c = EqualityComparer.Default; for (int i = 0; i < _count; i++) { - if (_entries[i].hashCode >= 0 && c.Equals(_entries[i].value, value)) return true; + if (_entries[i].hashCode >= 0 && EqualityComparer.Default.Equals(_entries[i].value, value)) return true; } } return false; @@ -344,7 +348,7 @@ public virtual void GetObjectData(SerializationInfo info, StreamingContext conte } info.AddValue(VersionName, _version); - info.AddValue(ComparerName, _comparer, typeof(IEqualityComparer)); + info.AddValue(ComparerName, _comparer ?? EqualityComparer.Default, typeof(IEqualityComparer)); info.AddValue(HashSizeName, _buckets == null ? 0 : _buckets.Length); // This is the length of the bucket array if (_buckets != null) @@ -362,21 +366,64 @@ private int FindEntry(TKey key) ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key); } - if (_buckets != null) + int[] buckets = _buckets; + int i = -1; + if (buckets != null) { - int hashCode = _comparer.GetHashCode(key) & 0x7FFFFFFF; - for (int i = _buckets[hashCode % _buckets.Length]; i >= 0; i = _entries[i].next) + IEqualityComparer comparer = _comparer; + int hashCode = comparer.GetHashCode(key) & 0x7FFFFFFF; + i = buckets[HashHelpers.MagicNumberRemainder(hashCode, buckets.Length, _magic, _shift)]; + + Entry[] entries = _entries; + do { - if (_entries[i].hashCode == hashCode && _comparer.Equals(_entries[i].key, key)) return i; - } + // Should be a while loop https://github.com/dotnet/coreclr/issues/15476 + // Test in if to drop range check for following array access + if ((uint)i >= (uint)entries.Length || (entries[i].hashCode == hashCode && comparer.Equals(entries[i].key, key))) + { + break; + } + + i = entries[i].next; + } while (true); } - return -1; + return i; + } + + private int FindEntryDefaultComparer(TKey key) + { + if (key == null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key); + } + + int[] buckets = _buckets; + int i = -1; + if (buckets != null) + { + int hashCode = key.GetHashCode() & 0x7FFFFFFF; + i = buckets[HashHelpers.MagicNumberRemainder(hashCode, buckets.Length, _magic, _shift)]; + + Entry[] entries = _entries; + do + { + // Should be a while loop https://github.com/dotnet/coreclr/issues/15476 + // Test in if to drop range check for following array access + if ((uint)i >= (uint)entries.Length || (entries[i].hashCode == hashCode && EqualityComparer.Default.Equals(entries[i].key, key))) + { + break; + } + + i = entries[i].next; + } while (true); + } + return i; } private void Initialize(int capacity) { - int size = HashHelpers.GetPrime(capacity); - int[] buckets = new int[size]; + HashHelpers.NearestPrimeInfo(capacity, out int prime, out _magic, out _shift); + int[] buckets = new int[prime]; for (int i = 0; i < buckets.Length; i++) { buckets[i] = -1; @@ -384,7 +431,7 @@ private void Initialize(int capacity) _freeList = -1; _buckets = buckets; - _entries = new Entry[size]; + _entries = new Entry[prime]; } private bool TryInsert(TKey key, TValue value, InsertionBehavior behavior) @@ -394,18 +441,31 @@ private bool TryInsert(TKey key, TValue value, InsertionBehavior behavior) ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key); } - if (_buckets == null) Initialize(0); - int hashCode = _comparer.GetHashCode(key) & 0x7FFFFFFF; - int targetBucket = hashCode % _buckets.Length; + if (_buckets == null) + { + Initialize(0); + } + IEqualityComparer comparer = _comparer; + int hashCode = comparer.GetHashCode(key) & 0x7FFFFFFF; int collisionCount = 0; - for (int i = _buckets[targetBucket]; i >= 0; i = _entries[i].next) + ref int bucket = ref _buckets[HashHelpers.MagicNumberRemainder(hashCode, _buckets.Length, _magic, _shift)]; + int i = bucket; + Entry[] entries = _entries; + do { - if (_entries[i].hashCode == hashCode && _comparer.Equals(_entries[i].key, key)) + // Should be a while loop https://github.com/dotnet/coreclr/issues/15476 + // Test uint in if rather than loop condition to drop range check for following array access + if ((uint)i >= (uint)entries.Length) + { + break; + } + + if (entries[i].hashCode == hashCode && comparer.Equals(entries[i].key, key)) { if (behavior == InsertionBehavior.OverwriteExisting) { - _entries[i].value = value; + entries[i].value = value; _version++; return true; } @@ -417,42 +477,145 @@ private bool TryInsert(TKey key, TValue value, InsertionBehavior behavior) return false; } + + i = entries[i].next; collisionCount++; - } + } while (true); + // Can be improved with "Ref Local Reassignment" + // https://github.com/dotnet/csharplang/blob/master/proposals/ref-local-reassignment.md + bool resized = false; + bool updateFreeList = false; int index; if (_freeCount > 0) { index = _freeList; - _freeList = _entries[index].next; + updateFreeList = true; _freeCount--; } else { - if (_count == _entries.Length) + int count = _count; + if (count == entries.Length) { - Resize(); - targetBucket = hashCode % _buckets.Length; + Resize(forceNewHashCodes: false); + resized = true; } - index = _count; - _count++; + index = count; + _count = count + 1; + entries = _entries; } - _entries[index].hashCode = hashCode; - _entries[index].next = _buckets[targetBucket]; - _entries[index].key = key; - _entries[index].value = value; - _buckets[targetBucket] = index; + ref int targetBucket = ref resized ? ref _buckets[HashHelpers.MagicNumberRemainder(hashCode, _buckets.Length, _magic, _shift)] : ref bucket; + ref Entry entry = ref entries[index]; + + if (updateFreeList) + { + _freeList = entry.next; + } + entry.hashCode = hashCode; + entry.next = targetBucket; + entry.key = key; + entry.value = value; + targetBucket = index; _version++; - // If we hit the collision threshold we'll need to switch to the comparer which is using randomized string hashing - // i.e. EqualityComparer.Default. + // Value types never rehash + if (default(TKey) == null && collisionCount > HashHelpers.HashCollisionThreshold && comparer is NonRandomizedStringEqualityComparer) + { + // If we hit the collision threshold we'll need to switch to the comparer which is using randomized string hashing + // i.e. EqualityComparer.Default. + _comparer = null; + Resize(forceNewHashCodes: true); + } + + return true; + } + + private bool TryInsertDefaultComparer(TKey key, TValue value, InsertionBehavior behavior) + { + if (key == null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key); + } + + if (_buckets == null) + { + Initialize(0); + } + int hashCode = key.GetHashCode() & 0x7FFFFFFF; + int collisionCount = 0; - if (collisionCount > HashHelpers.HashCollisionThreshold && _comparer is NonRandomizedStringEqualityComparer) + ref int bucket = ref _buckets[HashHelpers.MagicNumberRemainder(hashCode, _buckets.Length, _magic, _shift)]; + int i = bucket; + Entry[] entries = _entries; + do { - _comparer = (IEqualityComparer)EqualityComparer.Default; - Resize(_entries.Length, true); + // Should be a while loop https://github.com/dotnet/coreclr/issues/15476 + // Test uint in if rather than loop condition to drop range check for following array access + if ((uint)i >= (uint)entries.Length) + { + break; + } + + if (entries[i].hashCode == hashCode && EqualityComparer.Default.Equals(entries[i].key, key)) + { + if (behavior == InsertionBehavior.OverwriteExisting) + { + entries[i].value = value; + _version++; + return true; + } + + if (behavior == InsertionBehavior.ThrowOnExisting) + { + ThrowHelper.ThrowAddingDuplicateWithKeyArgumentException(key); + } + + return false; + } + + i = entries[i].next; + collisionCount++; + } while (true); + + // Can be improved with "Ref Local Reassignment" + // https://github.com/dotnet/csharplang/blob/master/proposals/ref-local-reassignment.md + bool resized = false; + bool updateFreeList = false; + int index; + if (_freeCount > 0) + { + index = _freeList; + updateFreeList = true; + _freeCount--; + } + else + { + int count = _count; + if (count == entries.Length) + { + Resize(forceNewHashCodes: false); + resized = true; + } + index = count; + _count = count + 1; + entries = _entries; + } + + ref int targetBucket = ref resized ? ref _buckets[HashHelpers.MagicNumberRemainder(hashCode, _buckets.Length, _magic, _shift)] : ref bucket; + ref Entry entry = ref entries[index]; + + if (updateFreeList) + { + _freeList = entry.next; } + entry.hashCode = hashCode; + entry.next = targetBucket; + entry.key = key; + entry.value = value; + targetBucket = index; + _version++; return true; } @@ -503,41 +666,47 @@ public virtual void OnDeserialization(object sender) HashHelpers.SerializationInfoTable.Remove(this); } - private void Resize() + private void Resize(bool forceNewHashCodes) { - Resize(HashHelpers.ExpandPrime(_count), false); - } + // Value types never rehash + Debug.Assert(default(TKey) == null || !forceNewHashCodes); - private void Resize(int newSize, bool forceNewHashCodes) - { - Debug.Assert(newSize >= _entries.Length); + int count = _count; + int prime = count; + if (default(TKey) != null || !forceNewHashCodes) + { + HashHelpers.ExpandPrimeInfo(prime, out prime, out _magic, out _shift); + Debug.Assert(prime > _entries.Length); + } - int[] buckets = new int[newSize]; + int[] buckets = new int[prime]; for (int i = 0; i < buckets.Length; i++) { buckets[i] = -1; } - Entry[] entries = new Entry[newSize]; + Entry[] entries = new Entry[prime]; - int count = _count; Array.Copy(_entries, 0, entries, 0, count); - if (forceNewHashCodes) + if (default(TKey) == null && forceNewHashCodes) { for (int i = 0; i < count; i++) { if (entries[i].hashCode != -1) { - entries[i].hashCode = (_comparer.GetHashCode(entries[i].key) & 0x7FFFFFFF); + Debug.Assert(_comparer == null); + entries[i].hashCode = (entries[i].key.GetHashCode() & 0x7FFFFFFF); } } } + uint magic = _magic; + int shift = _shift; for (int i = 0; i < count; i++) { if (entries[i].hashCode >= 0) { - int bucket = entries[i].hashCode % newSize; + int bucket = HashHelpers.MagicNumberRemainder(entries[i].hashCode, prime, magic, shift); entries[i].next = buckets[bucket]; buckets[bucket] = i; } @@ -559,15 +728,15 @@ public bool Remove(TKey key) if (_buckets != null) { - int hashCode = _comparer.GetHashCode(key) & 0x7FFFFFFF; - int bucket = hashCode % _buckets.Length; + int hashCode = (_comparer?.GetHashCode(key) ?? key.GetHashCode()) & 0x7FFFFFFF; + int bucket = HashHelpers.MagicNumberRemainder(hashCode, _buckets.Length, _magic, _shift); int last = -1; int i = _buckets[bucket]; while (i >= 0) { ref Entry entry = ref _entries[i]; - if (entry.hashCode == hashCode && _comparer.Equals(entry.key, key)) + if (entry.hashCode == hashCode && (_comparer?.Equals(entry.key, key) ?? EqualityComparer.Default.Equals(entry.key, key))) { if (last < 0) { @@ -613,15 +782,15 @@ public bool Remove(TKey key, out TValue value) if (_buckets != null) { - int hashCode = _comparer.GetHashCode(key) & 0x7FFFFFFF; - int bucket = hashCode % _buckets.Length; + int hashCode = (_comparer?.GetHashCode(key) ?? key.GetHashCode()) & 0x7FFFFFFF; + int bucket = HashHelpers.MagicNumberRemainder(hashCode, _buckets.Length, _magic, _shift); int last = -1; int i = _buckets[bucket]; while (i >= 0) { ref Entry entry = ref _entries[i]; - if (entry.hashCode == hashCode && _comparer.Equals(entry.key, key)) + if (entry.hashCode == hashCode && (_comparer?.Equals(entry.key, key) ?? EqualityComparer.Default.Equals(entry.key, key))) { if (last < 0) { @@ -661,7 +830,7 @@ public bool Remove(TKey key, out TValue value) public bool TryGetValue(TKey key, out TValue value) { - int i = FindEntry(key); + int i = _comparer == null ? FindEntryDefaultComparer(key) : FindEntry(key); if (i >= 0) { value = _entries[i].value; @@ -671,7 +840,9 @@ public bool TryGetValue(TKey key, out TValue value) return false; } - public bool TryAdd(TKey key, TValue value) => TryInsert(key, value, InsertionBehavior.None); + public bool TryAdd(TKey key, TValue value) => _comparer == null ? + TryInsertDefaultComparer(key, value, InsertionBehavior.None) : + TryInsert(key, value, InsertionBehavior.None); bool ICollection>.IsReadOnly { @@ -802,7 +973,7 @@ object IDictionary.this[object key] { if (IsCompatibleKey(key)) { - int i = FindEntry((TKey)key); + int i = _comparer == null ? FindEntryDefaultComparer((TKey)key) : FindEntry((TKey)key); if (i >= 0) { return _entries[i].value; diff --git a/src/mscorlib/shared/System/Collections/HashHelpers.cs b/src/mscorlib/shared/System/Collections/HashHelpers.cs index 49cff85b5874..56270b028de9 100644 --- a/src/mscorlib/shared/System/Collections/HashHelpers.cs +++ b/src/mscorlib/shared/System/Collections/HashHelpers.cs @@ -4,8 +4,10 @@ using System.Diagnostics; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using System.Runtime.Serialization; using System.Threading; +using Internal.Runtime.CompilerServices; namespace System.Collections { @@ -33,6 +35,92 @@ internal static class HashHelpers 187751, 225307, 270371, 324449, 389357, 467237, 560689, 672827, 807403, 968897, 1162687, 1395263, 1674319, 2009191, 2411033, 2893249, 3471899, 4166287, 4999559, 5999471, 7199369}; + public static readonly uint[] PrimeMagicShift = + { + // prime, magic multiplier, magic shift + 3, 0x55555556, 32, + 7, 0x92492493, 34, + 11, 0x2e8ba2e9, 33, + 17, 0x78787879, 35, + 23, 0xb21642c9, 36, + 29, 0x8d3dcb09, 36, + 37, 0xdd67c8a7, 37, + 47, 0xae4c415d, 37, + 59, 0x22b63cbf, 35, + 71, 0xe6c2b449, 38, + 89, 0xb81702e1, 38, + 107, 0x4c8f8d29, 37, + 131, 0x3e88cb3d, 37, + 163, 0x0c907da5, 35, + 197, 0x532ae21d, 38, + 239, 0x891ac73b, 39, + 293, 0xdfac1f75, 40, + 353, 0xb9a7862b, 40, + 431, 0x980e4157, 40, + 521, 0x3ee4f99d, 39, + 631, 0x33ee2623, 39, + 761, 0x561e46a5, 40, + 919, 0x8e9fe543, 41, + 1103, 0x1db54401, 39, + 1327, 0xc58bdd47, 42, + 1597, 0xa425d4b9, 42, + 1931, 0x10f82d9b, 39, + 2333, 0x705d0d0f, 42, + 2801, 0x2ecb7285, 41, + 3371, 0x9b876783, 43, + 4049, 0x817c5d53, 43, + 4861, 0x35ed914d, 42, + 5839, 0x0b394d8f, 40, + 7013, 0x9584d635, 44, + 8419, 0x7c8c7b75, 44, + 10103, 0x33e4f01d, 43, + 12143, 0x565a3073, 44, + 14591, 0x23eeaa5d, 43, + 17519, 0x77b510e9, 45, + 21023, 0x63c14fe5, 45, + 25229, 0x531fe999, 45, + 30293, 0x8a75366b, 46, + 36353, 0xe6c11447, 47, + 43627, 0xc047bac3, 47, + 52361, 0x0a035099, 43, + 62851, 0x42bbed05, 46, + 75431, 0x379ac159, 46, + 90523, 0x05cab127, 43, + 108631, 0x9a713743, 48, + 130363, 0x80b236c9, 48, + 156437, 0x6b3eeec1, 48, + 187751, 0xb2b7bcf9, 49, + 225307, 0x4a76bbc7, 48, + 270371, 0x7c1aeabf, 49, + 324449, 0x676b743d, 49, + 389357, 0x2b16ec6d, 48, + 467237, 0x8fa1117f, 50, + 560689, 0x77b0a38f, 50, + 672827, 0x63bddbb1, 50, + 807403, 0x0531def9, 46, + 968897, 0x8a86bc61, 51, + 1162687, 0x737002ad, 51, + 1395263, 0x180c7f9f, 49, + 1674319, 0x140a67af, 49, + 2009191, 0x42cd47bf, 51, + 2411033, 0x37ab0b5f, 51, + 2893249, 0x5cc7a9dd, 52, + 3471899, 0x4d510d43, 52, + 4166287, 0x080dc5ad, 49, + 4999559, 0x035b11b9, 48, + 5999471, 0xb2f90627, 54, + 7199369, 0x9524d54d, 54, + 14398753, 0x4a92658f, 54, + 28797523, 0x4a9262ad, 55, + 57595063, 0x9524c277, 57, + 115190149, 0x9524c083, 58, + 230380307, 0x4a926011, 58, + 460760623, 0x9524bff1, 60, + 921521257, 0x9524bfd3, 61, + 1843042529, 0x4a925fdf, 61, + 2146435069, 0x20040081, 60 + }; + public static bool IsPrime(int candidate) { if ((candidate & 1) != 0) @@ -104,5 +192,76 @@ internal static ConditionalWeakTable SerializationInf return s_serializationInfoTable; } } + + // To implement magic-number divide with a 32-bit magic number, + // multiply by the magic number, take the top 64 bits, and shift that + // by the amount given in the table. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint MagicNumberDivide(uint numerator, uint magic, int shift) + { + return (uint)((numerator * (ulong)magic) >> shift); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int MagicNumberRemainder(int numerator, int divisor, uint magic, int shift) + { + Debug.Assert(numerator >= 0); + uint product = MagicNumberDivide((uint)numerator, magic, shift); + Debug.Assert(product == numerator / divisor); + int result = (int)(numerator - (product * divisor)); + Debug.Assert(result == numerator % divisor); + return result; + } + + [StructLayout(LayoutKind.Sequential)] + public readonly struct PrimeInfo + { + public readonly int Prime; + public readonly uint Magic; + public readonly int Shift; + } + + public static void NearestPrimeInfo(int min, out int prime, out uint magic, out int shift) + { + if (min < 0) + { + throw new ArgumentException(SR.Arg_HTCapacityOverflow); + } + + uint[] primeMagicShift = PrimeMagicShift; + for (int i = 0; i < primeMagicShift.Length; i += 3) + { + ref uint primeRef = ref primeMagicShift[i]; + if (primeRef >= min) + { + PrimeInfo primeInfo = Unsafe.As(ref primeRef); + prime = primeInfo.Prime; + magic = primeInfo.Magic; + shift = primeInfo.Shift; + return; + } + } + + throw new ArgumentException(SR.Arg_HTCapacityOverflow); + } + + public static void ExpandPrimeInfo(int oldSize, out int prime, out uint magic, out int shift) + { + int newSize = 2 * oldSize; + // Allow the hashtables to grow to maximum possible size (~2G elements) before encoutering capacity overflow. + // Note that this check works even when _items.Length overflowed thanks to the (uint) cast + if ((uint)newSize > MaxPrimeArrayLength && MaxPrimeArrayLength > oldSize) + { + Debug.Assert(MaxPrimeArrayLength == GetPrime(MaxPrimeArrayLength), "Invalid MaxPrimeArrayLength"); + + PrimeInfo primeInfo = Unsafe.As(ref PrimeMagicShift[PrimeMagicShift.Length - 3]); + prime = primeInfo.Prime; + magic = primeInfo.Magic; + shift = primeInfo.Shift; + return; + } + + NearestPrimeInfo(newSize, out prime, out magic, out shift); + } } }