Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,16 @@ The System.Collections.Immutable library is built-in as part of the shared frame
</PropertyGroup>

<ItemGroup>
<Compile Include="System\Collections\Frozen\Byte\ByteFrozenSet.cs" />
<Compile Include="System\Collections\Frozen\Char\Latin1CharFrozenSet.cs" />
<Compile Include="System\Collections\Frozen\Char\PerfectHashCharFrozenSet.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="$(CoreLibSharedDir)System\Collections\Generic\DebugViewDictionaryItem.cs" Link="Common\System\Collections\Generic\DebugViewDictionaryItem.cs" />
<Compile Include="$(CoreLibSharedDir)System\Collections\Generic\IDictionaryDebugView.cs" Link="Common\System\Collections\Generic\IDictionaryDebugView.cs" />
<Compile Include="$(CoreLibSharedDir)System\SearchValues\BitVector256.cs" Link="Common\System\SearchValues\BitVector256.cs" />
<Compile Include="$(CoreLibSharedDir)System\SearchValues\PerfectHashLookup.cs" Link="Common\System\SearchValues\PerfectHashLookup.cs" />
<Compile Include="System\Collections\Frozen\Constants.cs" />
<Compile Include="System\Collections\Frozen\DefaultFrozenDictionary.cs" />
<Compile Include="System\Collections\Frozen\DefaultFrozenSet.cs" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Buffers;
using System.Collections.Generic;
using System.Diagnostics;

