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 - +