diff --git a/src/coreclr/nativeaot/System.Private.Reflection.Execution/src/System.Private.Reflection.Execution.csproj b/src/coreclr/nativeaot/System.Private.Reflection.Execution/src/System.Private.Reflection.Execution.csproj
index 5648a7deed20c2..f208fd2d0a573e 100644
--- a/src/coreclr/nativeaot/System.Private.Reflection.Execution/src/System.Private.Reflection.Execution.csproj
+++ b/src/coreclr/nativeaot/System.Private.Reflection.Execution/src/System.Private.Reflection.Execution.csproj
@@ -67,7 +67,7 @@
Internal\LowLevelLinq\LowLevelEnumerable.ToArray.cs
-
+
System\Collections\HashHelpers.cs
diff --git a/src/libraries/System.Private.CoreLib/src/System/Collections/HashHelpers.cs b/src/libraries/Common/src/System/Collections/HashHelpers.cs
similarity index 59%
rename from src/libraries/System.Private.CoreLib/src/System/Collections/HashHelpers.cs
rename to src/libraries/Common/src/System/Collections/HashHelpers.cs
index c64f58145ca10d..76ff750a1c1418 100644
--- a/src/libraries/System.Private.CoreLib/src/System/Collections/HashHelpers.cs
+++ b/src/libraries/Common/src/System/Collections/HashHelpers.cs
@@ -14,6 +14,7 @@ internal static partial class HashHelpers
public const int MaxPrimeArrayLength = 0x7FFFFFC3;
public const int HashPrime = 101;
+ private const int MinPrime = 3;
// Table of prime numbers to use as hash table sizes.
// A typical resize algorithm would pick the smallest prime number in this array
@@ -39,40 +40,114 @@ internal static partial class HashHelpers
public static bool IsPrime(int candidate)
{
- if ((candidate & 1) != 0)
+ // This only tests hash table capacities, whose minimum is MinPrime, so 2 is intentionally excluded.
+ Debug.Assert(candidate >= MinPrime);
+
+ if ((candidate & 1) == 0 || (uint)candidate % MinPrime == 0)
{
- int limit = (int)Math.Sqrt(candidate);
- for (int divisor = 3; divisor <= limit; divisor += 2)
+ return candidate == MinPrime;
+ }
+
+ return HasNoPrimeDivisors(candidate, (int)Math.Sqrt(candidate));
+ }
+
+ private static bool HasNoPrimeDivisors(int candidate, int limit)
+ {
+ // Every prime greater than 3 is 6k - 1 or 6k + 1, so test both candidates in each group.
+ for (int divisor = 5; divisor <= limit; divisor += 6)
+ {
+ if (candidate % divisor == 0 || candidate % (divisor + 2) == 0)
{
- if ((candidate % divisor) == 0)
- return false;
+ return false;
}
- return true;
}
- return candidate == 2;
+
+ return true;
}
public static int GetPrime(int min)
{
if (min < 0)
+ {
throw new ArgumentException(SR.Arg_HTCapacityOverflow);
+ }
- foreach (int prime in Primes)
+ if (min <= MinPrime)
{
- if (prime >= min)
- return prime;
+ return MinPrime;
}
- // Outside of our predefined table. Compute the hard way.
- for (int i = (min | 1); i < int.MaxValue; i += 2)
+ // A short linear scan is faster for the common small capacities.
+ const int LinearSearchCount = 16;
+
+ ReadOnlySpan primes = Primes;
+ if (min <= primes[LinearSearchCount - 1])
{
- if (IsPrime(i) && ((i - 1) % HashPrime != 0))
- return i;
+ for (int i = 1; i < LinearSearchCount; i++)
+ {
+ if (primes[i] >= min)
+ {
+ return primes[i];
+ }
+ }
+ }
+ else
+ {
+ int index = primes.Slice(LinearSearchCount).BinarySearch(min);
+ index = index < 0 ? ~index : index;
+ index += LinearSearchCount;
+ if ((uint)index < (uint)primes.Length)
+ {
+ return primes[index];
+ }
+ }
+
+ return GetPrimeAtLeastCore(min);
+ }
+
+ public static int GetPrimeAtLeast(int min)
+ {
+ if (min < 0)
+ {
+ throw new ArgumentException(SR.Arg_HTCapacityOverflow);
+ }
+
+ return min <= MinPrime ? MinPrime : GetPrimeAtLeastCore(min);
+ }
+
+ private static int GetPrimeAtLeastCore(int min)
+ {
+ Debug.Assert(min > MinPrime);
+
+ int candidate = min | 1;
+ uint remainder = (uint)candidate % 6;
+ if (remainder == 3)
+ {
+ candidate += 2;
+ }
+
+ int increment = remainder == 1 ? 4 : 2;
+ int limit = (int)Math.Sqrt(candidate);
+ long nextLimitSquared = (long)(limit + 1) * (limit + 1);
+ while (true)
+ {
+ while (nextLimitSquared <= candidate)
+ {
+ limit++;
+ nextLimitSquared = (long)(limit + 1) * (limit + 1);
+ }
+
+ if ((uint)(candidate - 1) % HashPrime != 0 && HasNoPrimeDivisors(candidate, limit))
+ {
+ return candidate;
+ }
+
+ candidate += increment;
+ increment = 6 - increment;
}
- return min;
}
- // Returns size of hashtable to grow to.
+ // Returns the size of the hashtable to grow to.
public static int ExpandPrime(int oldSize)
{
int newSize = 2 * oldSize;
diff --git a/src/libraries/System.Collections.Concurrent/src/System.Collections.Concurrent.csproj b/src/libraries/System.Collections.Concurrent/src/System.Collections.Concurrent.csproj
index 94d98c52f7336f..c465a795ad5d04 100644
--- a/src/libraries/System.Collections.Concurrent/src/System.Collections.Concurrent.csproj
+++ b/src/libraries/System.Collections.Concurrent/src/System.Collections.Concurrent.csproj
@@ -15,7 +15,7 @@
-
diff --git a/src/libraries/System.Collections.Immutable/src/System.Collections.Immutable.csproj b/src/libraries/System.Collections.Immutable/src/System.Collections.Immutable.csproj
index af568a93a25513..73e17424aefb7d 100644
--- a/src/libraries/System.Collections.Immutable/src/System.Collections.Immutable.csproj
+++ b/src/libraries/System.Collections.Immutable/src/System.Collections.Immutable.csproj
@@ -14,7 +14,7 @@ The System.Collections.Immutable library is built-in as part of the shared frame
-
+
diff --git a/src/libraries/System.Collections/src/System.Collections.csproj b/src/libraries/System.Collections/src/System.Collections.csproj
index 54a2c4e9e694bc..8605801d83faff 100644
--- a/src/libraries/System.Collections/src/System.Collections.csproj
+++ b/src/libraries/System.Collections/src/System.Collections.csproj
@@ -29,7 +29,7 @@
-
+
diff --git a/src/libraries/System.Collections/tests/Generic/Dictionary/Dictionary.Generic.Tests.cs b/src/libraries/System.Collections/tests/Generic/Dictionary/Dictionary.Generic.Tests.cs
index 2197973ca51306..925cdb7a2e6937 100644
--- a/src/libraries/System.Collections/tests/Generic/Dictionary/Dictionary.Generic.Tests.cs
+++ b/src/libraries/System.Collections/tests/Generic/Dictionary/Dictionary.Generic.Tests.cs
@@ -468,6 +468,16 @@ public void TrimExcess_Generic_LargeInitialCapacity_TrimReducesSize()
Assert.Equal(7, dictionary.EnsureCapacity(0));
}
+ [Theory]
+ [InlineData(132, 137)]
+ [InlineData(607, 613)]
+ public void TrimExcess_Generic_UsesNearestValidPrime(int requestedCapacity, int expectedCapacity)
+ {
+ var dictionary = new Dictionary(1000);
+ dictionary.TrimExcess(requestedCapacity);
+ Assert.Equal(expectedCapacity, dictionary.Capacity);
+ }
+
[Theory]
[InlineData(20)]
[InlineData(23)]
diff --git a/src/libraries/System.Collections/tests/Generic/HashSet/HashSet.Generic.Tests.cs b/src/libraries/System.Collections/tests/Generic/HashSet/HashSet.Generic.Tests.cs
index 673fb788101ff7..40b8018b0cafa8 100644
--- a/src/libraries/System.Collections/tests/Generic/HashSet/HashSet.Generic.Tests.cs
+++ b/src/libraries/System.Collections/tests/Generic/HashSet/HashSet.Generic.Tests.cs
@@ -231,6 +231,16 @@ public void HashHet_Generic_TrimExcess_LargePopulatedHashSet_TrimReducesSize(int
Assert.Equal(clone, set);
}
+ [Theory]
+ [InlineData(132, 137)]
+ [InlineData(607, 613)]
+ public void HashSet_Generic_TrimExcess_UsesNearestValidPrime(int requestedCapacity, int expectedCapacity)
+ {
+ var set = new HashSet(1000);
+ set.TrimExcess(requestedCapacity);
+ Assert.Equal(expectedCapacity, set.Capacity);
+ }
+
[Theory]
[InlineData(10, 20, 0)]
[InlineData(10, 20, 7)]
diff --git a/src/libraries/System.Linq.AsyncEnumerable/src/System.Linq.AsyncEnumerable.csproj b/src/libraries/System.Linq.AsyncEnumerable/src/System.Linq.AsyncEnumerable.csproj
index 527d335cab0d04..debf8fd60b87d3 100644
--- a/src/libraries/System.Linq.AsyncEnumerable/src/System.Linq.AsyncEnumerable.csproj
+++ b/src/libraries/System.Linq.AsyncEnumerable/src/System.Linq.AsyncEnumerable.csproj
@@ -13,7 +13,7 @@
-
+
diff --git a/src/libraries/System.Linq.Parallel/src/System.Linq.Parallel.csproj b/src/libraries/System.Linq.Parallel/src/System.Linq.Parallel.csproj
index cba84351a501d4..e48472993a9c16 100644
--- a/src/libraries/System.Linq.Parallel/src/System.Linq.Parallel.csproj
+++ b/src/libraries/System.Linq.Parallel/src/System.Linq.Parallel.csproj
@@ -14,7 +14,7 @@
-
+
diff --git a/src/libraries/System.Linq/src/System.Linq.csproj b/src/libraries/System.Linq/src/System.Linq.csproj
index beeb52f1af76ba..e97e9aa911c0ca 100644
--- a/src/libraries/System.Linq/src/System.Linq.csproj
+++ b/src/libraries/System.Linq/src/System.Linq.csproj
@@ -6,7 +6,7 @@
-
+
diff --git a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems
index bd846a6aed09b1..d50cffdeb86c9c 100644
--- a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems
+++ b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems
@@ -235,7 +235,6 @@
-
@@ -1542,6 +1541,9 @@
Common\System\SR.cs
+
+ Common\System\Collections\HashHelpers.cs
+
System\Collections\Concurrent\IProducerConsumerQueue.cs
diff --git a/src/libraries/System.Private.CoreLib/src/System/Collections/Generic/Dictionary.cs b/src/libraries/System.Private.CoreLib/src/System/Collections/Generic/Dictionary.cs
index 9fa26ab0332e36..da279d4d24bf54 100644
--- a/src/libraries/System.Private.CoreLib/src/System/Collections/Generic/Dictionary.cs
+++ b/src/libraries/System.Private.CoreLib/src/System/Collections/Generic/Dictionary.cs
@@ -1250,6 +1250,7 @@ private void Resize(int newSize, bool forceNewHashCodes)
// Value types never rehash
Debug.Assert(!forceNewHashCodes || !typeof(TKey).IsValueType);
Debug.Assert(_entries != null, "_entries should be non-null");
+ Debug.Assert(HashHelpers.IsPrime(newSize));
Debug.Assert(newSize >= _entries.Length);
Entry[] entries = new Entry[newSize];
@@ -1696,21 +1697,30 @@ public void TrimExcess(int capacity)
ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.capacity);
}
- int newSize = HashHelpers.GetPrime(capacity);
+ int newSize = HashHelpers.GetPrimeAtLeast(capacity);
Entry[]? oldEntries = _entries;
- int currentCapacity = oldEntries == null ? 0 : oldEntries.Length;
- if (newSize >= currentCapacity)
+ if (oldEntries is null || newSize >= oldEntries.Length)
{
return;
}
- int oldCount = _count;
_version++;
- Initialize(newSize);
- Debug.Assert(oldEntries is not null);
+ Debug.Assert(HashHelpers.IsPrime(newSize));
+ Debug.Assert(newSize >= Count);
+
+ int[] buckets = new int[newSize];
+ Entry[] entries = new Entry[newSize];
+
+ // Assign member variables after both arrays allocated to guard against corruption from OOM if second fails
+ _freeList = -1;
+#if TARGET_64BIT
+ _fastModMultiplier = HashHelpers.GetFastModMultiplier((uint)newSize);
+#endif
+ _buckets = buckets;
+ _entries = entries;
- CopyEntries(oldEntries, oldCount);
+ CopyEntries(oldEntries, _count);
}
private void CopyEntries(Entry[] entries, int count)
diff --git a/src/libraries/System.Private.CoreLib/src/System/Collections/Generic/HashSet.cs b/src/libraries/System.Private.CoreLib/src/System/Collections/Generic/HashSet.cs
index 34756784e28b91..9191143faa033a 100644
--- a/src/libraries/System.Private.CoreLib/src/System/Collections/Generic/HashSet.cs
+++ b/src/libraries/System.Private.CoreLib/src/System/Collections/Generic/HashSet.cs
@@ -172,16 +172,7 @@ private void ConstructFrom(HashSet source)
else
{
Initialize(source.Count);
-
- Entry[]? entries = source._entries;
- for (int i = 0; i < source._count; i++)
- {
- ref Entry entry = ref entries![i];
- if (entry.Next >= -1)
- {
- AddIfNotPresent(entry.Value, out _);
- }
- }
+ CopyEntries(source._entries!, source._count);
}
Debug.Assert(Count == source.Count);
@@ -1296,6 +1287,7 @@ private void Resize(int newSize, bool forceNewHashCodes)
// Value types never rehash
Debug.Assert(!forceNewHashCodes || !typeof(T).IsValueType);
Debug.Assert(_entries != null, "_entries should be non-null");
+ Debug.Assert(HashHelpers.IsPrime(newSize));
Debug.Assert(newSize >= _entries.Length);
var entries = new Entry[newSize];
@@ -1337,6 +1329,30 @@ private void Resize(int newSize, bool forceNewHashCodes)
_entries = entries;
}
+ private void CopyEntries(Entry[] entries, int count)
+ {
+ Debug.Assert(_entries is not null);
+
+ Entry[] newEntries = _entries;
+ int newCount = 0;
+ for (int i = 0; i < count; i++)
+ {
+ int hashCode = entries[i].HashCode;
+ if (entries[i].Next >= -1)
+ {
+ ref Entry entry = ref newEntries[newCount];
+ entry = entries[i];
+ ref int bucket = ref GetBucketRef(hashCode);
+ entry.Next = bucket - 1; // Value in _buckets is 1-based
+ bucket = newCount + 1;
+ newCount++;
+ }
+ }
+
+ _count = newCount;
+ _freeCount = 0;
+ }
+
///
/// Sets the capacity of a object to the actual number of elements it contains,
/// rounded up to a nearby, implementation-specific value.
@@ -1353,35 +1369,30 @@ public void TrimExcess(int capacity)
{
ArgumentOutOfRangeException.ThrowIfLessThan(capacity, Count);
- int newSize = HashHelpers.GetPrime(capacity);
+ int newSize = HashHelpers.GetPrimeAtLeast(capacity);
Entry[]? oldEntries = _entries;
- int currentCapacity = oldEntries == null ? 0 : oldEntries.Length;
- if (newSize >= currentCapacity)
+ if (oldEntries is null || newSize >= oldEntries.Length)
{
return;
}
- int oldCount = _count;
_version++;
- Initialize(newSize);
- Entry[]? entries = _entries;
- int count = 0;
- for (int i = 0; i < oldCount; i++)
- {
- int hashCode = oldEntries![i].HashCode; // At this point, we know we have entries.
- if (oldEntries[i].Next >= -1)
- {
- ref Entry entry = ref entries![count];
- entry = oldEntries[i];
- ref int bucket = ref GetBucketRef(hashCode);
- entry.Next = bucket - 1; // Value in _buckets is 1-based
- bucket = count + 1;
- count++;
- }
- }
- _count = count;
- _freeCount = 0;
+ Debug.Assert(HashHelpers.IsPrime(newSize));
+ Debug.Assert(newSize >= Count);
+
+ var buckets = new int[newSize];
+ var entries = new Entry[newSize];
+
+ // Assign member variables after both arrays are allocated to guard against corruption from OOM if second fails.
+ _freeList = -1;
+ _buckets = buckets;
+ _entries = entries;
+#if TARGET_64BIT
+ _fastModMultiplier = HashHelpers.GetFastModMultiplier((uint)newSize);
+#endif
+
+ CopyEntries(oldEntries, _count);
}
#endregion
diff --git a/src/libraries/System.Reflection.MetadataLoadContext/src/System.Reflection.MetadataLoadContext.csproj b/src/libraries/System.Reflection.MetadataLoadContext/src/System.Reflection.MetadataLoadContext.csproj
index 20b4bcd95aa7dc..dc5fff77c4f63d 100644
--- a/src/libraries/System.Reflection.MetadataLoadContext/src/System.Reflection.MetadataLoadContext.csproj
+++ b/src/libraries/System.Reflection.MetadataLoadContext/src/System.Reflection.MetadataLoadContext.csproj
@@ -77,7 +77,7 @@
-
+
diff --git a/src/libraries/System.Reflection.MetadataLoadContext/src/System/Reflection/TypeLoading/General/HashHelpers.cs b/src/libraries/System.Reflection.MetadataLoadContext/src/System/Reflection/TypeLoading/General/HashHelpers.cs
deleted file mode 100644
index 0f105fad849a8d..00000000000000
--- a/src/libraries/System.Reflection.MetadataLoadContext/src/System/Reflection/TypeLoading/General/HashHelpers.cs
+++ /dev/null
@@ -1,68 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-namespace System.Reflection.TypeLoading
-{
- internal static partial class HashHelpers
- {
- public const int HashPrime = 101;
-
- // Table of prime numbers to use as hash table sizes.
- // A typical resize algorithm would pick the smallest prime number in this array
- // that is larger than twice the previous capacity.
- // Suppose our Hashtable currently has capacity x and enough elements are added
- // such that a resize needs to occur. Resizing first computes 2x then finds the
- // first prime in the table greater than 2x, i.e. if primes are ordered
- // p_1, p_2, ..., p_i, ..., it finds p_n such that p_n-1 < 2x < p_n.
- // Doubling is important for preserving the asymptotic complexity of the
- // hashtable operations such as add. Having a prime guarantees that double
- // hashing does not lead to infinite loops. IE, your hash function will be
- // h1(key) + i*h2(key), 0 <= i < size. h2 and the size must be relatively prime.
- // We prefer the low computation costs of higher prime numbers over the increased
- // memory allocation of a fixed prime number i.e. when right sizing a HashSet.
- public static ReadOnlySpan Primes =>
- [
- 3, 7, 11, 17, 23, 29, 37, 47, 59, 71, 89, 107, 131, 163, 197, 239, 293, 353, 431, 521, 631, 761, 919,
- 1103, 1327, 1597, 1931, 2333, 2801, 3371, 4049, 4861, 5839, 7013, 8419, 10103, 12143, 14591,
- 17519, 21023, 25229, 30293, 36353, 43627, 52361, 62851, 75431, 90523, 108631, 130363, 156437,
- 187751, 225307, 270371, 324449, 389357, 467237, 560689, 672827, 807403, 968897, 1162687, 1395263,
- 1674319, 2009191, 2411033, 2893249, 3471899, 4166287, 4999559, 5999471, 7199369
- ];
-
- public static bool IsPrime(int candidate)
- {
- if ((candidate & 1) != 0)
- {
- int limit = (int)Math.Sqrt(candidate);
- for (int divisor = 3; divisor <= limit; divisor += 2)
- {
- if ((candidate % divisor) == 0)
- return false;
- }
- return true;
- }
- return (candidate == 2);
- }
-
- public static int GetPrime(int min)
- {
- if (min < 0)
- throw new ArgumentException(SR.Arg_HTCapacityOverflow);
-
- foreach (int prime in Primes)
- {
- if (prime >= min)
- return prime;
- }
-
- //outside of our predefined table.
- //compute the hard way.
- for (int i = (min | 1); i < int.MaxValue; i += 2)
- {
- if (IsPrime(i) && ((i - 1) % HashPrime != 0))
- return i;
- }
- return min;
- }
- }
-}
diff --git a/src/libraries/System.Reflection.MetadataLoadContext/src/System/Reflection/TypeLoading/Modules/GetTypeCoreCache.cs b/src/libraries/System.Reflection.MetadataLoadContext/src/System/Reflection/TypeLoading/Modules/GetTypeCoreCache.cs
index 0c5188c0b15b18..85cd3ea03e5708 100644
--- a/src/libraries/System.Reflection.MetadataLoadContext/src/System/Reflection/TypeLoading/Modules/GetTypeCoreCache.cs
+++ b/src/libraries/System.Reflection.MetadataLoadContext/src/System/Reflection/TypeLoading/Modules/GetTypeCoreCache.cs
@@ -1,6 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using System.Collections;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
diff --git a/src/libraries/System.Runtime.Serialization.Formatters/src/System.Runtime.Serialization.Formatters.csproj b/src/libraries/System.Runtime.Serialization.Formatters/src/System.Runtime.Serialization.Formatters.csproj
index cdd090c8abb79f..366a4331aea6d0 100644
--- a/src/libraries/System.Runtime.Serialization.Formatters/src/System.Runtime.Serialization.Formatters.csproj
+++ b/src/libraries/System.Runtime.Serialization.Formatters/src/System.Runtime.Serialization.Formatters.csproj
@@ -38,7 +38,7 @@
-
diff --git a/src/libraries/System.Text.Json/src/System.Text.Json.csproj b/src/libraries/System.Text.Json/src/System.Text.Json.csproj
index 5ad47e9be7e18b..a85631cbfa6c7c 100644
--- a/src/libraries/System.Text.Json/src/System.Text.Json.csproj
+++ b/src/libraries/System.Text.Json/src/System.Text.Json.csproj
@@ -395,7 +395,7 @@ The System.Text.Json library is built-in as part of the shared framework in .NET
-
+