namespace System.Collections.Frozen
{
/// <summary>Provides a frozen set to use when the values are <see cref="byte"/>s and the default comparer is used.</summary>
internal sealed partial class ByteFrozenSet : FrozenSetInternalBase<byte, ByteFrozenSet.GSW>
{
private readonly BitVector256 _values;
private readonly int _count;
private byte[]? _items;

internal ByteFrozenSet(ReadOnlySpan<byte> values) : base(EqualityComparer<byte>.Default)
{
Debug.Assert(!values.IsEmpty);

foreach (byte b in values)
{
_values.Set(b);
}

_count = _values.Count();
}

internal ByteFrozenSet(IEnumerable<byte> values) : base(EqualityComparer<byte>.Default)
{
foreach (byte b in values)
{
_values.Set(b);
}

_count = _values.Count();

Debug.Assert(_count > 0);
}

/// <inheritdoc />
private protected override byte[] ItemsCore => _items ??= _values.GetByteValues();

/// <inheritdoc />
private protected override Enumerator GetEnumeratorCore() => new Enumerator(ItemsCore);

/// <inheritdoc />
private protected override int CountCore => _count;

/// <inheritdoc />
private protected override int FindItemIndex(byte item) => _values.IndexOf(item);

/// <inheritdoc />
private protected override bool ContainsCore(byte item) => _values.Contains(item);

internal struct GSW : IGenericSpecializedWrapper
{
private ByteFrozenSet _set;
public void Store(FrozenSet<byte> set) => _set = (ByteFrozenSet)set;

public int Count => _set.Count;
public IEqualityComparer<byte> Comparer => _set.Comparer;
public int FindItemIndex(byte item) => _set.FindItemIndex(item);
public Enumerator GetEnumerator() => _set.GetEnumerator();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Buffers;
using System.Collections.Generic;
using System.Diagnostics;

namespace System.Collections.Frozen
{
/// <summary>Provides a frozen set to use when the values are <see cref="char"/>s, the default comparer is used, and all values are &lt;= 255.</summary>
/// <remarks>
/// This should practically always be used with ASCII-only values, but since we're using the <see cref="BitVector256"/> helper,
/// we might as well use it for values between 128-255 too (hence Latin1 instead of Ascii in the name).
/// </remarks>
internal sealed partial class Latin1CharFrozenSet : FrozenSetInternalBase<char, Latin1CharFrozenSet.GSW>
{
private readonly BitVector256 _values;
private readonly int _count;
private char[]? _items;

internal Latin1CharFrozenSet(ReadOnlySpan<char> values) : base(EqualityComparer<char>.Default)
{
Debug.Assert(!values.IsEmpty);

foreach (char c in values)
{
if (c > 255)
{
// Source was modified concurrent with the call to FrozenSet.Create.
ThrowHelper.ThrowInvalidOperationException();
}

_values.Set(c);
}

_count = _values.Count();
}

/// <inheritdoc />
private protected override char[] ItemsCore => _items ??= _values.GetCharValues();

/// <inheritdoc />
private protected override Enumerator GetEnumeratorCore() => new Enumerator(ItemsCore);

/// <inheritdoc />
private protected override int CountCore => _count;

/// <inheritdoc />
private protected override int FindItemIndex(char item) => _values.IndexOf(item);

/// <inheritdoc />
private protected override bool ContainsCore(char item) => _values.Contains256(item);

internal struct GSW : IGenericSpecializedWrapper
{
private Latin1CharFrozenSet _set;
public void Store(FrozenSet<char> set) => _set = (Latin1CharFrozenSet)set;

public int Count => _set.Count;
public IEqualityComparer<char> Comparer => _set.Comparer;
public int FindItemIndex(char item) => _set.FindItemIndex(item);
public Enumerator GetEnumerator() => _set.GetEnumerator();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Buffers;
using System.Collections.Generic;
using System.Diagnostics;

namespace System.Collections.Frozen
{
/// <summary>Provides a frozen set to use when the values are <see cref="char"/>s, the default comparer is used, and some values are &gt;= 255.</summary>
/// <remarks>
/// This is logically similar to using a <see cref="FrozenHashTable"/>, but where that is bucketized,
/// <see cref="PerfectHashLookup"/> is not, so we can avoid some indirection during lookups.
/// The latter is also specialized for <see cref="char"/> values, with lower memory consumption and a slightly cheaper FastMod.
/// </remarks>
internal sealed partial class PerfectHashCharFrozenSet : FrozenSetInternalBase<char, PerfectHashCharFrozenSet.GSW>
{
private readonly uint _multiplier;
private readonly char[] _hashEntries;
private char[]? _items;

internal PerfectHashCharFrozenSet(ReadOnlySpan<char> values) : base(EqualityComparer<char>.Default)
{
Debug.Assert(!values.IsEmpty);

int max = 0;
foreach (char c in values)
{
max = Math.Max(max, c);
}

PerfectHashLookup.Initialize(values, max, out _multiplier, out _hashEntries);
}

private char[] AllocateItemsArray()
{
var set = new HashSet<char>(_hashEntries);
char[] items = new char[set.Count];
set.CopyTo(items);
_items = items;
return items;
}

/// <inheritdoc />
private protected override char[] ItemsCore => _items ?? AllocateItemsArray();

/// <inheritdoc />
private protected override Enumerator GetEnumeratorCore() => new Enumerator(ItemsCore);

/// <inheritdoc />
private protected override int CountCore => ItemsCore.Length;

/// <inheritdoc />
/// <remarks>
/// This is an internal helper where results are not exposed to the user.
/// The returned index does not have to correspond to the value in the <see cref="ItemsCore"/> array.
/// In this case, calculating the real index would be costly, so we return the offset into <see cref="_hashEntries"/> instead.
/// </remarks>
private protected override int FindItemIndex(char item) => PerfectHashLookup.IndexOf(_hashEntries, _multiplier, item);

/// <inheritdoc />
private protected override bool ContainsCore(char item) => PerfectHashLookup.Contains(_hashEntries, _multiplier, item);

/// <inheritdoc />
/// <remarks>
/// We're overriding this method to account for the fact that the indexes returned by <see cref="FindItemIndex(char)"/>
/// are based on <see cref="_hashEntries"/> instead of <see cref="ItemsCore"/>.
/// </remarks>
private protected override KeyValuePair<int, int> CheckUniqueAndUnfoundElements(IEnumerable<char> other, bool returnIfUnfound) =>
CheckUniqueAndUnfoundElements(other, returnIfUnfound, _hashEntries.Length);

internal struct GSW : IGenericSpecializedWrapper
{
private PerfectHashCharFrozenSet _set;
public void Store(FrozenSet<char> set) => _set = (PerfectHashCharFrozenSet)set;

public int Count => _set.Count;
public IEqualityComparer<char> Comparer => _set.Comparer;
public int FindItemIndex(char item) => _set.FindItemIndex(item);
public Enumerator GetEnumerator() => _set.GetEnumerator();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,27 @@ public static class FrozenSet
/// <returns>A frozen set.</returns>
public static FrozenSet<T> Create<T>(IEqualityComparer<T>? equalityComparer, params ReadOnlySpan<T> source)
{
bool isDefaultComparer = equalityComparer is null || ReferenceEquals(equalityComparer, FrozenSet<T>.Empty.Comparer);

if (source.Length == 0)
{
return equalityComparer is null || ReferenceEquals(equalityComparer, FrozenSet<T>.Empty.Comparer) ?
return isDefaultComparer ?
FrozenSet<T>.Empty :
new EmptyFrozenSet<T>(equalityComparer);
new EmptyFrozenSet<T>(equalityComparer!);
}

#if NET9_0_OR_GREATER
if (typeof(T) == typeof(byte) && isDefaultComparer)
{
return (FrozenSet<T>)(object)new ByteFrozenSet(Unsafe.BitCast<ReadOnlySpan<T>, ReadOnlySpan<byte>>(source));
}

if (typeof(T) == typeof(char) && isDefaultComparer)
{
return (FrozenSet<T>)(object)CreateFrozenSetForChars(Unsafe.BitCast<ReadOnlySpan<T>, ReadOnlySpan<char>>(source), null);
}
#endif

HashSet<T> set =
#if NET
new(source.Length, equalityComparer); // we assume there are few-to-no duplicates when using this API
Expand Down Expand Up @@ -71,6 +85,31 @@ public static FrozenSet<T> ToFrozenSet<T>(this IEnumerable<T> source, IEqualityC
return fs;
}

if (typeof(T) == typeof(byte) && EqualityComparer<byte>.Default.Equals(comparer))
{
newSet = null;

if (source is not ICollection<byte> { Count: > 0 })
{
newSet = new HashSet<T>(source);

if (newSet.Count == 0)
{
return FrozenSet<T>.Empty;
}

source = newSet;
}

return (FrozenSet<T>)(object)new ByteFrozenSet((IEnumerable<byte>)source);
}

if (typeof(T) == typeof(char) && EqualityComparer<char>.Default.Equals(comparer))
{
newSet = null;
return (FrozenSet<T>)(object)CreateFrozenSetForChars(default, (IEnumerable<char>)source);
}

// Ensure we have a HashSet<> using the specified comparer such that all items
// are non-null and unique according to that comparer.
newSet = source as HashSet<T>;
Expand Down Expand Up @@ -222,6 +261,63 @@ private static FrozenSet<T> CreateFromSet<T>(HashSet<T> source)
// No special-cases apply. Use the default frozen set.
return new DefaultFrozenSet<T>(source);
}

private static FrozenSet<char> CreateFrozenSetForChars(ReadOnlySpan<char> span, IEnumerable<char>? enumerable)
{
Debug.Assert(span.IsEmpty || enumerable is not null);

// Extract the span from the enumerable. In most cases that should be free.
if (enumerable is not null)
{
if (enumerable is string s)
{
span = s;
}
else if (enumerable is char[] array)
{
span = array;
}
else
{
if (enumerable is not List<char> list)
{
list = new List<char>(enumerable);
}

#if NET8_0_OR_GREATER
span = CollectionsMarshal.AsSpan(list);
#else
span = list.ToArray();
#endif
}
}

if (span.IsEmpty)
{
return FrozenSet<char>.Empty;
}

#if NET8_0_OR_GREATER
bool allValuesLessThan256 = !span.ContainsAnyExceptInRange((char)0, (char)255);
#else
bool allValuesLessThan256 = true;
foreach (char c in span)
{
if (c > 255)
{
allValuesLessThan256 = false;
break;
}
}
#endif

if (allValuesLessThan256)
{
return new Latin1CharFrozenSet(span);
}

return new PerfectHashCharFrozenSet(span);
}
}

/// <summary>Provides an immutable, read-only set optimized for fast lookup and enumeration.</summary>
Expand Down Expand Up @@ -305,6 +401,10 @@ void ICollection.CopyTo(Array array, int index)
/// <param name="item">The element to locate.</param>
/// <returns><see langword="true"/> if the set contains the specified element; otherwise, <see langword="false"/>.</returns>
public bool Contains(T item) =>
ContainsCore(item);

/// <inheritdoc cref="Contains(T)"/>
private protected virtual bool ContainsCore(T item) =>
FindItemIndex(item) >= 0;

/// <summary>Searches the set for a given value and returns the equal value it finds, if any.</summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -187,12 +187,15 @@ private bool ComparersAreCompatible(IReadOnlySet<T> other) =>
/// than _count; i.e. everything in other was in this and this had at least one element
/// not contained in other.
/// </remarks>
private unsafe KeyValuePair<int, int> CheckUniqueAndUnfoundElements(IEnumerable<T> other, bool returnIfUnfound)
private protected virtual KeyValuePair<int, int> CheckUniqueAndUnfoundElements(IEnumerable<T> other, bool returnIfUnfound) =>
CheckUniqueAndUnfoundElements(other, returnIfUnfound, _thisSet.Count);

private protected KeyValuePair<int, int> CheckUniqueAndUnfoundElements(IEnumerable<T> other, bool returnIfUnfound, int itemCount)
{
Debug.Assert(_thisSet.Count != 0, "EmptyFrozenSet should have been used.");
Debug.Assert(itemCount != 0, "EmptyFrozenSet should have been used.");

const int BitsPerInt32 = 32;
int intArrayLength = (_thisSet.Count / BitsPerInt32) + 1;
int intArrayLength = (itemCount / BitsPerInt32) + 1;

int[]? rentedArray = null;
Span<int> seenItems = intArrayLength <= 128 ?
Expand Down
Loading
Loading