<ItemGroup>
<Compile Include="Properties\InternalsVisibleTo.cs" />
+ <Compile Include="System\Polyfills.cs" />
+ <Compile Include="System\Collections\ThrowHelper.cs" />
+ <Compile Include="$(CoreLibSharedDir)System\Collections\HashHelpers.cs" Link="System\Collections\HashHelpers.cs" />
+
+ <Compile Include="System\Collections\Frozen\Constants.cs" />
+ <Compile Include="System\Collections\Frozen\DefaultFrozenDictionary.cs" />
+ <Compile Include="System\Collections\Frozen\DefaultFrozenSet.cs" />
+ <Compile Include="System\Collections\Frozen\EmptyFrozenDictionary.cs" />
+ <Compile Include="System\Collections\Frozen\EmptyFrozenSet.cs" />
+ <Compile Include="System\Collections\Frozen\FrozenDictionary.cs" />
+ <Compile Include="System\Collections\Frozen\FrozenHashTable.cs" />
+ <Compile Include="System\Collections\Frozen\FrozenSet.cs" />
+ <Compile Include="System\Collections\Frozen\FrozenSetInternalBase.cs" />
+ <Compile Include="System\Collections\Frozen\ImmutableArrayFactory.cs" />
+ <Compile Include="System\Collections\Frozen\ItemsFrozenSet.cs" />
+ <Compile Include="System\Collections\Frozen\KeysAndValuesFrozenDictionary.cs" />
+ <Compile Include="System\Collections\Frozen\SmallFrozenDictionary.cs" />
+ <Compile Include="System\Collections\Frozen\SmallFrozenSet.cs" />
+ <Compile Include="System\Collections\Frozen\SmallComparableValueTypeFrozenDictionary.cs" />
+ <Compile Include="System\Collections\Frozen\SmallComparableValueTypeFrozenSet.cs" />
+ <Compile Include="System\Collections\Frozen\ValueTypeDefaultComparerFrozenDictionary.cs" />
+ <Compile Include="System\Collections\Frozen\ValueTypeDefaultComparerFrozenSet.cs" />
+ <Compile Include="System\Collections\Frozen\Int32\Int32FrozenDictionary.cs" />
+ <Compile Include="System\Collections\Frozen\Int32\Int32FrozenSet.cs" />
+ <Compile Include="System\Collections\Frozen\String\Hashing.cs" />
<Compile Include="System\Collections\Frozen\String\KeyAnalyzer.cs" />
+ <Compile Include="System\Collections\Frozen\String\LengthBucketsFrozenDictionary.cs" />
+ <Compile Include="System\Collections\Frozen\String\LengthBucketsFrozenSet.cs" />
+ <Compile Include="System\Collections\Frozen\String\OrdinalStringFrozenDictionary.cs" />
<Compile Include="System\Collections\Frozen\String\OrdinalStringFrozenDictionary_FullCaseInsensitiveAscii.cs" />
<Compile Include="System\Collections\Frozen\String\OrdinalStringFrozenDictionary_FullCaseInsensitive.cs" />
<Compile Include="System\Collections\Frozen\String\OrdinalStringFrozenDictionary_Full.cs" />
<Compile Include="System\Collections\Frozen\String\OrdinalStringFrozenDictionary_RightJustifiedSingleChar.cs" />
<Compile Include="System\Collections\Frozen\String\OrdinalStringFrozenDictionary_RightJustifiedSubstring.cs" />
<Compile Include="System\Collections\Frozen\String\OrdinalStringFrozenDictionary_LeftJustifiedSubstring.cs" />
- <Compile Include="System\Collections\Frozen\String\OrdinalStringFrozenDictionary.cs" />
- <Compile Include="System\Collections\Frozen\String\Hashing.cs" />
+ <Compile Include="System\Collections\Frozen\String\OrdinalStringFrozenSet.cs" />
<Compile Include="System\Collections\Frozen\String\OrdinalStringFrozenSet_FullCaseInsensitiveAscii.cs" />
<Compile Include="System\Collections\Frozen\String\OrdinalStringFrozenSet_FullCaseInsensitive.cs" />
<Compile Include="System\Collections\Frozen\String\OrdinalStringFrozenSet_Full.cs" />
<Compile Include="System\Collections\Frozen\String\OrdinalStringFrozenSet_RightJustifiedCaseInsensitiveSubstring.cs" />
<Compile Include="System\Collections\Frozen\String\OrdinalStringFrozenSet_RightJustifiedSubstring.cs" />
<Compile Include="System\Collections\Frozen\String\OrdinalStringFrozenSet_RightJustifiedSingleChar.cs" />
- <Compile Include="System\Collections\Frozen\String\OrdinalStringFrozenSet.cs" />
-
- <Compile Include="System\Polyfills.cs" />
- <Compile Include="System\Collections\ThrowHelper.cs" />
- <Compile Include="$(CoreLibSharedDir)System\Collections\HashHelpers.cs" Link="System\Collections\HashHelpers.cs" />
-
- <Compile Include="System\Collections\Frozen\Constants.cs" />
- <Compile Include="System\Collections\Frozen\DefaultFrozenDictionary.cs" />
- <Compile Include="System\Collections\Frozen\DefaultFrozenSet.cs" />
- <Compile Include="System\Collections\Frozen\EmptyFrozenDictionary.cs" />
- <Compile Include="System\Collections\Frozen\EmptyFrozenSet.cs" />
- <Compile Include="System\Collections\Frozen\FrozenDictionary.cs" />
- <Compile Include="System\Collections\Frozen\FrozenHashTable.cs" />
- <Compile Include="System\Collections\Frozen\FrozenSet.cs" />
- <Compile Include="System\Collections\Frozen\FrozenSetInternalBase.cs" />
- <Compile Include="System\Collections\Frozen\ImmutableArrayFactory.cs" />
- <Compile Include="System\Collections\Frozen\ItemsFrozenSet.cs" />
- <Compile Include="System\Collections\Frozen\KeysAndValuesFrozenDictionary.cs" />
- <Compile Include="System\Collections\Frozen\String\LengthBucketsFrozenDictionary.cs" />
- <Compile Include="System\Collections\Frozen\String\LengthBucketsFrozenSet.cs" />
- <Compile Include="System\Collections\Frozen\SmallFrozenDictionary.cs" />
- <Compile Include="System\Collections\Frozen\SmallFrozenSet.cs" />
- <Compile Include="System\Collections\Frozen\ValueTypeDefaultComparerFrozenDictionary.cs" />
- <Compile Include="System\Collections\Frozen\ValueTypeDefaultComparerFrozenSet.cs" />
- <Compile Include="System\Collections\Frozen\Int32\Int32FrozenDictionary.cs" />
- <Compile Include="System\Collections\Frozen\Int32\Int32FrozenSet.cs" />
-
+
<Compile Include="System\Collections\Generic\IHashKeyCollection.cs" />
<Compile Include="System\Collections\Generic\ISortKeyCollection.cs" />
<Compile Include="System\Collections\Frozen\WrappedDictionaryFrozenDictionary.cs" />
</ItemGroup>
- <ItemGroup Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net7.0'))">
- <Compile Include="System\Collections\Frozen\Integer\SmallIntegerFrozenDictionary.cs" />
- <Compile Include="System\Collections\Frozen\Integer\SmallIntegerFrozenSet.cs" />
- </ItemGroup>
-
- <ItemGroup Condition="!$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net7.0'))">
- <Compile Include="System\Collections\Frozen\Int32\SmallInt32FrozenDictionary.cs" />
- <Compile Include="System\Collections\Frozen\Int32\SmallInt32FrozenSet.cs" />
- </ItemGroup>
-
<ItemGroup Condition="'$(TargetFramework)' == '$(NetCoreAppCurrent)'">
<Reference Include="System.Collections" />
<Reference Include="System.Linq" />
/// <remarks>
/// These numbers were arrived through simple benchmarks conducted against .NET 7.
/// It's worth potentially tweaking these values if the implementation of the
- /// collections changes in a substantial way, or if the JIT got smarter over time.
+ /// collections changes in a substantial way, or if the JIT improves related code gen over time.
/// </remarks>
internal static class Constants
{
- /// <summary>
- /// Threshold when we switch from scanning to hashing for non-integer collections.
- /// </summary>
+ /// <summary>Threshold when we switch from scanning to hashing for non-value-type or non-default-comparer collections.</summary>
/// <remarks>
/// This determines the threshold where we switch from
/// the scanning-based SmallFrozenDictionary/Set to the hashing-based
/// </remarks>
public const int MaxItemsInSmallFrozenCollection = 4;
- /// <summary>
- /// Threshold when we switch from scanning to hashing integer collections.
- /// </summary>
+ /// <summary>Threshold when we switch from scanning to hashing value type collections using a default comparer.</summary>
/// <remarks>
/// This determines the threshold when we switch from the scanning
- /// SmallIntegerFrozenDictionary/Set to the
- /// hashing IntegerFrozenDictionary/Set.
+ /// SmallValueTypeDefaultComparerFrozenDictionary/Set to the
+ /// hashing ValueTypeDefaultComparerFrozenDictionary/Set.
/// </remarks>
- public const int MaxItemsInSmallIntegerFrozenCollection = 10;
-
- /// <summary>
- /// How much free space is allowed in a sparse integer set
- /// </summary>
- /// <remarks>
- /// This determines how much free space is allowed in a sparse integer set.
- /// This is a space/perf trade off. The sparse sets just use a bit vector to
- /// hold the state, so lookup is always fast. But there's a point where you're
- /// too much heap space.
- /// </remarks>
- public const int MaxSparsenessFactorInSparseRangeIntegerSet = 8;
+ public const int MaxItemsInSmallComparableValueTypeFrozenCollection = 10;
}
}
if (typeof(TKey).IsValueType)
{
// Optimize for value types when the default comparer is being used. In such a case, the implementation
- // may use EqualityComparer<TKey>.Default.Equals/GetHashCode directly, with generic specialization enabling
+ // may use {Equality}Comparer<TKey>.Default.Compare/Equals/GetHashCode directly, with generic specialization enabling
// the Equals/GetHashCode methods to be devirtualized and possibly inlined.
if (ReferenceEquals(comparer, EqualityComparer<TKey>.Default))
{
-#if NET7_0_OR_GREATER
- if (typeof(TKey) == typeof(sbyte)) return PickIntegerDictionary<sbyte>(source);
- if (typeof(TKey) == typeof(byte)) return PickIntegerDictionary<byte>(source);
- if (typeof(TKey) == typeof(short)) return PickIntegerDictionary<short>(source);
- if (typeof(TKey) == typeof(ushort)) return PickIntegerDictionary<ushort>(source);
- if (typeof(TKey) == typeof(int)) return PickIntegerDictionary<int>(source);
- if (typeof(TKey) == typeof(uint)) return PickIntegerDictionary<uint>(source);
- if (typeof(TKey) == typeof(long)) return PickIntegerDictionary<long>(source);
- if (typeof(TKey) == typeof(ulong)) return PickIntegerDictionary<ulong>(source);
-
- static FrozenDictionary<TKey, TValue> PickIntegerDictionary<TInt>(Dictionary<TKey, TValue> source) where TInt : struct, IBinaryInteger<TInt> =>
- (FrozenDictionary<TKey, TValue>)(object)
- (source.Count <= Constants.MaxItemsInSmallIntegerFrozenCollection ? new SmallIntegerFrozenDictionary<TInt, TValue>((Dictionary<TInt, TValue>)(object)source) :
- typeof(TInt) == typeof(int) ? new Int32FrozenDictionary<TValue>((Dictionary<int, TValue>)(object)source) :
- new ValueTypeDefaultComparerFrozenDictionary<TInt, TValue>((Dictionary<TInt, TValue>)(object)source));
-#else
+ if (default(TKey) is IComparable<TKey> &&
+ source.Count <= Constants.MaxItemsInSmallComparableValueTypeFrozenCollection)
+ {
+ return (FrozenDictionary<TKey, TValue>)(object)new SmallComparableValueTypeFrozenDictionary<TKey, TValue>(source);
+ }
+
if (typeof(TKey) == typeof(int))
{
- return (FrozenDictionary<TKey, TValue>)(object)
- (source.Count <= Constants.MaxItemsInSmallIntegerFrozenCollection ?
- new SmallInt32FrozenDictionary<TValue>((Dictionary<int, TValue>)(object)source) :
- new Int32FrozenDictionary<TValue>((Dictionary<int, TValue>)(object)source));
+ return (FrozenDictionary<TKey, TValue>)(object)new Int32FrozenDictionary<TValue>((Dictionary<int, TValue>)(object)source);
}
-#endif
+
return new ValueTypeDefaultComparerFrozenDictionary<TKey, TValue>(source);
}
}
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.CompilerServices;
-using System.Runtime.InteropServices;
namespace System.Collections.Frozen
{
private readonly Bucket[] _buckets;
private readonly ulong _fastModMultiplier;
+ /// <summary>Initializes the hashtable with the computed hashcodes and bucket information.</summary>
+ /// <param name="hashCodes">The array of hashcodes grouped into contiguous regions by bucket. Each bucket is one and only one region of the array.</param>
+ /// <param name="buckets">
+ /// The array of buckets, indexed by hashCodes % buckets.Length, where each bucket is
+ /// the start/end index into <paramref name="hashCodes"/> for all items in that bucket.
+ /// </param>
+ /// <param name="fastModMultiplier">The multiplier to use as part of a FastMod method call.</param>
private FrozenHashTable(int[] hashCodes, Bucket[] buckets, ulong fastModMultiplier)
{
Debug.Assert(hashCodes.Length != 0);
{
Debug.Assert(entries.Length != 0);
- int[] hashCodes = new int[entries.Length];
+ // Calculate the hashcodes for every entry.
+ int[] arrayPoolHashCodes = ArrayPool<int>.Shared.Rent(entries.Length);
+ Span<int> hashCodes = arrayPoolHashCodes.AsSpan(0, entries.Length);
for (int i = 0; i < entries.Length; i++)
{
hashCodes[i] = hasher(entries[i]);
}
+ // Determine how many buckets to use. This might be fewer than the number of entries
+ // if any entries have identical hashcodes (not just different hashcodes that might
+ // map to the same bucket).
int numBuckets = CalcNumBuckets(hashCodes, optimizeForReading);
ulong fastModMultiplier = HashHelpers.GetFastModMultiplier((uint)numBuckets);
- var chainBuddies = new Dictionary<uint, List<ChainBuddy>>();
+ // Create two spans:
+ // - 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.
+ int[] arrayPoolBuckets = ArrayPool<int>.Shared.Rent(numBuckets + hashCodes.Length);
+ Span<int> bucketStarts = arrayPoolBuckets.AsSpan(0, numBuckets);
+ Span<int> nexts = arrayPoolBuckets.AsSpan(numBuckets, hashCodes.Length);
+ bucketStarts.Fill(-1);
+
+ // Populate the bucket entries and starts. For each hash code, compute its bucket,
+ // and store at the bucket entry corresponding to the hashcode item the entry for that
+ // item, which includes a copy of the hash code and the current bucket start, which
+ // is then replaced by this entry as it's pushed into the bucket list.
for (int index = 0; index < hashCodes.Length; index++)
{
int hashCode = hashCodes[index];
- uint bucket = HashHelpers.FastMod((uint)hashCode, (uint)numBuckets, fastModMultiplier);
+ int bucketNum = (int)HashHelpers.FastMod((uint)hashCode, (uint)bucketStarts.Length, fastModMultiplier);
-#if NET6_0_OR_GREATER
- ref List<ChainBuddy>? list = ref CollectionsMarshal.GetValueRefOrAddDefault(chainBuddies, bucket, out _);
- list ??= new List<ChainBuddy>();
-#else
- if (!chainBuddies.TryGetValue(bucket, out List<ChainBuddy>? list))
- {
- chainBuddies[bucket] = list = new List<ChainBuddy>();
- }
-#endif
-
- list.Add(new ChainBuddy(hashCode, index));
+ ref int bucketStart = ref bucketStarts[bucketNum];
+ nexts[index] = bucketStart;
+ bucketStart = index;
}
- var buckets = new Bucket[numBuckets]; // buckets left uninitialized will by default have an endIndex < startIndex
-
+ // Write out the hashcodes and buckets arrays to be used by the FrozenHashtable instance.
+ // We iterate through each bucket start, and from each, each item in that chain, writing
+ // out all of the items in each chain next to each other in the hashcodes list (and
+ // calling the setter to allow the consumer to reorder its entries appropriately).
+ // Along the way we could how many items are in each chain, and use that along with
+ // the starting index to write out the bucket information for indexing into hashcodes.
+ var hashtableHashcodes = new int[hashCodes.Length];
+ var hashtableBuckets = new Bucket[bucketStarts.Length];
int count = 0;
- foreach (KeyValuePair<uint, List<ChainBuddy>> chain in chainBuddies)
+ for (int bucketNum = 0; bucketNum < hashtableBuckets.Length; bucketNum++)
{
- List<ChainBuddy> list = chain.Value;
- buckets[chain.Key] = new Bucket(count, list.Count);
- for (int i = 0; i < list.Count; i++)
+ int bucketStart = bucketStarts[bucketNum];
+ if (bucketStart < 0)
{
- hashCodes[count] = list[i].HashCode;
- setter(count, entries[list[i].Index]);
+ continue;
+ }
+
+ int bucketCount = 0;
+ int index = bucketStart;
+ bucketStart = count;
+ while (index >= 0)
+ {
+ hashtableHashcodes[count] = hashCodes[index];
+ setter(count, entries[index]);
count++;
+ bucketCount++;
+
+ index = nexts[index];
}
+
+ hashtableBuckets[bucketNum] = new Bucket(bucketStart, bucketCount);
}
+ Debug.Assert(count == hashtableHashcodes.Length);
- return new FrozenHashTable(hashCodes, buckets, fastModMultiplier);
+ ArrayPool<int>.Shared.Return(arrayPoolBuckets);
+ ArrayPool<int>.Shared.Return(arrayPoolHashCodes);
+
+ return new FrozenHashTable(hashtableHashcodes, hashtableBuckets, fastModMultiplier);
}
/// <summary>
internal int[] HashCodes { get; }
/// <summary>
- /// Given an array of hash codes, figure out the best number of hash buckets to use.
+ /// Given a span of hash codes, figure out the best number of hash buckets to use.
/// </summary>
/// <remarks>
/// This tries to select a prime number of buckets. Rather than iterating through all possible bucket
/// sizes, starting at the exact number of hash codes and incrementing the bucket count by 1 per trial,
/// this is a trade-off between speed of determining a good number of buckets and maximal density.
/// </remarks>
- private static int CalcNumBuckets(int[] hashCodes, bool optimizeForReading)
+ private static int CalcNumBuckets(ReadOnlySpan<int> hashCodes, bool optimizeForReading)
{
- Debug.Assert(hashCodes is not null);
Debug.Assert(hashCodes.Length != 0);
const double AcceptableCollisionRate = 0.05; // What is a satisfactory rate of hash collisions?
}
// Filter out duplicate codes, since no increase in buckets will avoid collisions from duplicate input hash codes.
- var codes = new HashSet<int>(hashCodes);
+ var codes =
+#if NETCOREAPP2_0_OR_GREATER
+ new HashSet<int>(hashCodes.Length);
+#else
+ new HashSet<int>();
+#endif
+ foreach (int hashCode in hashCodes)
+ {
+ codes.Add(hashCode);
+ }
Debug.Assert(codes.Count != 0);
// In our precomputed primes table, find the index of the smallest prime that's at least as large as our number of
return bestNumBuckets;
}
- private readonly struct ChainBuddy
- {
- public readonly int HashCode;
- public readonly int Index;
-
- public ChainBuddy(int hashCode, int index)
- {
- HashCode = hashCode;
- Index = index;
- }
- }
-
private readonly struct Bucket
{
public readonly int StartIndex;
if (typeof(T).IsValueType)
{
// Optimize for value types when the default comparer is being used. In such a case, the implementation
- // may use EqualityComparer<T>.Default.Equals/GetHashCode directly, with generic specialization enabling
+ // may use {Equality}Comparer<T>.Default.Compare/Equals/GetHashCode directly, with generic specialization enabling
// the Equals/GetHashCode methods to be devirtualized and possibly inlined.
if (ReferenceEquals(comparer, EqualityComparer<T>.Default))
{
-#if NET7_0_OR_GREATER
- if (typeof(T) == typeof(sbyte)) return PickIntegerSet<sbyte>(source);
- if (typeof(T) == typeof(byte)) return PickIntegerSet<byte>(source);
- if (typeof(T) == typeof(short)) return PickIntegerSet<short>(source);
- if (typeof(T) == typeof(ushort)) return PickIntegerSet<ushort>(source);
- if (typeof(T) == typeof(int)) return PickIntegerSet<int>(source);
- if (typeof(T) == typeof(uint)) return PickIntegerSet<uint>(source);
- if (typeof(T) == typeof(long)) return PickIntegerSet<long>(source);
- if (typeof(T) == typeof(ulong)) return PickIntegerSet<ulong>(source);
-
- static FrozenSet<T> PickIntegerSet<TInt>(HashSet<T> source)
- where TInt : struct, IBinaryInteger<TInt> => (FrozenSet<T>)(object)
- (source.Count <= Constants.MaxItemsInSmallIntegerFrozenCollection ? new SmallIntegerFrozenSet<TInt>((HashSet<TInt>)(object)source) :
- typeof(T) == typeof(int) ? new Int32FrozenSet((HashSet<int>)(object)source) :
- new ValueTypeDefaultComparerFrozenSet<T>(source));
-#else
+ if (default(T) is IComparable<T> &&
+ source.Count <= Constants.MaxItemsInSmallComparableValueTypeFrozenCollection)
+ {
+ return (FrozenSet<T>)(object)new SmallComparableValueTypeFrozenSet<T>(source);
+ }
+
if (typeof(T) == typeof(int))
{
- return (FrozenSet<T>)(object)(source.Count <= Constants.MaxItemsInSmallIntegerFrozenCollection ?
- new SmallInt32FrozenSet((HashSet<int>)(object)source) :
- new Int32FrozenSet((HashSet<int>)(object)source));
+ return (FrozenSet<T>)(object)new Int32FrozenSet((HashSet<int>)(object)source);
}
-#endif
+
return new ValueTypeDefaultComparerFrozenSet<T>(source);
}
}
{
/// <summary>Provides a frozen dictionary to use when the key is an <see cref="int"/> and the default comparer is used.</summary>
/// <remarks>
- /// This key type is specialized as a memory optimization, as the frozen hash table already contains the array of all
+ /// This dictionary type is specialized as a memory optimization, as the frozen hash table already contains the array of all
/// int values, and we can thus use its array as the keys rather than maintaining a duplicate copy.
/// </remarks>
internal sealed class Int32FrozenDictionary<TValue> : FrozenDictionary<int, TValue>
namespace System.Collections.Frozen
{
/// <summary>Provides a frozen set to use when the value is an <see cref="int"/> and the default comparer is used.</summary>
+ /// <remarks>
+ /// This set type is specialized as a memory optimization, as the frozen hash table already contains the array of all
+ /// int values, and we can thus use its array as the items rather than maintaining a duplicate copy.
+ /// </remarks>
internal sealed class Int32FrozenSet : FrozenSetInternalBase<int, Int32FrozenSet.GSW>
{
private readonly FrozenHashTable _hashTable;
+++ /dev/null
-// 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.Generic;
-using System.Diagnostics;
-using System.Linq;
-using System.Runtime.CompilerServices;
-
-namespace System.Collections.Frozen
-{
- /// <summary>Provides a frozen dictionary to use when the key is an <see cref="int"/>, the default comparer is used, and the item count is small.</summary>
- /// <remarks>
- /// No hashing here, just a straight-up linear scan through the items.
- /// </remarks>
- internal sealed class SmallInt32FrozenDictionary<TValue> : FrozenDictionary<int, TValue>
- {
- private readonly int[] _keys;
- private readonly TValue[] _values;
- private readonly int _max;
-
- internal SmallInt32FrozenDictionary(Dictionary<int, TValue> source) : base(EqualityComparer<int>.Default)
- {
- Debug.Assert(source.Count != 0);
- Debug.Assert(ReferenceEquals(source.Comparer, EqualityComparer<int>.Default));
-
- _keys = source.Keys.ToArray();
- _values = source.Values.ToArray();
- Array.Sort(_keys, _values);
-
- _max = _keys[_keys.Length - 1];
- }
-
- private protected override int[] KeysCore => _keys;
- private protected override TValue[] ValuesCore => _values;
- private protected override Enumerator GetEnumeratorCore() => new Enumerator(_keys, _values);
- private protected override int CountCore => _keys.Length;
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private protected override ref readonly TValue GetValueRefOrNullRefCore(int key)
- {
- if (key <= _max)
- {
- int[] keys = _keys;
- for (int i = 0; i < keys.Length; i++)
- {
- if (key <= keys[i])
- {
- if (key < keys[i])
- {
- break;
- }
-
- return ref _values[i];
- }
- }
- }
-
- return ref Unsafe.NullRef<TValue>();
- }
- }
-}
+++ /dev/null
-// 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.Generic;
-using System.Diagnostics;
-using System.Linq;
-
-namespace System.Collections.Frozen
-{
- /// <summary>Provides a frozen set to use when the value is an <see cref="int"/>, the default comparer is used, and the item count is small.</summary>
- /// <remarks>
- /// No hashing here, just a straight-up linear scan through the items.
- /// </remarks>
- internal sealed class SmallInt32FrozenSet : FrozenSetInternalBase<int, SmallInt32FrozenSet.GSW>
- {
- private readonly int[] _items;
- private readonly int _max;
-
- internal SmallInt32FrozenSet(HashSet<int> source) : base(EqualityComparer<int>.Default)
- {
- Debug.Assert(source.Count != 0);
- Debug.Assert(ReferenceEquals(source.Comparer, EqualityComparer<int>.Default));
-
- int[] items = source.ToArray();
- Array.Sort(items);
-
- _items = items;
- _max = _items[_items.Length - 1];
- }
-
- private protected override int[] ItemsCore => _items;
- private protected override Enumerator GetEnumeratorCore() => new Enumerator(_items);
- private protected override int CountCore => _items.Length;
-
- private protected override int FindItemIndex(int item)
- {
- if (item <= _max)
- {
- int[] items = _items;
- for (int i = 0; i < items.Length; i++)
- {
- if (item <= items[i])
- {
- if (item < items[i])
- {
- break;
- }
-
- return i;
- }
- }
- }
-
- return -1;
- }
-
- internal struct GSW : IGenericSpecializedWrapper
- {
- private SmallInt32FrozenSet _set;
- public void Store(FrozenSet<int> set) => _set = (SmallInt32FrozenSet)set;
-
- public int Count => _set.Count;
- public IEqualityComparer<int> Comparer => _set.Comparer;
- public int FindItemIndex(int item) => _set.FindItemIndex(item);
- public Enumerator GetEnumerator() => _set.GetEnumerator();
- }
- }
-}
+++ /dev/null
-// 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.Generic;
-using System.Diagnostics;
-using System.Linq;
-using System.Numerics;
-using System.Runtime.CompilerServices;
-
-namespace System.Collections.Frozen
-{
- /// <summary>Provides a frozen dictionary to use when the key is an integer, the default comparer is used, and the item count is small.</summary>
- /// <remarks>
- /// No hashing here, just a straight-up linear scan through the items.
- /// </remarks>
- internal sealed class SmallIntegerFrozenDictionary<TKey, TValue> : FrozenDictionary<TKey, TValue>
- where TKey : struct, IBinaryInteger<TKey>
- {
- private readonly TKey[] _keys;
- private readonly TValue[] _values;
- private readonly TKey _max;
-
- internal SmallIntegerFrozenDictionary(Dictionary<TKey, TValue> source) : base(EqualityComparer<TKey>.Default)
- {
- Debug.Assert(source.Count != 0);
- Debug.Assert(ReferenceEquals(source.Comparer, EqualityComparer<TKey>.Default));
-
- _keys = source.Keys.ToArray();
- _values = source.Values.ToArray();
- Array.Sort(_keys, _values);
-
- _max = _keys[^1];
- }
-
- private protected override TKey[] KeysCore => _keys;
- private protected override TValue[] ValuesCore => _values;
- private protected override Enumerator GetEnumeratorCore() => new Enumerator(_keys, _values);
- private protected override int CountCore => _keys.Length;
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private protected override ref readonly TValue GetValueRefOrNullRefCore(TKey key)
- {
- if (key <= _max)
- {
- TKey[] keys = _keys;
- for (int i = 0; i < keys.Length; i++)
- {
- if (key <= keys[i])
- {
- if (key < keys[i])
- {
- break;
- }
-
- return ref _values[i];
- }
- }
- }
-
- return ref Unsafe.NullRef<TValue>();
- }
- }
-}
+++ /dev/null
-// 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.Generic;
-using System.Collections.Immutable;
-using System.Diagnostics;
-using System.Linq;
-using System.Numerics;
-
-namespace System.Collections.Frozen
-{
- /// <summary>Provides a frozen set to use when the value is an integer, the default comparer is used, and the item count is small.</summary>
- /// <remarks>
- /// No hashing here, just a straight-up linear scan through the items.
- /// </remarks>
- internal sealed class SmallIntegerFrozenSet<T> : FrozenSetInternalBase<T, SmallIntegerFrozenSet<T>.GSW>
- where T : struct, IBinaryInteger<T>
- {
- private readonly T[] _items;
- private readonly T _max;
-
- internal SmallIntegerFrozenSet(HashSet<T> source) : base(EqualityComparer<T>.Default)
- {
- Debug.Assert(source.Count != 0);
- Debug.Assert(ReferenceEquals(source.Comparer, EqualityComparer<T>.Default));
-
- T[] items = source.ToArray();
- Array.Sort(items);
-
- _items = items;
- _max = _items[^1];
- }
-
- private protected override T[] ItemsCore => _items;
- private protected override Enumerator GetEnumeratorCore() => new Enumerator(_items);
- private protected override int CountCore => _items.Length;
-
- private protected override int FindItemIndex(T item)
- {
- if (item <= _max)
- {
- T[] items = _items;
- for (int i = 0; i < items.Length; i++)
- {
- if (item <= items[i])
- {
- if (item < items[i])
- {
- break;
- }
-
- return i;
- }
- }
- }
-
- return -1;
- }
-
- internal struct GSW : IGenericSpecializedWrapper
- {
- private SmallIntegerFrozenSet<T> _set;
- public void Store(FrozenSet<T> set) => _set = (SmallIntegerFrozenSet<T>)set;
-
- public int Count => _set.Count;
- public IEqualityComparer<T> Comparer => _set.Comparer;
- public int FindItemIndex(T item) => _set.FindItemIndex(item);
- public Enumerator GetEnumerator() => _set.GetEnumerator();
- }
- }
-}
--- /dev/null
+// 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.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Runtime.CompilerServices;
+
+namespace System.Collections.Frozen
+{
+ /// <summary>Provides a frozen dictionary to use when the key is a comparable value type, the default comparer is used, and the item count is small.</summary>
+ /// <remarks>
+ /// No hashing involved, just a linear scan through the keys. This implementation is close in nature to that of <see cref="SmallComparableValueTypeFrozenDictionary{TKey, TValue}"/>,
+ /// except that this implementation sorts the keys in order to a) extract a max that it can compare against at the beginning of each match in order to
+ /// immediately rule out keys too large to be contained, and b) early-exits from the linear scan when a comparison determines the key is too
+ /// small to be contained.
+ /// </remarks>
+ internal sealed class SmallComparableValueTypeFrozenDictionary<TKey, TValue> : FrozenDictionary<TKey, TValue>
+ where TKey : notnull
+ {
+ private readonly TKey[] _keys;
+ private readonly TValue[] _values;
+ private readonly TKey _max;
+
+ internal SmallComparableValueTypeFrozenDictionary(Dictionary<TKey, TValue> source) : base(EqualityComparer<TKey>.Default)
+ {
+ // TKey is logically constrained to `where TKey : struct, IComparable<TKey>`, but we can't actually write that
+ // constraint currently and still have this be used from the calling context that has an unconstrained TKey.
+ // So, we assert it here instead. The implementation relies on {Equality}Comparer<TKey>.Default to sort things out.
+ Debug.Assert(default(TKey) is IComparable<TKey>);
+ Debug.Assert(default(TKey) is not null);
+ Debug.Assert(typeof(TKey).IsValueType);
+
+ Debug.Assert(source.Count != 0);
+ Debug.Assert(ReferenceEquals(source.Comparer, EqualityComparer<TKey>.Default));
+
+ _keys = source.Keys.ToArray();
+ _values = source.Values.ToArray();
+ Array.Sort(_keys, _values);
+
+ _max = _keys[_keys.Length - 1];
+ }
+
+ private protected override TKey[] KeysCore => _keys;
+ private protected override TValue[] ValuesCore => _values;
+ private protected override Enumerator GetEnumeratorCore() => new Enumerator(_keys, _values);
+ private protected override int CountCore => _keys.Length;
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private protected override ref readonly TValue GetValueRefOrNullRefCore(TKey key)
+ {
+ if (Comparer<TKey>.Default.Compare(key, _max) <= 0)
+ {
+ TKey[] keys = _keys;
+ for (int i = 0; i < keys.Length; i++)
+ {
+ int c = Comparer<TKey>.Default.Compare(key, keys[i]);
+ if (c <= 0)
+ {
+ if (c == 0)
+ {
+ return ref _values[i];
+ }
+
+ break;
+ }
+ }
+ }
+
+ return ref Unsafe.NullRef<TValue>();
+ }
+ }
+}
--- /dev/null
+// 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.Generic;
+using System.Collections.Immutable;
+using System.Diagnostics;
+using System.Linq;
+
+namespace System.Collections.Frozen
+{
+ /// <summary>Provides a frozen set to use when the item is a comparable value type, the default comparer is used, and the item count is small.</summary>
+ /// <remarks>
+ /// No hashing involved, just a linear scan through the items. This implementation is close in nature to that of <see cref="ValueTypeDefaultComparerFrozenSet{T}"/>,
+ /// except that this implementation sorts the values in order to a) extract a max that it can compare against at the beginning of each match in order to
+ /// immediately rule out values too large to be contained, and b) early-exits from the linear scan when a comparison determines the value is too
+ /// small to be contained.
+ /// </remarks>
+ internal sealed class SmallComparableValueTypeFrozenSet<T> : FrozenSetInternalBase<T, SmallComparableValueTypeFrozenSet<T>.GSW>
+ {
+ private readonly T[] _items;
+ private readonly T _max;
+
+ internal SmallComparableValueTypeFrozenSet(HashSet<T> source) : base(EqualityComparer<T>.Default)
+ {
+ // T is logically constrained to `where T : struct, IComparable<T>`, but we can't actually write that
+ // constraint currently and still have this be used from the calling context that has an unconstrained T.
+ // So, we assert it here instead. The implementation relies on {Equality}Comparer<T>.Default to sort things out.
+ Debug.Assert(default(T) is IComparable<T>);
+ Debug.Assert(default(T) is not null);
+ Debug.Assert(typeof(T).IsValueType);
+
+ Debug.Assert(source.Count != 0);
+ Debug.Assert(ReferenceEquals(source.Comparer, EqualityComparer<T>.Default));
+
+ _items = source.ToArray();
+ Array.Sort(_items);
+
+ _max = _items[_items.Length - 1];
+ }
+
+ private protected override T[] ItemsCore => _items;
+ private protected override Enumerator GetEnumeratorCore() => new Enumerator(_items);
+ private protected override int CountCore => _items.Length;
+
+ private protected override int FindItemIndex(T item)
+ {
+ if (Comparer<T>.Default.Compare(item, _max) <= 0)
+ {
+ T[] items = _items;
+ for (int i = 0; i < items.Length; i++)
+ {
+ int c = Comparer<T>.Default.Compare(item, items[i]);
+ if (c <= 0)
+ {
+ if (c == 0)
+ {
+ return i;
+ }
+
+ break;
+ }
+ }
+ }
+
+ return -1;
+ }
+
+ internal struct GSW : IGenericSpecializedWrapper
+ {
+ private SmallComparableValueTypeFrozenSet<T> _set;
+ public void Store(FrozenSet<T> set) => _set = (SmallComparableValueTypeFrozenSet<T>)set;
+
+ public int Count => _set.Count;
+ public IEqualityComparer<T> Comparer => _set.Comparer;
+ public int FindItemIndex(T item) => _set.FindItemIndex(item);
+ public Enumerator GetEnumerator() => _set.GetEnumerator();
+ }
+ }
+}
namespace System.Collections.Frozen
{
+ /// <summary><see cref="FrozenDictionary{TKey, TValue}"/> implementation that just wraps a <see cref="Dictionary{TKey, TValue}"/>.</summary>
internal sealed class WrappedDictionaryFrozenDictionary<TKey, TValue> :
FrozenDictionary<TKey, TValue>, IDictionary<TKey, TValue>, IEnumerable<KeyValuePair<TKey, TValue>>
where TKey : notnull
{
+ // Note that while most of the FrozenDictionary implementations have an equivalent FrozenSet implementation,
+ // there's no corresponding WrappedHashSetFrozenSet<T> because HashSet<T> doesn't provide a way to implement
+ // FrozenSet<T>.FindItemIndex.
+
private readonly Dictionary<TKey, TValue> _source;
private TKey[]? _keys;
private TValue[]? _values;
protected override IDictionary<TKey, TValue> GenericIDictionaryFactory(int count) =>
GenericIDictionaryFactory(count, optimizeForReading: true);
+ protected virtual bool AllowVeryLargeSizes => true;
+
protected virtual IDictionary<TKey, TValue> GenericIDictionaryFactory(int count, bool optimizeForReading)
{
var d = new Dictionary<TKey, TValue>();
[Theory]
[InlineData(100_000, false)]
[InlineData(100_000, true)]
- public void CreateVeryLargeDictionary_Success(int largeCount, bool optimizeForReading)
+ public virtual void CreateVeryLargeDictionary_Success(int largeCount, bool optimizeForReading)
{
- GenericIDictionaryFactory(largeCount, optimizeForReading);
+ if (AllowVeryLargeSizes)
+ {
+ GenericIDictionaryFactory(largeCount, optimizeForReading);
+ }
}
[Fact]
[InlineData(8_000_000, true)]
public void CreateHugeDictionary_Success(int largeCount, bool optimizeForReading)
{
- GenericIDictionaryFactory(largeCount, optimizeForReading);
+ if (AllowVeryLargeSizes)
+ {
+ GenericIDictionaryFactory(largeCount, optimizeForReading);
+ }
}
}
{
protected override KeyValuePair<SimpleClass, SimpleClass> CreateT(int seed)
{
- return new KeyValuePair<SimpleClass, SimpleClass>(CreateTKey(seed), CreateTKey(seed + 500));
+ return new KeyValuePair<SimpleClass, SimpleClass>(CreateTKey(seed), CreateTValue(seed + 500));
}
protected override SimpleClass CreateTKey(int seed)
Value.CompareTo(other.Value);
}
+ public class FrozenDictionary_Generic_Tests_SimpleStruct_int : FrozenDictionary_Generic_Tests<SimpleStruct, int>
+ {
+ protected override KeyValuePair<SimpleStruct, int> CreateT(int seed)
+ {
+ return new KeyValuePair<SimpleStruct, int>(CreateTKey(seed), CreateTValue(seed + 500));
+ }
+
+ protected override SimpleStruct CreateTKey(int seed) => new SimpleStruct { Value = seed + 1 };
+
+ protected override int CreateTValue(int seed) => seed;
+
+ protected override bool DefaultValueAllowed => true;
+
+ protected override bool AllowVeryLargeSizes => false; // hash code contention leads to longer running times
+ }
+
+ public struct SimpleStruct : IEquatable<SimpleStruct>, IComparable<SimpleStruct>
+ {
+ public int Value { get; set; }
+
+ public int CompareTo(SimpleStruct other) => Value.CompareTo(other.Value);
+
+ public bool Equals(SimpleStruct other) => Value == other.Value;
+
+ public override int GetHashCode() => 0; // to force hashcode contention in implementation
+
+ public override bool Equals([NotNullWhen(true)] object? obj) =>
+ obj is SimpleStruct other && Equals(other);
+ }
+
public sealed class NonDefaultEqualityComparer<TKey> : IEqualityComparer<TKey>
{
public static NonDefaultEqualityComparer<TKey> Instance { get; } = new();
var kvpArray = new KeyValuePair<string, int>[4];
((ICollection)frozen).CopyTo(kvpArray, 1);
Assert.Equal(new KeyValuePair<string, int>(null, 0), kvpArray[0]);
- Assert.Equal(new KeyValuePair<string, int>("hello", 123), kvpArray[1]);
- Assert.Equal(new KeyValuePair<string, int>("world", 456), kvpArray[2]);
+ Assert.True(
+ (kvpArray[1].Equals(new KeyValuePair<string, int>("hello", 123)) && kvpArray[2].Equals(new KeyValuePair<string, int>("world", 456))) ||
+ (kvpArray[2].Equals(new KeyValuePair<string, int>("hello", 123)) && kvpArray[1].Equals(new KeyValuePair<string, int>("world", 456))));
Assert.Equal(new KeyValuePair<string, int>(null, 0), kvpArray[3]);
var deArray = new DictionaryEntry[4];
((ICollection)frozen).CopyTo(deArray, 2);
Assert.Equal(new DictionaryEntry(null, null), deArray[0]);
Assert.Equal(new DictionaryEntry(null, null), deArray[1]);
- Assert.Equal(new DictionaryEntry("hello", 123), deArray[2]);
- Assert.Equal(new DictionaryEntry("world", 456), deArray[3]);
+ Assert.True(
+ (deArray[2].Equals(new DictionaryEntry("hello", 123)) && deArray[3].Equals(new DictionaryEntry("world", 456))) ||
+ (deArray[3].Equals(new DictionaryEntry("hello", 123)) && deArray[2].Equals(new DictionaryEntry("world", 456))));
}
[Fact]
// from https://raw.githubusercontent.com/dotnet/roslyn/0456b4adc6939e366e7c509318b3ac6a85cda496/src/Compilers/CSharp/Test/Emit2/CodeGen/CodeGenLengthBasedSwitchTests.cs
new[] { "", "a", "b", "c", "no", "yes", "four", "alice", "blurb", "hello", "lamps", "lambs", "lower", "names", "slurp", "towed", "words" },
+ new[] { "", "a", "b", "c", "no", "yes", "four", "alice", "blurb", "hello", "lamps", "lambs", "lower", "names", "slurp", "towed", "words", "\u03BB" }, // plus a non-ASCII char
new[] { "", "a", "b", "c", "no", "yes", "four", "alice", "blurb", "hello" },
new[] { "abcdefgh", "abcdefg", "abcdef", "abcde", "abcd", "abc", "ab", "a" },
Enumerable.Range(0, 100).Select(i => i.ToString("D2")).ToArray(),
Enumerable.Range(0, 10).Select(i => $"ABCDEFGH\U0001F600{i}").ToArray(), // right justified single char non-ascii
Enumerable.Range(0, 100).Select(i => $"{i:D2}ABCDEFGH\U0001F600").ToArray(), // left justified substring non-ascii
Enumerable.Range(0, 100).Select(i => $"ABCDEFGH\U0001F600{i:D2}").ToArray(), // right justified substring non-ascii
+ Enumerable.Range(0, 20).Select(i => i.ToString("D2")).Select(s => (char)(s[0] + 128) + "" + (char)(s[1] + 128)).ToArray(), // left-justified non-ascii
}
select new object[] { optimizeForReading, keys.ToDictionary(i => i, i => i, comparer) };
protected override ISet<T> GenericISetFactory(int count) =>
GenericISetFactory(count, optimizeForReading: true);
+ protected virtual bool TestLargeSizes => true;
+
protected virtual ISet<T> GenericISetFactory(int count, bool optimizeForReading)
{
var s = new HashSet<T>();
[InlineData(100_000, true)]
public void CreateVeryLargeSet_Success(int largeCount, bool optimizeForReading)
{
- GenericISetFactory(largeCount, optimizeForReading);
+ if (TestLargeSizes)
+ {
+ GenericISetFactory(largeCount, optimizeForReading);
+ }
}
[Fact]
[InlineData(5000, false)]
public void ComparingWithOtherSets(int size, bool optimizeForReading)
{
+ if (size > 10 && !TestLargeSizes)
+ {
+ return;
+ }
+
foreach (IEqualityComparer<T> comparer in new IEqualityComparer<T>[] { EqualityComparer<T>.Default })//, NonDefaultEqualityComparer<T>.Instance })
{
IEqualityComparer<T> otherComparer = ReferenceEquals(comparer, EqualityComparer<T>.Default) ? NonDefaultEqualityComparer<T>.Instance : EqualityComparer<T>.Default;
}
}
+ public class FrozenSet_Generic_Tests_SimpleStruct : FrozenSet_Generic_Tests<SimpleStruct>
+ {
+ protected override SimpleStruct CreateT(int seed) => new SimpleStruct { Value = seed + 1 };
+
+ protected override bool TestLargeSizes => false; // hash code contention leads to longer running times
+ }
+
public class FrozenSet_NonGeneric_Tests : ICollection_NonGeneric_Tests
{
protected override ICollection NonGenericICollectionFactory() =>