Skip to content
This repository was archived by the owner on Nov 1, 2020. It is now read-only.

Commit c17eeaf

Browse files
benaadamsjkotas
authored andcommitted
Avoid mod operator when fast alternative available (dotnet/coreclr#27299)
Signed-off-by: dotnet-bot <dotnet-bot@microsoft.com>
1 parent 3624e52 commit c17eeaf

File tree

2 files changed

+77
-36
lines changed

2 files changed

+77
-36
lines changed

src/System.Private.CoreLib/shared/System/Collections/Generic/Dictionary.cs

Lines changed: 52 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@ private struct Entry
5151

5252
private int[]? _buckets;
5353
private Entry[]? _entries;
54+
#if BIT64
55+
private ulong _fastModMultiplier;
56+
#endif
5457
private int _count;
5558
private int _freeList;
5659
private int _freeCount;
@@ -330,16 +333,15 @@ private ref TValue FindValue(TKey key)
330333
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key);
331334
}
332335

333-
int[]? buckets = _buckets;
334336
ref Entry entry = ref Unsafe.NullRef<Entry>();
335-
if (buckets != null)
337+
if (_buckets != null)
336338
{
337339
Debug.Assert(_entries != null, "expected entries to be != null");
338340
IEqualityComparer<TKey>? comparer = _comparer;
339341
if (comparer == null)
340342
{
341343
uint hashCode = (uint)key.GetHashCode();
342-
int i = buckets[hashCode % (uint)buckets.Length];
344+
int i = GetBucket(hashCode);
343345
Entry[]? entries = _entries;
344346
uint collisionCount = 0;
345347
if (default(TKey)! != null) // TODO-NULLABLE: default(T) == null warning (https://github.com/dotnet/roslyn/issues/34757)
@@ -407,7 +409,7 @@ private ref TValue FindValue(TKey key)
407409
else
408410
{
409411
uint hashCode = (uint)comparer.GetHashCode(key);
410-
int i = buckets[hashCode % (uint)buckets.Length];
412+
int i = GetBucket(hashCode);
411413
Entry[]? entries = _entries;
412414
uint collisionCount = 0;
413415
// Value in _buckets is 1-based; subtract 1 from i. We do it here so it fuses with the following conditional.
@@ -453,10 +455,16 @@ private ref TValue FindValue(TKey key)
453455
private int Initialize(int capacity)
454456
{
455457
int size = HashHelpers.GetPrime(capacity);
458+
int[] buckets = new int[size];
459+
Entry[] entries = new Entry[size];
456460

461+
// Assign member variables after both arrays allocated to guard against corruption from OOM if second fails
457462
_freeList = -1;
458-
_buckets = new int[size];
459-
_entries = new Entry[size];
463+
#if BIT64
464+
_fastModMultiplier = HashHelpers.GetFastModMultiplier((uint)size);
465+
#endif
466+
_buckets = buckets;
467+
_entries = entries;
460468

461469
return size;
462470
}
@@ -481,7 +489,7 @@ private bool TryInsert(TKey key, TValue value, InsertionBehavior behavior)
481489
uint hashCode = (uint)((comparer == null) ? key.GetHashCode() : comparer.GetHashCode(key));
482490

483491
uint collisionCount = 0;
484-
ref int bucket = ref _buckets[hashCode % (uint)_buckets.Length];
492+
ref int bucket = ref GetBucket(hashCode);
485493
// Value in _buckets is 1-based
486494
int i = bucket - 1;
487495

@@ -625,7 +633,7 @@ private bool TryInsert(TKey key, TValue value, InsertionBehavior behavior)
625633
if (count == entries.Length)
626634
{
627635
Resize();
628-
bucket = ref _buckets[hashCode % (uint)_buckets.Length];
636+
bucket = ref GetBucket(hashCode);
629637
}
630638
index = count;
631639
_count = count + 1;
@@ -716,7 +724,6 @@ private void Resize(int newSize, bool forceNewHashCodes)
716724
Debug.Assert(_entries != null, "_entries should be non-null");
717725
Debug.Assert(newSize >= _entries.Length);
718726

719-
int[] buckets = new int[newSize];
720727
Entry[] entries = new Entry[newSize];
721728

722729
int count = _count;
@@ -734,19 +741,23 @@ private void Resize(int newSize, bool forceNewHashCodes)
734741
}
735742
}
736743

744+
// Assign member variables after both arrays allocated to guard against corruption from OOM if second fails
745+
_buckets = new int[newSize];
746+
#if BIT64
747+
_fastModMultiplier = HashHelpers.GetFastModMultiplier((uint)newSize);
748+
#endif
737749
for (int i = 0; i < count; i++)
738750
{
739751
if (entries[i].next >= -1)
740752
{
741-
uint bucket = entries[i].hashCode % (uint)newSize;
753+
ref int bucket = ref GetBucket(entries[i].hashCode);
742754
// Value in _buckets is 1-based
743-
entries[i].next = buckets[bucket] - 1;
755+
entries[i].next = bucket - 1;
744756
// Value in _buckets is 1-based
745-
buckets[bucket] = i + 1;
757+
bucket = i + 1;
746758
}
747759
}
748760

749-
_buckets = buckets;
750761
_entries = entries;
751762
}
752763

