Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,16 @@ public static FrozenHashTable Create(Span<int> hashCodes, bool hashCodesAreUniqu
// - bucketStarts: initially filled with all -1s, the ith element stores the index
// into hashCodes of the head element of that bucket's chain.
// - nexts: the ith element stores the index of the next item in the chain.
// Use long to check for overflow before allocating - very large collections can overflow int.
#if NET
if ((long)numBuckets + hashCodes.Length > Array.MaxLength)
Comment thread
EgorBo marked this conversation as resolved.
#else
if ((long)numBuckets + hashCodes.Length > 0x7FFFFFC7)
#endif
{
throw new OutOfMemoryException();
Comment thread
danmoseley marked this conversation as resolved.
}
Comment thread
danmoseley marked this conversation as resolved.

int[] arrayPoolBuckets = ArrayPool<int>.Shared.Rent(numBuckets + hashCodes.Length);
Comment thread
danmoseley marked this conversation as resolved.
Span<int> bucketStarts = arrayPoolBuckets.AsSpan(0, numBuckets);
Span<int> nexts = arrayPoolBuckets.AsSpan(numBuckets, hashCodes.Length);
Expand Down Expand Up @@ -174,7 +184,20 @@ private static int CalcNumBuckets(ReadOnlySpan<int> hashCodes, bool hashCodesAre

// Based on our observations, in more than 99.5% of cases the number of buckets that meets our criteria is
// at least twice as big as the number of unique hash codes.
int minNumBuckets = uniqueCodesCount * 2;
// Use long to avoid integer overflow when uniqueCodesCount is large (> ~1 billion).
long minNumBuckets = (long)uniqueCodesCount * 2;
Comment thread
danmoseley marked this conversation as resolved.

// If the minimum bucket count combined with hash codes exceeds array length limits,
// skip the expensive collision-counting loop below — any bucket count it finds
// would cause Create to fail. Fall back to the next prime above uniqueCodesCount.
#if NET
if (minNumBuckets + hashCodes.Length > Array.MaxLength)
#else
if (minNumBuckets + hashCodes.Length > 0x7FFFFFC7)
#endif
{
return HashHelpers.GetPrime(uniqueCodesCount);
Comment thread
stephentoub marked this conversation as resolved.
}

// In our precomputed primes table, find the index of the smallest prime that's at least as large as our number of
// hash codes. If there are more codes than in our precomputed primes table, which accommodates millions of values,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -516,7 +516,30 @@ protected static long NextLong(Random random)

public class FrozenDictionary_Generic_Tests_int_int : FrozenDictionary_Generic_Tests_base_for_numbers<int>
{
protected override int Next(Random random) => random.Next();
protected override int Next(Random random) => random.Next();
Comment thread
danmoseley marked this conversation as resolved.

[Fact]
[OuterLoop("Allocates a large collection")]
public void ToFrozenDictionary_LargeDictionary_ExceedsPrimeTable()
{
// Validate that FrozenHashTable handles collections whose
// uniqueCodesCount * 2 exceeds the precomputed primes table,
// exercising the CalcNumBuckets early-return path.
// The integer overflow fix for >1B items (long minNumBuckets)
Comment thread
danmoseley marked this conversation as resolved.
Outdated
// cannot be practically tested without multi-GB allocations.
const int count = 4_000_000;
Comment thread
stephentoub marked this conversation as resolved.
Outdated
var dict = new Dictionary<int, int>(count);
for (int i = 0; i < count; i++)
{
dict.Add(i, i);
}

FrozenDictionary<int, int> frozen = dict.ToFrozenDictionary();
Assert.Equal(count, frozen.Count);
Assert.True(frozen.ContainsKey(0));
Assert.True(frozen.ContainsKey(count - 1));
Assert.False(frozen.ContainsKey(count));
}
}

public class FrozenDictionary_Generic_Tests_uint_uint : FrozenDictionary_Generic_Tests_base_for_numbers<uint>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,29 @@ public class FrozenSet_Generic_Tests_int : FrozenSet_Generic_Tests<int>
protected override bool DefaultValueAllowed => true;

protected override int CreateT(int seed) => new Random(seed).Next();

[Fact]
[OuterLoop("Allocates a large collection")]
public void ToFrozenSet_LargeSet_ExceedsPrimeTable()
{
// Validate that FrozenHashTable handles collections whose
// uniqueCodesCount * 2 exceeds the precomputed primes table,
// exercising the CalcNumBuckets early-return path.
// The integer overflow fix for >1B items (long minNumBuckets)
// cannot be practically tested without multi-GB allocations.
const int count = 4_000_000;
var set = new HashSet<int>(count);
for (int i = 0; i < count; i++)
{
set.Add(i);
}

FrozenSet<int> frozen = set.ToFrozenSet();
Assert.Equal(count, frozen.Count);
Assert.True(frozen.Contains(0));
Assert.True(frozen.Contains(count - 1));
Assert.False(frozen.Contains(count));
}
}

public class FrozenSet_Generic_Tests_SimpleClass : FrozenSet_Generic_Tests<SimpleClass>
Expand Down
Loading