From ff47c088edb3c8438c372dd5e487af2691e6edb9 Mon Sep 17 00:00:00 2001 From: Ben Adams Date: Mon, 11 Dec 2017 02:19:19 +0000 Subject: [PATCH 1/4] Improve Dictionary FindEntry CQ --- .../System/Collections/Generic/Dictionary.cs | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/src/mscorlib/shared/System/Collections/Generic/Dictionary.cs b/src/mscorlib/shared/System/Collections/Generic/Dictionary.cs index 4f342752d837..9e3a483d91bf 100644 --- a/src/mscorlib/shared/System/Collections/Generic/Dictionary.cs +++ b/src/mscorlib/shared/System/Collections/Generic/Dictionary.cs @@ -362,15 +362,28 @@ 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[hashCode % buckets.Length]; + + 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 void Initialize(int capacity) From 02bf3148e1f896889ebac3f6590f28878bb49e13 Mon Sep 17 00:00:00 2001 From: Ben Adams Date: Mon, 11 Dec 2017 06:09:03 +0000 Subject: [PATCH 2/4] Improve Dictionary TryInsert CQ --- .../System/Collections/Generic/Dictionary.cs | 71 +++++++++++++------ 1 file changed, 49 insertions(+), 22 deletions(-) diff --git a/src/mscorlib/shared/System/Collections/Generic/Dictionary.cs b/src/mscorlib/shared/System/Collections/Generic/Dictionary.cs index 9e3a483d91bf..4f90aa74a32b 100644 --- a/src/mscorlib/shared/System/Collections/Generic/Dictionary.cs +++ b/src/mscorlib/shared/System/Collections/Generic/Dictionary.cs @@ -408,17 +408,27 @@ private bool TryInsert(TKey key, TValue value, InsertionBehavior behavior) } if (_buckets == null) Initialize(0); - int hashCode = _comparer.GetHashCode(key) & 0x7FFFFFFF; - int targetBucket = hashCode % _buckets.Length; + 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[hashCode % _buckets.Length]; + 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; } @@ -430,41 +440,56 @@ 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; + 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; - _version++; + ref int targetBucket = ref resized ? ref _buckets[hashCode % _buckets.Length] : ref bucket; + ref Entry entry = ref entries[index]; - // If we hit the collision threshold we'll need to switch to the comparer which is using randomized string hashing - // i.e. EqualityComparer.Default. + if (updateFreeList) + { + _freeList = entry.next; + } + entry.hashCode = hashCode; + entry.next = targetBucket; + entry.key = key; + entry.value = value; + targetBucket = index; + _version++; - if (collisionCount > HashHelpers.HashCollisionThreshold && _comparer is NonRandomizedStringEqualityComparer) + // 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 = (IEqualityComparer)EqualityComparer.Default; - Resize(_entries.Length, true); + Resize(entries.Length, true); } return true; @@ -523,6 +548,8 @@ private void Resize() private void Resize(int newSize, bool forceNewHashCodes) { + // Value types never rehash + Debug.Assert(!forceNewHashCodes || default(TKey) == null); Debug.Assert(newSize >= _entries.Length); int[] buckets = new int[newSize]; @@ -535,7 +562,7 @@ private void Resize(int newSize, bool forceNewHashCodes) int count = _count; Array.Copy(_entries, 0, entries, 0, count); - if (forceNewHashCodes) + if (default(TKey) == null && forceNewHashCodes) { for (int i = 0; i < count; i++) { From b50667f5c40d7aff6d8bcccbe7dc519dd9125a2c Mon Sep 17 00:00:00 2001 From: Ben Adams Date: Fri, 15 Dec 2017 22:07:42 +0000 Subject: [PATCH 3/4] Use EqualityComparer.Default Intrinsic --- .../System/Collections/Generic/Dictionary.cs | 183 +++++++++++++++--- 1 file changed, 154 insertions(+), 29 deletions(-) diff --git a/src/mscorlib/shared/System/Collections/Generic/Dictionary.cs b/src/mscorlib/shared/System/Collections/Generic/Dictionary.cs index 4f90aa74a32b..37d84bdc4de2 100644 --- a/src/mscorlib/shared/System/Collections/Generic/Dictionary.cs +++ b/src/mscorlib/shared/System/Collections/Generic/Dictionary.cs @@ -72,10 +72,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 +143,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 +208,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 +237,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 +247,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 +277,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 +291,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 +345,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) @@ -386,6 +387,36 @@ private int FindEntry(TKey key) 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[hashCode % buckets.Length]; + + 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); @@ -407,7 +438,10 @@ private bool TryInsert(TKey key, TValue value, InsertionBehavior behavior) ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key); } - if (_buckets == null) Initialize(0); + if (_buckets == null) + { + Initialize(0); + } IEqualityComparer comparer = _comparer; int hashCode = comparer.GetHashCode(key) & 0x7FFFFFFF; int collisionCount = 0; @@ -484,17 +518,105 @@ private bool TryInsert(TKey key, TValue value, InsertionBehavior behavior) _version++; // Value types never rehash - if (default(TKey) == null && collisionCount > HashHelpers.HashCollisionThreshold && _comparer is NonRandomizedStringEqualityComparer) + 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 = (IEqualityComparer)EqualityComparer.Default; + _comparer = null; Resize(entries.Length, 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; + + ref int bucket = ref _buckets[hashCode % _buckets.Length]; + int i = bucket; + Entry[] entries = _entries; + do + { + // 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(); + resized = true; + } + index = count; + _count = count + 1; + entries = _entries; + } + + ref int targetBucket = ref resized ? ref _buckets[hashCode % _buckets.Length] : 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; + } + public virtual void OnDeserialization(object sender) { SerializationInfo siInfo; @@ -568,7 +690,8 @@ private void Resize(int newSize, bool forceNewHashCodes) { 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); } } } @@ -599,7 +722,7 @@ public bool Remove(TKey key) if (_buckets != null) { - int hashCode = _comparer.GetHashCode(key) & 0x7FFFFFFF; + int hashCode = (_comparer?.GetHashCode(key) ?? key.GetHashCode()) & 0x7FFFFFFF; int bucket = hashCode % _buckets.Length; int last = -1; int i = _buckets[bucket]; @@ -607,7 +730,7 @@ public bool Remove(TKey key) { 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) { @@ -653,7 +776,7 @@ public bool Remove(TKey key, out TValue value) if (_buckets != null) { - int hashCode = _comparer.GetHashCode(key) & 0x7FFFFFFF; + int hashCode = (_comparer?.GetHashCode(key) ?? key.GetHashCode()) & 0x7FFFFFFF; int bucket = hashCode % _buckets.Length; int last = -1; int i = _buckets[bucket]; @@ -661,7 +784,7 @@ public bool Remove(TKey key, out TValue value) { 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) { @@ -701,7 +824,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; @@ -711,7 +834,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 { @@ -842,7 +967,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; From a8917c0b5fbb8c4be68f3782a13bd7fcec60cc4b Mon Sep 17 00:00:00 2001 From: Ben Adams Date: Tue, 19 Dec 2017 05:00:39 +0000 Subject: [PATCH 4/4] Dictionary magic remainder rather than mod --- .../System/Collections/Generic/Dictionary.cs | 60 ++++--- .../shared/System/Collections/HashHelpers.cs | 159 ++++++++++++++++++ 2 files changed, 192 insertions(+), 27 deletions(-) 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); + } } }