@@ -760,17 +771,16 @@ public bool Remove(TKey key)
760771
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key);
761772
}
762773

763-
int[]? buckets = _buckets;
764-
Entry[]? entries = _entries;
765-
if (buckets != null)
774+
if (_buckets != null)
766775
{
767-
Debug.Assert(entries != null, "entries should be non-null");
776+
Debug.Assert(_entries != null, "entries should be non-null");
768777
uint collisionCount = 0;
769778
uint hashCode = (uint)(_comparer?.GetHashCode(key) ?? key.GetHashCode());
770-
uint bucket = hashCode % (uint)buckets.Length;
779+
ref int bucket = ref GetBucket(hashCode);
780+
Entry[]? entries = _entries;
771781
int last = -1;
772782
// Value in buckets is 1-based
773-
int i = buckets[bucket] - 1;
783+
int i = bucket - 1;
774784
while (i >= 0)
775785
{
776786
ref Entry entry = ref entries[i];
@@ -780,7 +790,7 @@ public bool Remove(TKey key)
780790
if (last < 0)
781791
{
782792
// Value in buckets is 1-based
783-
buckets[bucket] = entry.next + 1;
793+
bucket = entry.next + 1;
784794
}
785795
else
786796
{
@@ -829,17 +839,16 @@ public bool Remove(TKey key, [MaybeNullWhen(false)] out TValue value)
829839
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key);
830840
}
831841

832-
int[]? buckets = _buckets;
833-
Entry[]? entries = _entries;
834-
if (buckets != null)
842+
if (_buckets != null)
835843
{
836-
Debug.Assert(entries != null, "entries should be non-null");
844+
Debug.Assert(_entries != null, "entries should be non-null");
837845
uint collisionCount = 0;
838846
uint hashCode = (uint)(_comparer?.GetHashCode(key) ?? key.GetHashCode());
839-
uint bucket = hashCode % (uint)buckets.Length;
847+
ref int bucket = ref GetBucket(hashCode);
848+
Entry[]? entries = _entries;
840849
int last = -1;
841850
// Value in buckets is 1-based
842-
int i = buckets[bucket] - 1;
851+
int i = bucket - 1;
843852
while (i >= 0)
844853
{
845854
ref Entry entry = ref entries[i];
@@ -849,7 +858,7 @@ public bool Remove(TKey key, [MaybeNullWhen(false)] out TValue value)
849858
if (last < 0)
850859
{
851860
// Value in buckets is 1-based
852-
buckets[bucket] = entry.next + 1;
861+
bucket = entry.next + 1;
853862
}
854863
else
855864
{
@@ -982,6 +991,7 @@ public int EnsureCapacity(int capacity)
982991
_version++;
983992
if (_buckets == null)
984993
return Initialize(capacity);
994+
985995
int newSize = HashHelpers.GetPrime(capacity);
986996
Resize(newSize, forceNewHashCodes: false);
987997
return newSize;
@@ -1011,8 +1021,8 @@ public void TrimExcess(int capacity)
10111021
{
10121022
if (capacity < Count)
10131023
ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.capacity);
1014-
int newSize = HashHelpers.GetPrime(capacity);
10151024

1025+
int newSize = HashHelpers.GetPrime(capacity);
10161026
Entry[]? oldEntries = _entries;
10171027
int currentCapacity = oldEntries == null ? 0 : oldEntries.Length;
10181028
if (newSize >= currentCapacity)
@@ -1022,7 +1032,6 @@ public void TrimExcess(int capacity)
10221032
_version++;
10231033
Initialize(newSize);
10241034
Entry[]? entries = _entries;
1025-
int[]? buckets = _buckets;
10261035
int count = 0;
10271036
for (int i = 0; i < oldCount; i++)
10281037
{
@@ -1031,11 +1040,11 @@ public void TrimExcess(int capacity)
10311040
{
10321041
ref Entry entry = ref entries![count];
10331042
entry = oldEntries[i];
1034-
uint bucket = hashCode % (uint)newSize;
1043+
ref int bucket = ref GetBucket(hashCode);
10351044
// Value in _buckets is 1-based
1036-
entry.next = buckets![bucket] - 1; // If we get here, we have entries, therefore buckets is not null.
1045+
entry.next = bucket - 1;
10371046
// Value in _buckets is 1-based
1038-
buckets[bucket] = count + 1;
1047+
bucket = count + 1;
10391048
count++;
10401049
}
10411050
}
@@ -1153,6 +1162,17 @@ void IDictionary.Remove(object key)
11531162
}
11541163
}
11551164

1165+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
1166+
private ref int GetBucket(uint hashCode)
1167+
{
1168+
int[] buckets = _buckets!;
1169+
#if BIT64
1170+
return ref buckets[HashHelpers.FastMod(hashCode, (uint)buckets.Length, _fastModMultiplier)];
1171+
#else
1172+
return ref buckets[hashCode % (uint)buckets.Length];
1173+
#endif
1174+
}
1175+
11561176
public struct Enumerator : IEnumerator<KeyValuePair<TKey, TValue>>,
11571177
IDictionaryEnumerator
11581178
{

src/System.Private.CoreLib/shared/System/Collections/HashHelpers.cs

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// See the LICENSE file in the project root for more information.
44

55
using System.Diagnostics;
6+
using System.Runtime.CompilerServices;
67

78
namespace System.Collections
89
{
@@ -28,12 +29,14 @@ internal static partial class HashHelpers
2829
// h1(key) + i*h2(key), 0 <= i < size. h2 and the size must be relatively prime.
2930
// We prefer the low computation costs of higher prime numbers over the increased
3031
// memory allocation of a fixed prime number i.e. when right sizing a HashSet.
31-
public static readonly int[] primes = {
32+
private static readonly int[] s_primes =
33+
{
3234
3, 7, 11, 17, 23, 29, 37, 47, 59, 71, 89, 107, 131, 163, 197, 239, 293, 353, 431, 521, 631, 761, 919,
3335
1103, 1327, 1597, 1931, 2333, 2801, 3371, 4049, 4861, 5839, 7013, 8419, 10103, 12143, 14591,
3436
17519, 21023, 25229, 30293, 36353, 43627, 52361, 62851, 75431, 90523, 108631, 130363, 156437,
3537
187751, 225307, 270371, 324449, 389357, 467237, 560689, 672827, 807403, 968897, 1162687, 1395263,
36-
1674319, 2009191, 2411033, 2893249, 3471899, 4166287, 4999559, 5999471, 7199369 };
38+
1674319, 2009191, 2411033, 2893249, 3471899, 4166287, 4999559, 5999471, 7199369
39+
};
3740

3841
public static bool IsPrime(int candidate)
3942
{
@@ -55,9 +58,8 @@ public static int GetPrime(int min)
5558
if (min < 0)
5659
throw new ArgumentException(SR.Arg_HTCapacityOverflow);
5760

58-
for (int i = 0; i < primes.Length; i++)
61+
foreach (int prime in s_primes)
5962
{
60-
int prime = primes[i];
6163
if (prime >= min)
6264
return prime;
6365
}
@@ -86,5 +88,24 @@ public static int ExpandPrime(int oldSize)
8688

8789
return GetPrime(newSize);
8890
}
91+
92+
#if BIT64
93+
public static ulong GetFastModMultiplier(uint divisor)
94+
=> ulong.MaxValue / divisor + 1;
95+
96+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
97+
public static uint FastMod(uint value, uint divisor, ulong multiplier)
98+
{
99+
// Using fastmod from Daniel Lemire https://lemire.me/blog/2019/02/08/faster-remainders-when-the-divisor-is-a-constant-beating-compilers-and-libdivide/
100+
101+
ulong lowbits = multiplier * value;
102+
// 64bit * 64bit => 128bit isn't currently supported by Math https://github.com/dotnet/corefx/issues/41822
103+
// otherwise we'd want this to be (uint)Math.MultiplyHigh(lowbits, divisor)
104+
uint high = (uint)((((ulong)(uint)lowbits * divisor >> 32) + (lowbits >> 32) * divisor) >> 32);
105+
106+
Debug.Assert(high == value % divisor);
107+
return high;
108+
}
109+
#endif
89110
}
90111
}

0 commit comments

Comments
 (0)