diff --git a/src/mscorlib/shared/System/Collections/Generic/Dictionary.cs b/src/mscorlib/shared/System/Collections/Generic/Dictionary.cs index 37d84bdc4de2..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 @@ -369,8 +372,8 @@ private int FindEntry(TKey key) { IEqualityComparer comparer = _comparer; int hashCode = comparer.GetHashCode(key) & 0x7FFFFFFF; - i = buckets[hashCode % buckets.Length]; - + i = buckets[HashHelpers.MagicNumberRemainder(hashCode, buckets.Length, _magic, _shift)]; + Entry[] entries = _entries; do { @@ -399,7 +402,7 @@ private int FindEntryDefaultComparer(TKey key) if (buckets != null) { int hashCode = key.GetHashCode() & 0x7FFFFFFF; - i = buckets[hashCode % buckets.Length]; + i = buckets[HashHelpers.MagicNumberRemainder(hashCode, buckets.Length, _magic, _shift)]; Entry[] entries = _entries; do @@ -419,8 +422,8 @@ private int FindEntryDefaultComparer(TKey key) 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; @@ -428,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) @@ -446,7 +449,7 @@ private bool TryInsert(TKey key, TValue value, InsertionBehavior behavior) int hashCode = comparer.GetHashCode(key) & 0x7FFFFFFF; int collisionCount = 0; - ref int bucket = ref _buckets[hashCode % _buckets.Length]; + ref int bucket = ref _buckets[HashHelpers.MagicNumberRemainder(hashCode, _buckets.Length, _magic, _shift)]; int i = bucket; Entry[] entries = _entries; do @@ -495,7 +498,7 @@ private bool TryInsert(TKey key, TValue value, InsertionBehavior behavior) int count = _count; if (count == entries.Length) { - Resize(); + Resize(forceNewHashCodes: false); resized = true; } index = count; @@ -503,7 +506,7 @@ private bool TryInsert(TKey key, TValue value, InsertionBehavior behavior) entries = _entries; } - ref int targetBucket = ref resized ? ref _buckets[hashCode % _buckets.Length] : ref bucket; + ref int targetBucket = ref resized ? ref _buckets[HashHelpers.MagicNumberRemainder(hashCode, _buckets.Length, _magic, _shift)] : ref bucket; ref Entry entry = ref entries[index]; if (updateFreeList) @@ -523,7 +526,7 @@ private bool TryInsert(TKey key, TValue value, InsertionBehavior behavior) // 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(entries.Length, true); + Resize(forceNewHashCodes: true); } return true; @@ -543,7 +546,7 @@ private bool TryInsertDefaultComparer(TKey key, TValue value, InsertionBehavior int hashCode = key.GetHashCode() & 0x7FFFFFFF; int collisionCount = 0; - ref int bucket = ref _buckets[hashCode % _buckets.Length]; + ref int bucket = ref _buckets[HashHelpers.MagicNumberRemainder(hashCode, _buckets.Length, _magic, _shift)]; int i = bucket; Entry[] entries = _entries; do @@ -592,7 +595,7 @@ private bool TryInsertDefaultComparer(TKey key, TValue value, InsertionBehavior int count = _count; if (count == entries.Length) { - Resize(); + Resize(forceNewHashCodes: false); resized = true; } index = count; @@ -600,7 +603,7 @@ private bool TryInsertDefaultComparer(TKey key, TValue value, InsertionBehavior entries = _entries; } - ref int targetBucket = ref resized ? ref _buckets[hashCode % _buckets.Length] : ref bucket; + ref int targetBucket = ref resized ? ref _buckets[HashHelpers.MagicNumberRemainder(hashCode, _buckets.Length, _magic, _shift)] : ref bucket; ref Entry entry = ref entries[index]; if (updateFreeList) @@ -663,25 +666,26 @@ public virtual void OnDeserialization(object sender) HashHelpers.SerializationInfoTable.Remove(this); } - private void Resize() - { - Resize(HashHelpers.ExpandPrime(_count), false); - } - - private void Resize(int newSize, bool forceNewHashCodes) + private void Resize(bool forceNewHashCodes) { // Value types never rehash - Debug.Assert(!forceNewHashCodes || default(TKey) == null); - Debug.Assert(newSize >= _entries.Length); + Debug.Assert(default(TKey) == null || !forceNewHashCodes); - int[] buckets = new int[newSize]; + 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[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 (default(TKey) == null && forceNewHashCodes) @@ -696,11 +700,13 @@ private void Resize(int newSize, bool forceNewHashCodes) } } + 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; } @@ -723,7 +729,7 @@ public bool Remove(TKey key) if (_buckets != null) { int hashCode = (_comparer?.GetHashCode(key) ?? key.GetHashCode()) & 0x7FFFFFFF; - int bucket = hashCode % _buckets.Length; + int bucket = HashHelpers.MagicNumberRemainder(hashCode, _buckets.Length, _magic, _shift); int last = -1; int i = _buckets[bucket]; while (i >= 0) @@ -777,7 +783,7 @@ public bool Remove(TKey key, out TValue value) if (_buckets != null) { int hashCode = (_comparer?.GetHashCode(key) ?? key.GetHashCode()) & 0x7FFFFFFF; - int bucket = hashCode % _buckets.Length; + int bucket = HashHelpers.MagicNumberRemainder(hashCode, _buckets.Length, _magic, _shift); int last = -1; int i = _buckets[bucket]; while (i >= 0) 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); + } } }