From 338bea62b5169b0ded4afc165b64651ee3957476 Mon Sep 17 00:00:00 2001 From: Greg Date: Mon, 21 Aug 2023 22:28:25 -0500 Subject: [PATCH 1/4] Added pooled connection library (#99) --- .../.idea/indexLayout.xml | 8 + .../.idea/workspace.xml | 25 + OctaneEngine/Collections.Pooled/BitHelper.cs | 66 + OctaneEngine/Collections.Pooled/ClearMode.cs | 37 + .../HashHelpers.SerializationInfoTable.cs | 29 + .../Collections.Pooled/HashHelpers.cs | 93 + .../ICollectionDebugView.cs | 31 + .../IDictionaryDebugView.cs | 73 + .../Collections.Pooled/IReadOnlyPooledList.cs | 18 + .../NonRandomizedStringEqualityComparer.cs | 77 + .../Collections.Pooled/PooledDictionary.cs | 2029 ++++++++++++ .../Collections.Pooled/PooledExtensions.cs | 335 ++ OctaneEngine/Collections.Pooled/PooledList.cs | 1665 ++++++++++ .../Collections.Pooled/PooledQueue.cs | 768 +++++ OctaneEngine/Collections.Pooled/PooledSet.cs | 2724 +++++++++++++++++ .../PooledSetEqualityComparer.cs | 63 + .../Collections.Pooled/PooledStack.cs | 694 +++++ .../Collections.Pooled/QueueDebugView.cs | 28 + .../Collections.Pooled/StackDebugView.cs | 28 + .../Collections.Pooled/ThrowHelper.cs | 692 +++++ OctaneEngine/OctaneEngine.csproj | 1 - 21 files changed, 9483 insertions(+), 1 deletion(-) create mode 100644 OctaneEngine/.idea/.idea.OctaneEngine.dir/.idea/indexLayout.xml create mode 100644 OctaneEngine/.idea/.idea.OctaneEngine.dir/.idea/workspace.xml create mode 100644 OctaneEngine/Collections.Pooled/BitHelper.cs create mode 100644 OctaneEngine/Collections.Pooled/ClearMode.cs create mode 100644 OctaneEngine/Collections.Pooled/HashHelpers.SerializationInfoTable.cs create mode 100644 OctaneEngine/Collections.Pooled/HashHelpers.cs create mode 100644 OctaneEngine/Collections.Pooled/ICollectionDebugView.cs create mode 100644 OctaneEngine/Collections.Pooled/IDictionaryDebugView.cs create mode 100644 OctaneEngine/Collections.Pooled/IReadOnlyPooledList.cs create mode 100644 OctaneEngine/Collections.Pooled/NonRandomizedStringEqualityComparer.cs create mode 100644 OctaneEngine/Collections.Pooled/PooledDictionary.cs create mode 100644 OctaneEngine/Collections.Pooled/PooledExtensions.cs create mode 100644 OctaneEngine/Collections.Pooled/PooledList.cs create mode 100644 OctaneEngine/Collections.Pooled/PooledQueue.cs create mode 100644 OctaneEngine/Collections.Pooled/PooledSet.cs create mode 100644 OctaneEngine/Collections.Pooled/PooledSetEqualityComparer.cs create mode 100644 OctaneEngine/Collections.Pooled/PooledStack.cs create mode 100644 OctaneEngine/Collections.Pooled/QueueDebugView.cs create mode 100644 OctaneEngine/Collections.Pooled/StackDebugView.cs create mode 100644 OctaneEngine/Collections.Pooled/ThrowHelper.cs diff --git a/OctaneEngine/.idea/.idea.OctaneEngine.dir/.idea/indexLayout.xml b/OctaneEngine/.idea/.idea.OctaneEngine.dir/.idea/indexLayout.xml new file mode 100644 index 0000000..7b08163 --- /dev/null +++ b/OctaneEngine/.idea/.idea.OctaneEngine.dir/.idea/indexLayout.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/OctaneEngine/.idea/.idea.OctaneEngine.dir/.idea/workspace.xml b/OctaneEngine/.idea/.idea.OctaneEngine.dir/.idea/workspace.xml new file mode 100644 index 0000000..bdd2997 --- /dev/null +++ b/OctaneEngine/.idea/.idea.OctaneEngine.dir/.idea/workspace.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/OctaneEngine/Collections.Pooled/BitHelper.cs b/OctaneEngine/Collections.Pooled/BitHelper.cs new file mode 100644 index 0000000..0470daa --- /dev/null +++ b/OctaneEngine/Collections.Pooled/BitHelper.cs @@ -0,0 +1,66 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Runtime.CompilerServices; + +namespace Collections.Pooled +{ + internal ref struct BitHelper + { + private const int IntSize = sizeof(int) * 8; + private readonly Span _span; + + internal BitHelper(Span span, bool clear) + { + if (clear) + { + span.Clear(); + } + _span = span; + } + + internal void MarkBit(int bitPosition) + { + int bitArrayIndex = bitPosition / IntSize; + if ((uint)bitArrayIndex < (uint)_span.Length) + { + _span[bitArrayIndex] |= (1 << (bitPosition % IntSize)); + } + } + + internal bool IsMarked(int bitPosition) + { + int bitArrayIndex = bitPosition / IntSize; + return + (uint)bitArrayIndex < (uint)_span.Length && + (_span[bitArrayIndex] & (1 << (bitPosition % IntSize))) != 0; + } + + internal int FindFirstUnmarked(int startPosition = 0) + { + int i = startPosition; + for (int bi = i / IntSize; (uint)bi < (uint)_span.Length; bi = ++i / IntSize) + { + if ((_span[bi] & (1 << (i % IntSize))) == 0) + return i; + } + return -1; + } + + internal int FindFirstMarked(int startPosition = 0) + { + int i = startPosition; + for (int bi = i / IntSize; (uint)bi < (uint)_span.Length; bi = ++i / IntSize) + { + if ((_span[bi] & (1 << (i % IntSize))) != 0) + return i; + } + return -1; + } + + /// How many ints must be allocated to represent n bits. Returns (n+31)/32, but avoids overflow. + internal static int ToIntArrayLength(int n) => n > 0 ? ((n - 1) / IntSize + 1) : 0; + } +} diff --git a/OctaneEngine/Collections.Pooled/ClearMode.cs b/OctaneEngine/Collections.Pooled/ClearMode.cs new file mode 100644 index 0000000..3e5b4e0 --- /dev/null +++ b/OctaneEngine/Collections.Pooled/ClearMode.cs @@ -0,0 +1,37 @@ +namespace Collections.Pooled +{ + /// + /// This enum allows control over how data is treated when internal + /// arrays are returned to the ArrayPool. Be careful to understand + /// what each option does before using anything other than the default + /// of Auto. + /// + public enum ClearMode + { + /// + /// Auto has different behavior depending on the host project's target framework. + /// .NET Core 2.1: Reference types and value types that contain reference types are cleared + /// when the internal arrays are returned to the pool. Value types that do not contain reference + /// types are not cleared when returned to the pool. + /// .NET Standard 2.0: All user types are cleared before returning to the pool, in case they + /// contain reference types. + /// For .NET Standard, Auto and Always have the same behavior. + /// + Auto = 0, + /// + /// The Always setting has the effect of always clearing user types before returning to the pool. + /// This is the default behavior on .NET Standard.You might want to turn this on in a .NET Core project + /// if you were concerned about sensitive data stored in value types leaking to other pars of your application. + /// + Always = 1, + /// + /// Never will cause pooled collections to never clear user types before returning them to the pool. + /// You might want to use this setting in a .NET Standard project when you know that a particular collection stores + /// only value types and you want the performance benefit of not taking time to reset array items to their default value. + /// Be careful with this setting: if used for a collection that contains reference types, or value types that contain + /// reference types, this setting could cause memory issues by making the garbage collector unable to clean up instances + /// that are still being referenced by arrays sitting in the ArrayPool. + /// + Never = 2 + } +} diff --git a/OctaneEngine/Collections.Pooled/HashHelpers.SerializationInfoTable.cs b/OctaneEngine/Collections.Pooled/HashHelpers.SerializationInfoTable.cs new file mode 100644 index 0000000..86add66 --- /dev/null +++ b/OctaneEngine/Collections.Pooled/HashHelpers.SerializationInfoTable.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +// Used by Hashtable and Dictionary's SeralizationInfo .ctor's to store the SeralizationInfo +// object until OnDeserialization is called. + +using System.Threading; +using System.Runtime.CompilerServices; +using System.Runtime.Serialization; + +namespace Collections.Pooled +{ + internal static partial class HashHelpers + { + private static ConditionalWeakTable s_serializationInfoTable; + + public static ConditionalWeakTable SerializationInfoTable + { + get + { + if (s_serializationInfoTable == null) + Interlocked.CompareExchange(ref s_serializationInfoTable, new ConditionalWeakTable(), null); + + return s_serializationInfoTable; + } + } + } +} \ No newline at end of file diff --git a/OctaneEngine/Collections.Pooled/HashHelpers.cs b/OctaneEngine/Collections.Pooled/HashHelpers.cs new file mode 100644 index 0000000..c915084 --- /dev/null +++ b/OctaneEngine/Collections.Pooled/HashHelpers.cs @@ -0,0 +1,93 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System; +using System.Diagnostics; + +namespace Collections.Pooled +{ + internal static partial class HashHelpers + { + public const int HashCollisionThreshold = 100; + + // This is the maximum prime smaller than Array.MaxArrayLength + public const int MaxPrimeArrayLength = 0x7FEFFFFD; + + 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 readonly int[] 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("Cannot get the next prime from a negative number."); + + for (int i = 0; i < primes.Length; i++) + { + int prime = primes[i]; + 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; + } + + // Returns size of hashtable to grow to. + public static int ExpandPrime(int oldSize) + { + int newSize = 2 * oldSize; + + // Allow the hashtables to grow to maximum possible size (~2G elements) before encountering capacity overflow. + // Note that this check works even when _items.Length overflowed thanks to the (uint) cast + if ((uint)newSize > MaxPrimeArrayLength && MaxPrimeArrayLength > oldSize) + { + Debug.Assert(MaxPrimeArrayLength == GetPrime(MaxPrimeArrayLength), "Invalid MaxPrimeArrayLength"); + return MaxPrimeArrayLength; + } + + return GetPrime(newSize); + } + } +} diff --git a/OctaneEngine/Collections.Pooled/ICollectionDebugView.cs b/OctaneEngine/Collections.Pooled/ICollectionDebugView.cs new file mode 100644 index 0000000..2471d42 --- /dev/null +++ b/OctaneEngine/Collections.Pooled/ICollectionDebugView.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; + +namespace Collections.Pooled +{ + internal sealed class ICollectionDebugView + { + private readonly ICollection _collection; + + public ICollectionDebugView(ICollection collection) + { + _collection = collection ?? throw new ArgumentNullException(nameof(collection)); + } + + [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] + public T[] Items + { + get + { + T[] items = new T[_collection.Count]; + _collection.CopyTo(items, 0); + return items; + } + } + } +} diff --git a/OctaneEngine/Collections.Pooled/IDictionaryDebugView.cs b/OctaneEngine/Collections.Pooled/IDictionaryDebugView.cs new file mode 100644 index 0000000..1f22664 --- /dev/null +++ b/OctaneEngine/Collections.Pooled/IDictionaryDebugView.cs @@ -0,0 +1,73 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; + +namespace Collections.Pooled +{ + internal sealed class IDictionaryDebugView + { + private readonly IDictionary _dict; + + public IDictionaryDebugView(IDictionary dictionary) + { + _dict = dictionary ?? throw new ArgumentNullException(nameof(dictionary)); + } + + [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] + public KeyValuePair[] Items + { + get + { + KeyValuePair[] items = new KeyValuePair[_dict.Count]; + _dict.CopyTo(items, 0); + return items; + } + } + } + + internal sealed class DictionaryKeyCollectionDebugView + { + private readonly ICollection _collection; + + public DictionaryKeyCollectionDebugView(ICollection collection) + { + _collection = collection ?? throw new ArgumentNullException(nameof(collection)); + } + + [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] + public TKey[] Items + { + get + { + TKey[] items = new TKey[_collection.Count]; + _collection.CopyTo(items, 0); + return items; + } + } + } + + internal sealed class DictionaryValueCollectionDebugView + { + private readonly ICollection _collection; + + public DictionaryValueCollectionDebugView(ICollection collection) + { + _collection = collection ?? throw new ArgumentNullException(nameof(collection)); + } + + [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] + public TValue[] Items + { + get + { + TValue[] items = new TValue[_collection.Count]; + _collection.CopyTo(items, 0); + return items; + } + } + } +} diff --git a/OctaneEngine/Collections.Pooled/IReadOnlyPooledList.cs b/OctaneEngine/Collections.Pooled/IReadOnlyPooledList.cs new file mode 100644 index 0000000..ca94bae --- /dev/null +++ b/OctaneEngine/Collections.Pooled/IReadOnlyPooledList.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; + +namespace Collections.Pooled +{ + /// + /// Represents a read-only collection of pooled elements that can be accessed by index + /// + /// The type of elements in the read-only pooled list. + + public interface IReadOnlyPooledList : IReadOnlyList + { + /// + /// Gets a for the items currently in the collection. + /// + ReadOnlySpan Span { get; } + } +} diff --git a/OctaneEngine/Collections.Pooled/NonRandomizedStringEqualityComparer.cs b/OctaneEngine/Collections.Pooled/NonRandomizedStringEqualityComparer.cs new file mode 100644 index 0000000..9fa3f3a --- /dev/null +++ b/OctaneEngine/Collections.Pooled/NonRandomizedStringEqualityComparer.cs @@ -0,0 +1,77 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.Serialization; + +namespace Collections.Pooled +{ + /// + /// NonRandomizedStringEqualityComparer is the comparer used by default with the PooledDictionary. + /// We use NonRandomizedStringEqualityComparer as default comparer as it doesnt use the randomized string hashing which + /// keeps the performance not affected till we hit collision threshold and then we switch to the comparer which is using + /// randomized string hashing. + /// + [Serializable] // Required for compatibility with .NET Core 2.0 as we exposed the NonRandomizedStringEqualityComparer inside the serialization blob + public sealed class NonRandomizedStringEqualityComparer : EqualityComparer, ISerializable + { + private static readonly int s_empyStringHashCode = string.Empty.GetHashCode(); + + internal static new IEqualityComparer Default { get; } = new NonRandomizedStringEqualityComparer(); + + private NonRandomizedStringEqualityComparer() { } + + // This is used by the serialization engine. + private NonRandomizedStringEqualityComparer(SerializationInfo information, StreamingContext context) { } + + public sealed override bool Equals(string x, string y) => string.Equals(x, y); + + public sealed override int GetHashCode(string str) + => str is null ? 0 : str.Length == 0 ? s_empyStringHashCode : GetNonRandomizedHashCode(str); + + void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context) + { + info.SetType(typeof(NonRandomizedStringEqualityComparer)); + } + + // Use this if and only if 'Denial of Service' attacks are not a concern (i.e. never used for free-form user input), + // or are otherwise mitigated. + // This code was ported from an internal method on String, which relied on private members to get the char* pointer. + private static unsafe int GetNonRandomizedHashCode(string str) + { + ReadOnlySpan chars = str.AsSpan(); + fixed (char* src = chars) + { + Debug.Assert(src[chars.Length] == '\0', "src[this.Length] == '\\0'"); + Debug.Assert(((int)src) % 4 == 0, "Managed string should start at 4 bytes boundary"); + + uint hash1 = (5381 << 16) + 5381; + uint hash2 = hash1; + + uint* ptr = (uint*)src; + int length = chars.Length; + + while (length > 2) + { + length -= 4; + // Where length is 4n-1 (e.g. 3,7,11,15,19) this additionally consumes the null terminator + hash1 = (((hash1 << 5) | (hash1 >> 27)) + hash1) ^ ptr[0]; + hash2 = (((hash2 << 5) | (hash2 >> 27)) + hash2) ^ ptr[1]; + ptr += 2; + } + + if (length > 0) + { + // Where length is 4n-3 (e.g. 1,5,9,13,17) this additionally consumes the null terminator + hash2 = (((hash2 << 5) | (hash2 >> 27)) + hash2) ^ ptr[0]; + } + + return (int)(hash1 + (hash2 * 1566083941)); + } + } + } +} diff --git a/OctaneEngine/Collections.Pooled/PooledDictionary.cs b/OctaneEngine/Collections.Pooled/PooledDictionary.cs new file mode 100644 index 0000000..8a7ef3d --- /dev/null +++ b/OctaneEngine/Collections.Pooled/PooledDictionary.cs @@ -0,0 +1,2029 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Buffers; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.Serialization; +using System.Threading; + +namespace Collections.Pooled +{ + /// + /// Used internally to control behavior of insertion into a . + /// + internal enum InsertionBehavior : byte + { + /// + /// The default insertion behavior. + /// + None = 0, + + /// + /// Specifies that an existing entry with the same key should be overwritten if encountered. + /// + OverwriteExisting = 1, + + /// + /// Specifies that if an existing entry with the same key is encountered, an exception should be thrown. + /// + ThrowOnExisting = 2 + } + + /// + /// A can support multiple readers concurrently, as long as the collection is not modified. + /// Even so, enumerating through a collection is intrinsically not a thread-safe procedure. + /// In the rare case where an enumeration contends with write accesses, the collection must be locked during the entire enumeration. + /// To allow the collection to be accessed by multiple threads for reading and writing, you must implement your own synchronization. + /// + [DebuggerTypeProxy(typeof(IDictionaryDebugView<,>))] + [DebuggerDisplay("Count = {Count}")] + [Serializable] + public class PooledDictionary : IDictionary, IDictionary, IReadOnlyDictionary, + ISerializable, IDeserializationCallback, IDisposable + { + private struct Entry + { + public int hashCode; // Lower 31 bits of hash code, -1 if unused + public int next; // Index of next entry, -1 if last + public TKey key; // Key of entry + public TValue value; // Value of entry + } + + // store lower 31 bits of hash code + private const int Lower31BitMask = 0x7FFFFFFF; + + // constants for serialization + private const string VersionName = "Version"; // Do not rename (binary serialization) + private const string HashSizeName = "HashSize"; // Do not rename (binary serialization). Must save buckets.Length + private const string KeyValuePairsName = "KeyValuePairs"; // Do not rename (binary serialization) + private const string ComparerName = "Comparer"; // Do not rename (binary serialization) + private const string ClearKeyName = "CK"; // Do not rename (binary serialization) + private const string ClearValueName = "CV"; // Do not rename (binary serialization) + + private static readonly ArrayPool s_bucketPool = ArrayPool.Shared; + private static readonly ArrayPool s_entryPool = ArrayPool.Shared; + + // WARNING: + // It's important that the number of buckets be prime, and these arrays could exceed + // that size as they come from ArrayPool. Be careful not to index past _size or bad + // things will happen. + private int[] _buckets; + private Entry[] _entries; + private int _size; + + private int _count; + private int _freeList; + private int _freeCount; + private int _version; + private IEqualityComparer _comparer; + private KeyCollection _keys; + private ValueCollection _values; + private object _syncRoot; + private readonly bool _clearKeyOnFree; + private readonly bool _clearValueOnFree; + + #region Constructors + + /// + /// Creates a new instance of PooledDictionary. + /// + public PooledDictionary() : this(0, ClearMode.Auto, null) { } + + /// + /// Creates a new instance of PooledDictionary. + /// + public PooledDictionary(ClearMode clearMode) : this(0, clearMode, null) { } + + /// + /// Creates a new instance of PooledDictionary. + /// + public PooledDictionary(int capacity) : this(capacity, ClearMode.Auto, null) { } + + /// + /// Creates a new instance of PooledDictionary. + /// + public PooledDictionary(int capacity, ClearMode clearMode) : this(capacity, clearMode, null) { } + + /// + /// Creates a new instance of PooledDictionary. + /// + public PooledDictionary(IEqualityComparer comparer) : this(0, ClearMode.Auto, comparer) { } + + /// + /// Creates a new instance of PooledDictionary. + /// + public PooledDictionary(int capacity, IEqualityComparer comparer) : this(capacity, ClearMode.Auto, comparer) { } + + /// + /// Creates a new instance of PooledDictionary. + /// + public PooledDictionary(ClearMode clearMode, IEqualityComparer comparer) : this(0, clearMode, comparer) { } + + /// + /// Creates a new instance of PooledDictionary. + /// + public PooledDictionary(int capacity, ClearMode clearMode, IEqualityComparer comparer) + { + if (capacity < 0) ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.capacity); + if (capacity > 0) Initialize(capacity); + if (comparer != EqualityComparer.Default) + { + _comparer = comparer; + } + + _clearKeyOnFree = ShouldClearKey(clearMode); + _clearValueOnFree = ShouldClearValue(clearMode); + + if (typeof(TKey) == typeof(string) && _comparer == null) + { + // To start, move off default comparer for string which is randomised + _comparer = (IEqualityComparer)NonRandomizedStringEqualityComparer.Default; + } + } + + /// + /// Creates a new instance of PooledDictionary. + /// + public PooledDictionary(IDictionary dictionary) : this(dictionary, ClearMode.Auto, null) { } + + /// + /// Creates a new instance of PooledDictionary. + /// + public PooledDictionary(IDictionary dictionary, ClearMode clearMode) : this(dictionary, clearMode, null) { } + + /// + /// Creates a new instance of PooledDictionary. + /// + public PooledDictionary(IDictionary dictionary, IEqualityComparer comparer) : this(dictionary, ClearMode.Auto, comparer) { } + + /// + /// Creates a new instance of PooledDictionary. + /// + public PooledDictionary(IDictionary dictionary, ClearMode clearMode, IEqualityComparer comparer) : + this(dictionary?.Count ?? 0, clearMode, comparer) + { + if (dictionary == null) + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.dictionary); + + // It is likely that the passed-in dictionary is PooledDictionary. When this is the case, + // avoid the enumerator allocation and overhead by looping through the entries array directly. + // We only do this when dictionary is PooledDictionary and not a subclass, to maintain + // back-compat with subclasses that may have overridden the enumerator behavior. + if (dictionary is PooledDictionary pooled) + { + int count = pooled._count; + var entries = pooled._entries; + for (int i = 0; i < count; i++) + { + if (entries[i].hashCode >= 0) + { + TryInsert(entries[i].key, entries[i].value, InsertionBehavior.ThrowOnExisting); + } + } + return; + } + + foreach (var pair in dictionary) + { + TryInsert(pair.Key, pair.Value, InsertionBehavior.ThrowOnExisting); + } + } + + /// + /// Creates a new instance of PooledDictionary. + /// + public PooledDictionary(IEnumerable> collection) + : this(collection, ClearMode.Auto, null) { } + + /// + /// Creates a new instance of PooledDictionary. + /// + public PooledDictionary(IEnumerable> collection, ClearMode clearMode) + : this(collection, clearMode, null) { } + + /// + /// Creates a new instance of PooledDictionary. + /// + public PooledDictionary(IEnumerable> collection, IEqualityComparer comparer) + : this(collection, ClearMode.Auto, comparer) { } + + /// + /// Creates a new instance of PooledDictionary. + /// + public PooledDictionary(IEnumerable> collection, ClearMode clearMode, IEqualityComparer comparer) : + this((collection as ICollection>)?.Count ?? 0, clearMode, comparer) + { + if (collection == null) + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.collection); + + foreach (var pair in collection) + { + TryInsert(pair.Key, pair.Value, InsertionBehavior.ThrowOnExisting); + } + } + + /// + /// Creates a new instance of PooledDictionary. + /// + public PooledDictionary(IEnumerable<(TKey key, TValue value)> collection) + : this(collection, ClearMode.Auto, null) { } + + /// + /// Creates a new instance of PooledDictionary. + /// + public PooledDictionary(IEnumerable<(TKey key, TValue value)> collection, ClearMode clearMode) + : this(collection, clearMode, null) { } + + /// + /// Creates a new instance of PooledDictionary. + /// + public PooledDictionary(IEnumerable<(TKey key, TValue value)> collection, IEqualityComparer comparer) + : this(collection, ClearMode.Auto, comparer) { } + + /// + /// Creates a new instance of PooledDictionary. + /// + public PooledDictionary(IEnumerable<(TKey key, TValue value)> collection, ClearMode clearMode, IEqualityComparer comparer) + : this((collection as ICollection<(TKey, TValue)>)?.Count ?? 0, clearMode, comparer) + { + if (collection == null) + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.collection); + + foreach (var (key, value) in collection) + { + TryInsert(key, value, InsertionBehavior.ThrowOnExisting); + } + } + + /// + /// Creates a new instance of PooledDictionary. + /// + public PooledDictionary((TKey key, TValue value)[] array) + : this(array.AsSpan(), ClearMode.Auto, null) { } + + /// + /// Creates a new instance of PooledDictionary. + /// + public PooledDictionary((TKey key, TValue value)[] array, ClearMode clearMode) + : this(array.AsSpan(), clearMode, null) { } + + /// + /// Creates a new instance of PooledDictionary. + /// + public PooledDictionary((TKey key, TValue value)[] array, IEqualityComparer comparer) + : this(array.AsSpan(), ClearMode.Auto, comparer) { } + + /// + /// Creates a new instance of PooledDictionary. + /// + public PooledDictionary((TKey key, TValue value)[] array, ClearMode clearMode, IEqualityComparer comparer) + : this(array.AsSpan(), clearMode, comparer) { } + + /// + /// Creates a new instance of PooledDictionary. + /// + public PooledDictionary(ReadOnlySpan<(TKey key, TValue value)> span) + : this(span, ClearMode.Auto, null) { } + + /// + /// Creates a new instance of PooledDictionary. + /// + public PooledDictionary(ReadOnlySpan<(TKey key, TValue value)> span, ClearMode clearMode) + : this(span, clearMode, null) { } + + /// + /// Creates a new instance of PooledDictionary. + /// + public PooledDictionary(ReadOnlySpan<(TKey key, TValue value)> span, IEqualityComparer comparer) + : this(span, ClearMode.Auto, comparer) { } + + /// + /// Creates a new instance of PooledDictionary. + /// + public PooledDictionary(ReadOnlySpan<(TKey key, TValue value)> span, ClearMode clearMode, IEqualityComparer comparer) + : this(span.Length, clearMode, comparer) + { + foreach (var (key, value) in span) + { + TryInsert(key, value, InsertionBehavior.ThrowOnExisting); + } + } + +#pragma warning disable IDE0060 // Remove unused parameter + /// + /// Creates a new instance of PooledDictionary. + /// + protected PooledDictionary(SerializationInfo info, StreamingContext context) +#pragma warning restore IDE0060 + { + _clearKeyOnFree = (bool?)info.GetValue(ClearKeyName, typeof(bool)) ?? ShouldClearKey(ClearMode.Auto); + _clearValueOnFree = (bool?)info.GetValue(ClearValueName, typeof(bool)) ?? ShouldClearValue(ClearMode.Auto); + + // We can't do anything with the keys and values until the entire graph has been deserialized + // and we have a resonable estimate that GetHashCode is not going to fail. For the time being, + // we'll just cache this. The graph is not valid until OnDeserialization has been called. + HashHelpers.SerializationInfoTable.Add(this, info); + } + +#endregion + + /// + /// The used to compare keys in this dictionary. + /// + public IEqualityComparer Comparer + { + get + { + return (_comparer == null || _comparer is NonRandomizedStringEqualityComparer) + ? EqualityComparer.Default : _comparer; + } + } + + /// + /// The number of items in the dictionary. + /// + public int Count => _count - _freeCount; + + /// + /// Returns the ClearMode behavior for the collection, denoting whether values are + /// cleared from internal arrays before returning them to the pool. + /// + public ClearMode KeyClearMode => _clearKeyOnFree ? ClearMode.Always : ClearMode.Never; + + /// + /// Returns the ClearMode behavior for the collection, denoting whether values are + /// cleared from internal arrays before returning them to the pool. + /// + public ClearMode ValueClearMode => _clearValueOnFree ? ClearMode.Always : ClearMode.Never; + + /// + /// The keys in this dictionary. + /// + public KeyCollection Keys + { + get + { + if (_keys == null) _keys = new KeyCollection(this); + return _keys; + } + } + + ICollection IDictionary.Keys + { + get + { + if (_keys == null) _keys = new KeyCollection(this); + return _keys; + } + } + + IEnumerable IReadOnlyDictionary.Keys + { + get + { + if (_keys == null) _keys = new KeyCollection(this); + return _keys; + } + } + + /// + /// The values in this dictionary. + /// + public ValueCollection Values + { + get + { + if (_values == null) _values = new ValueCollection(this); + return _values; + } + } + + ICollection IDictionary.Values + { + get + { + if (_values == null) _values = new ValueCollection(this); + return _values; + } + } + + IEnumerable IReadOnlyDictionary.Values + { + get + { + if (_values == null) _values = new ValueCollection(this); + return _values; + } + } + + /// + /// Gets or sets an item in the dictionary by key. + /// + public TValue this[TKey key] + { + get + { + int i = FindEntry(key); + if (i >= 0) return _entries[i].value; + ThrowHelper.ThrowKeyNotFoundException(key); + return default; + } + set + { + bool modified = TryInsert(key, value, InsertionBehavior.OverwriteExisting); + Debug.Assert(modified); + } + } + + /// + /// Adds a key/value pair to the dictionary. + /// + public void Add(TKey key, TValue value) + { + bool modified = TryInsert(key, value, InsertionBehavior.ThrowOnExisting); + Debug.Assert(modified); // If there was an existing key and the Add failed, an exception will already have been thrown. + } + + public void AddRange(IEnumerable> enumerable) + { + if (enumerable is null) + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.enumerable); + + if (enumerable is ICollection> collection) + EnsureCapacity(_count + collection.Count); + + foreach (var pair in enumerable) + { + TryInsert(pair.Key, pair.Value, InsertionBehavior.ThrowOnExisting); + } + } + + public void AddRange(IEnumerable<(TKey key, TValue value)> enumerable) + { + if (enumerable is null) + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.enumerable); + + if (enumerable is ICollection> collection) + EnsureCapacity(_count + collection.Count); + + foreach (var (key, value) in enumerable) + { + TryInsert(key, value, InsertionBehavior.ThrowOnExisting); + } + } + + public void AddRange(ReadOnlySpan<(TKey key, TValue value)> span) + { + EnsureCapacity(_count + span.Length); + + foreach (var (key, value) in span) + { + TryInsert(key, value, InsertionBehavior.ThrowOnExisting); + } + } + + public void AddRange((TKey key, TValue value)[] array) + => AddRange(array.AsSpan()); + + public void AddOrUpdate(TKey key, TValue addValue, Func updater) + { + if (TryGetValue(key, out TValue value)) + { + var updatedValue = updater(key, value); + TryInsert(key, updatedValue, InsertionBehavior.OverwriteExisting); + } + else + { + TryInsert(key, addValue, InsertionBehavior.ThrowOnExisting); + } + } + + public void AddOrUpdate(TKey key, Func addValueFactory, Func updater) + { + if (TryGetValue(key, out TValue value)) + { + var updatedValue = updater(key, value); + TryInsert(key, updatedValue, InsertionBehavior.OverwriteExisting); + } + else + { + var addValue = addValueFactory(key); + TryInsert(key, addValue, InsertionBehavior.ThrowOnExisting); + } + } + + void ICollection>.Add(KeyValuePair keyValuePair) + => Add(keyValuePair.Key, keyValuePair.Value); + + bool ICollection>.Contains(KeyValuePair keyValuePair) + { + int i = FindEntry(keyValuePair.Key); + if (i >= 0 && EqualityComparer.Default.Equals(_entries[i].value, keyValuePair.Value)) + { + return true; + } + return false; + } + + bool ICollection>.Remove(KeyValuePair keyValuePair) + { + int i = FindEntry(keyValuePair.Key); + if (i >= 0 && EqualityComparer.Default.Equals(_entries[i].value, keyValuePair.Value)) + { + Remove(keyValuePair.Key); + return true; + } + return false; + } + + public void Clear() + { + int count = _count; + if (count > 0) + { + Array.Clear(_buckets, 0, _size); + + _count = 0; + _freeList = -1; + _freeCount = 0; + _size = 0; + Array.Clear(_entries, 0, count); + _version++; + } + } + + public bool ContainsKey(TKey key) + => FindEntry(key) >= 0; + + public bool ContainsValue(TValue value) + { + var entries = _entries; + if (value == null) + { + for (int i = 0; i < _count; i++) + { + if (entries[i].hashCode >= 0 && entries[i].value == null) return true; + } + } + else + { + if (default(TValue) != null) + { + // ValueType: Devirtualize with EqualityComparer.Default intrinsic + for (int i = 0; i < _count; i++) + { + if (entries[i].hashCode >= 0 && EqualityComparer.Default.Equals(entries[i].value, value)) return true; + } + } + else + { + // Object type: Shared Generic, EqualityComparer.Default won't devirtualize + // https://github.com/dotnet/coreclr/issues/17273 + // So cache in a local rather than get EqualityComparer per loop iteration + var defaultComparer = EqualityComparer.Default; + for (int i = 0; i < _count; i++) + { + if (entries[i].hashCode >= 0 && defaultComparer.Equals(entries[i].value, value)) return true; + } + } + } + return false; + } + + private void CopyTo(KeyValuePair[] array, int index) + { + if (array == null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.array); + } + + if ((uint)index > (uint)array.Length) + { + ThrowHelper.ThrowIndexArgumentOutOfRange_NeedNonNegNumException(); + } + + if (array.Length - index < Count) + { + ThrowHelper.ThrowArgumentException(ExceptionResource.Arg_ArrayPlusOffTooSmall); + } + + int count = _count; + var entries = _entries; + for (int i = 0; i < count; i++) + { + if (entries[i].hashCode >= 0) + { + array[index++] = new KeyValuePair(entries[i].key, entries[i].value); + } + } + } + + public Enumerator GetEnumerator() + => new Enumerator(this, Enumerator.KeyValuePair); + + IEnumerator> IEnumerable>.GetEnumerator() + => new Enumerator(this, Enumerator.KeyValuePair); + + void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context) + => GetObjectData(info, context); + + /// + /// Allows child classes to add their own serialization data. + /// + /// + /// + protected virtual void GetObjectData(SerializationInfo info, StreamingContext context) + { + if (info == null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.info); + } + + info.AddValue(VersionName, _version); + info.AddValue(ComparerName, _comparer ?? EqualityComparer.Default, typeof(IEqualityComparer)); + info.AddValue(HashSizeName, _size); // This is the length of the bucket array + info.AddValue(ClearKeyName, _clearKeyOnFree); + info.AddValue(ClearValueName, _clearValueOnFree); + + if (_buckets != null) + { + var array = new KeyValuePair[Count]; + CopyTo(array, 0); + info.AddValue(KeyValuePairsName, array, typeof(KeyValuePair[])); + } + } + + private int FindEntry(TKey key) + { + if (key == null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key); + } + + int i = -1; + int length = _size; + if (length <= 0) + return i; + + var buckets = _buckets; + var entries = _entries; + int collisionCount = 0; + IEqualityComparer comparer = _comparer; + + if (comparer == null) + { + int hashCode = key.GetHashCode() & Lower31BitMask; + // Value in _buckets is 1-based + i = buckets[hashCode % length] - 1; + if (default(TKey) != null) + { + // ValueType: Devirtualize with EqualityComparer.Default intrinsic + do + { + // Should be a while loop https://github.com/dotnet/coreclr/issues/15476 + // Test in if to drop range check for following array access + if ((uint)i >= (uint)length || (entries[i].hashCode == hashCode && EqualityComparer.Default.Equals(entries[i].key, key))) + { + break; + } + + i = entries[i].next; + if (collisionCount >= length) + { + // The chain of entries forms a loop; which means a concurrent update has happened. + // Break out of the loop and throw, rather than looping forever. + ThrowHelper.ThrowInvalidOperationException_ConcurrentOperationsNotSupported(); + } + collisionCount++; + } while (true); + } + else + { + // Object type: Shared Generic, EqualityComparer.Default won't devirtualize + // https://github.com/dotnet/coreclr/issues/17273 + // So cache in a local rather than get EqualityComparer per loop iteration + var defaultComparer = EqualityComparer.Default; + do + { + // Should be a while loop https://github.com/dotnet/coreclr/issues/15476 + // Test in if to drop range check for following array access + if ((uint)i >= (uint)length || (entries[i].hashCode == hashCode && defaultComparer.Equals(entries[i].key, key))) + { + break; + } + + i = entries[i].next; + if (collisionCount >= length) + { + // The chain of entries forms a loop; which means a concurrent update has happened. + // Break out of the loop and throw, rather than looping forever. + ThrowHelper.ThrowInvalidOperationException_ConcurrentOperationsNotSupported(); + } + collisionCount++; + } while (true); + } + } + else + { + int hashCode = comparer.GetHashCode(key) & Lower31BitMask; + // Value in _buckets is 1-based + i = buckets[hashCode % length] - 1; + do + { + // Should be a while loop https://github.com/dotnet/coreclr/issues/15476 + // Test in if to drop range check for following array access + if ((uint)i >= (uint)length || + (entries[i].hashCode == hashCode && comparer.Equals(entries[i].key, key))) + { + break; + } + + i = entries[i].next; + if (collisionCount >= length) + { + // The chain of entries forms a loop; which means a concurrent update has happened. + // Break out of the loop and throw, rather than looping forever. + ThrowHelper.ThrowInvalidOperationException_ConcurrentOperationsNotSupported(); + } + collisionCount++; + } while (true); + } + + return i; + } + + private int Initialize(int capacity) + { + _size = HashHelpers.GetPrime(capacity); + _freeList = -1; + _buckets = s_bucketPool.Rent(_size); + Array.Clear(_buckets, 0, _buckets.Length); + _entries = s_entryPool.Rent(_size); + + return _size; + } + + private bool TryInsert(TKey key, TValue value, InsertionBehavior behavior) + { + if (key == null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key); + } + + if (_buckets == null || _size == 0) + { + Initialize(0); + } + + var entries = _entries; + var comparer = _comparer; + var size = _size; + + int hashCode = ((comparer == null) ? key.GetHashCode() : comparer.GetHashCode(key)) & Lower31BitMask; + + int collisionCount = 0; + ref int bucket = ref _buckets[hashCode % size]; + // Value in _buckets is 1-based + int i = bucket - 1; + + if (comparer == null) + { + if (default(TKey) != null) + { + // ValueType: Devirtualize with EqualityComparer.Default intrinsic + do + { + // Should be a while loop https://github.com/dotnet/coreclr/issues/15476 + // Test uint in if rather than loop condition to drop range check for following array access + if ((uint)i >= (uint)size) + { + break; + } + + if (entries[i].hashCode == hashCode && EqualityComparer.Default.Equals(entries[i].key, key)) + { + if (behavior == InsertionBehavior.OverwriteExisting) + { + entries[i].value = value; + _version++; + return true; + } + + if (behavior == InsertionBehavior.ThrowOnExisting) + { + ThrowHelper.ThrowAddingDuplicateWithKeyArgumentException(key); + } + + return false; + } + + i = entries[i].next; + if (collisionCount >= size) + { + // The chain of entries forms a loop; which means a concurrent update has happened. + // Break out of the loop and throw, rather than looping forever. + ThrowHelper.ThrowInvalidOperationException_ConcurrentOperationsNotSupported(); + } + collisionCount++; + } while (true); + } + else + { + // Object type: Shared Generic, EqualityComparer.Default won't devirtualize + // https://github.com/dotnet/coreclr/issues/17273 + // So cache in a local rather than get EqualityComparer per loop iteration + var defaultComparer = EqualityComparer.Default; + do + { + // Should be a while loop https://github.com/dotnet/coreclr/issues/15476 + // Test uint in if rather than loop condition to drop range check for following array access + if ((uint)i >= (uint)size) + { + break; + } + + if (entries[i].hashCode == hashCode && defaultComparer.Equals(entries[i].key, key)) + { + if (behavior == InsertionBehavior.OverwriteExisting) + { + entries[i].value = value; + _version++; + return true; + } + + if (behavior == InsertionBehavior.ThrowOnExisting) + { + ThrowHelper.ThrowAddingDuplicateWithKeyArgumentException(key); + } + + return false; + } + + i = entries[i].next; + if (collisionCount >= size) + { + // The chain of entries forms a loop; which means a concurrent update has happened. + // Break out of the loop and throw, rather than looping forever. + ThrowHelper.ThrowInvalidOperationException_ConcurrentOperationsNotSupported(); + } + collisionCount++; + } while (true); + } + } + else + { + do + { + // Should be a while loop https://github.com/dotnet/coreclr/issues/15476 + // Test uint in if rather than loop condition to drop range check for following array access + if ((uint)i >= (uint)size) + { + break; + } + + if (entries[i].hashCode == hashCode && comparer.Equals(entries[i].key, key)) + { + if (behavior == InsertionBehavior.OverwriteExisting) + { + entries[i].value = value; + _version++; + return true; + } + + if (behavior == InsertionBehavior.ThrowOnExisting) + { + ThrowHelper.ThrowAddingDuplicateWithKeyArgumentException(key); + } + + return false; + } + + i = entries[i].next; + if (collisionCount >= size) + { + // The chain of entries forms a loop; which means a concurrent update has happened. + // Break out of the loop and throw, rather than looping forever. + ThrowHelper.ThrowInvalidOperationException_ConcurrentOperationsNotSupported(); + } + collisionCount++; + } while (true); + + } + + bool updateFreeList = false; + int index; + if (_freeCount > 0) + { + index = _freeList; + updateFreeList = true; + _freeCount--; + } + else + { + int count = _count; + if (count == size) + { + Resize(); + size = _size; + bucket = ref _buckets[hashCode % size]; + } + index = count; + _count = count + 1; + entries = _entries; + } + + ref Entry entry = ref entries[index]; + + if (updateFreeList) + { + _freeList = entry.next; + } + entry.hashCode = hashCode; + // Value in _buckets is 1-based + entry.next = bucket - 1; + entry.key = key; + entry.value = value; + // Value in _buckets is 1-based +#pragma warning disable IDE0059 // Value assigned to symbol is never used + bucket = index + 1; +#pragma warning restore IDE0059 + _version++; + + // Value types never rehash + if (default(TKey) == null && collisionCount > HashHelpers.HashCollisionThreshold && comparer is NonRandomizedStringEqualityComparer) + { + // If we hit the collision threshold we'll need to switch to the comparer which is using randomized string hashing + // i.e. EqualityComparer.Default. + _comparer = null; + Resize(size, true); + } + + return true; + } + + public virtual void OnDeserialization(object sender) + { + HashHelpers.SerializationInfoTable.TryGetValue(this, out SerializationInfo siInfo); + + if (siInfo == null) + { + // We can return immediately if this function is called twice. + // Note we remove the serialization info from the table at the end of this method. + return; + } + + int realVersion = siInfo.GetInt32(VersionName); + int hashsize = siInfo.GetInt32(HashSizeName); + _comparer = (IEqualityComparer)siInfo.GetValue(ComparerName, typeof(IEqualityComparer)); + + if (hashsize != 0) + { + Initialize(hashsize); + + var array = (KeyValuePair[]) + siInfo.GetValue(KeyValuePairsName, typeof(KeyValuePair[])); + + if (array == null) + { + throw new SerializationException("Serialized PooledDictionary missing data."); + } + + for (int i = 0; i < array.Length; i++) + { + if (array[i].Key == null) + { + throw new SerializationException("Serialized PooledDictionary had null key."); + } + Add(array[i].Key, array[i].Value); + } + } + else + { + _buckets = null; + } + + _version = realVersion; + HashHelpers.SerializationInfoTable.Remove(this); + } + + private void Resize() + => Resize(HashHelpers.ExpandPrime(_count), false); + + private void Resize(int newSize, bool forceNewHashCodes) + { + // Value types never rehash + Debug.Assert(!forceNewHashCodes || default(TKey) == null); + Debug.Assert(newSize >= _size); + + int[] buckets; + Entry[] entries; + bool replaceArrays; + int count = _count; + + // Because ArrayPool might give us larger arrays than we asked for, see if we can + // use the existing capacity without actually resizing. + if (_buckets.Length >= newSize && _entries.Length >= newSize) + { + Array.Clear(_buckets, 0, _buckets.Length); + Array.Clear(_entries, _size, newSize - _size); + buckets = _buckets; + entries = _entries; + replaceArrays = false; + } + else + { + buckets = s_bucketPool.Rent(newSize); + entries = s_entryPool.Rent(newSize); + + Array.Clear(buckets, 0, buckets.Length); + Array.Copy(_entries, 0, entries, 0, count); + replaceArrays = true; + } + + if (default(TKey) == null && forceNewHashCodes) + { + for (int i = 0; i < count; i++) + { + if (entries[i].hashCode >= 0) + { + Debug.Assert(_comparer == null); + entries[i].hashCode = (entries[i].key.GetHashCode() & Lower31BitMask); + } + } + } + + for (int i = 0; i < count; i++) + { + if (entries[i].hashCode >= 0) + { + int bucket = entries[i].hashCode % newSize; + // Value in _buckets is 1-based + entries[i].next = buckets[bucket] - 1; + // Value in _buckets is 1-based + buckets[bucket] = i + 1; + } + } + + if (replaceArrays) + { + ReturnArrays(); + _buckets = buckets; + _entries = entries; + } + _size = newSize; + } + + // The overload Remove(TKey key, out TValue value) is a copy of this method with one additional + // statement to copy the value for entry being removed into the output parameter. + // Code has been intentionally duplicated for performance reasons. + public bool Remove(TKey key) + { + if (key == null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key); + } + + var buckets = _buckets; + var entries = _entries; + int collisionCount = 0; + if (_size > 0) + { + int hashCode = (_comparer?.GetHashCode(key) ?? key.GetHashCode()) & Lower31BitMask; + int bucket = hashCode % _size; + int last = -1; + // Value in buckets is 1-based + int i = buckets[bucket] - 1; + while (i >= 0) + { + ref Entry entry = ref entries[i]; + + if (entry.hashCode == hashCode && (_comparer?.Equals(entry.key, key) ?? EqualityComparer.Default.Equals(entry.key, key))) + { + if (last < 0) + { + // Value in buckets is 1-based + buckets[bucket] = entry.next + 1; + } + else + { + entries[last].next = entry.next; + } + entry.hashCode = -1; + entry.next = _freeList; + + if (_clearKeyOnFree) + entry.key = default; + if (_clearValueOnFree) + entry.value = default; + + _freeList = i; + _freeCount++; + _version++; + return true; + } + + last = i; + i = entry.next; + if (collisionCount >= _size) + { + // The chain of entries forms a loop; which means a concurrent update has happened. + // Break out of the loop and throw, rather than looping forever. + ThrowHelper.ThrowInvalidOperationException_ConcurrentOperationsNotSupported(); + } + collisionCount++; + } + } + return false; + } + + // This overload is a copy of the overload Remove(TKey key) with one additional + // statement to copy the value for entry being removed into the output parameter. + // Code has been intentionally duplicated for performance reasons. + public bool Remove(TKey key, out TValue value) + { + if (key == null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key); + } + + var buckets = _buckets; + var entries = _entries; + int collisionCount = 0; + int hashCode = (_comparer?.GetHashCode(key) ?? key.GetHashCode()) & Lower31BitMask; + int bucket = hashCode % _size; + int last = -1; + // Value in buckets is 1-based + int i = buckets[bucket] - 1; + while (i >= 0) + { + ref Entry entry = ref entries[i]; + + if (entry.hashCode == hashCode && (_comparer?.Equals(entry.key, key) ?? EqualityComparer.Default.Equals(entry.key, key))) + { + if (last < 0) + { + // Value in buckets is 1-based + buckets[bucket] = entry.next + 1; + } + else + { + entries[last].next = entry.next; + } + + value = entry.value; + + entry.hashCode = -1; + entry.next = _freeList; + + if (_clearKeyOnFree) + entry.key = default; + if (_clearValueOnFree) + entry.value = default; + + _freeList = i; + _freeCount++; + return true; + } + + last = i; + i = entry.next; + if (collisionCount >= _size) + { + // The chain of entries forms a loop; which means a concurrent update has happened. + // Break out of the loop and throw, rather than looping forever. + ThrowHelper.ThrowInvalidOperationException_ConcurrentOperationsNotSupported(); + } + collisionCount++; + } + value = default; + return false; + } + + public bool TryGetValue(TKey key, out TValue value) + { + int i = FindEntry(key); + if (i >= 0) + { + value = _entries[i].value; + return true; + } + value = default; + return false; + } + + public bool TryAdd(TKey key, TValue value) + => TryInsert(key, value, InsertionBehavior.None); + + public TValue GetOrAdd(TKey key, TValue addValue) + { + if (TryGetValue(key, out TValue value)) + return value; + + Add(key, addValue); + return addValue; + } + + public TValue GetOrAdd(TKey key, Func valueFactory) + { + if (TryGetValue(key, out TValue value)) + return value; + + var addValue = valueFactory(key); + Add(key, addValue); + return addValue; + } + + bool ICollection>.IsReadOnly => false; + + void ICollection>.CopyTo(KeyValuePair[] array, int index) + => CopyTo(array, index); + + void ICollection.CopyTo(Array array, int index) + { + if (array == null) + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.array); + if (array.Rank != 1) + ThrowHelper.ThrowArgumentException(ExceptionResource.Arg_RankMultiDimNotSupported); + if (array.GetLowerBound(0) != 0) + ThrowHelper.ThrowArgumentException(ExceptionResource.Arg_NonZeroLowerBound); + if ((uint)index > (uint)array.Length) + ThrowHelper.ThrowIndexArgumentOutOfRange_NeedNonNegNumException(); + if (array.Length - index < Count) + ThrowHelper.ThrowArgumentException(ExceptionResource.Arg_ArrayPlusOffTooSmall); + + if (array is KeyValuePair[] pairs) + { + CopyTo(pairs, index); + } + else if (array is DictionaryEntry[] dictEntryArray) + { + Entry[] entries = _entries; + for (int i = 0; i < _count; i++) + { + if (entries[i].hashCode >= 0) + { + dictEntryArray[index++] = new DictionaryEntry(entries[i].key, entries[i].value); + } + } + } + else if (array is object[] objects) + { + try + { + int count = _count; + var entries = _entries; + for (int i = 0; i < count; i++) + { + if (entries[i].hashCode >= 0) + { + objects[index++] = new KeyValuePair(entries[i].key, entries[i].value); + } + } + } + catch (ArrayTypeMismatchException) + { + ThrowHelper.ThrowArgumentException_Argument_InvalidArrayType(); + } + } + else + { + ThrowHelper.ThrowArgumentException_Argument_InvalidArrayType(); + } + } + + IEnumerator IEnumerable.GetEnumerator() + => new Enumerator(this, Enumerator.KeyValuePair); + + /// + /// Ensures that the dictionary can hold up to 'capacity' entries without any further expansion of its backing storage + /// + public int EnsureCapacity(int capacity) + { + if (capacity < 0) + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.capacity); + int currentCapacity = _size; + if (currentCapacity >= capacity) + return currentCapacity; + _version++; + if (_buckets == null || _size == 0) + return Initialize(capacity); + int newSize = HashHelpers.GetPrime(capacity); + Resize(newSize, forceNewHashCodes: false); + return newSize; + } + + /// + /// Sets the capacity of this dictionary to what it would be if it had been originally initialized with all its entries + /// + /// This method can be used to minimize the memory overhead + /// once it is known that no new elements will be added. + /// + /// To allocate minimum size storage array, execute the following statements: + /// + /// dictionary.Clear(); + /// dictionary.TrimExcess(); + /// + public void TrimExcess() + => TrimExcess(Count); + + /// + /// Sets the capacity of this dictionary to hold up 'capacity' entries without any further expansion of its backing storage + /// + /// This method can be used to minimize the memory overhead + /// once it is known that no new elements will be added. + /// + public void TrimExcess(int capacity) + { + if (capacity < Count) + throw new ArgumentOutOfRangeException(nameof(capacity)); + int newSize = HashHelpers.GetPrime(capacity); + + Entry[] oldEntries = _entries; + int[] oldBuckets = _buckets; + int currentCapacity = oldEntries == null ? 0 : oldEntries.Length; + if (newSize >= currentCapacity) + return; + + int oldCount = _count; + _version++; + Initialize(newSize); + var entries = _entries; + var buckets = _buckets; + int count = 0; + for (int i = 0; i < oldCount; i++) + { + int hashCode = oldEntries[i].hashCode; + if (hashCode >= 0) + { +#pragma warning disable IDE0059 // Value assigned to symbol is never used + ref Entry entry = ref entries[count]; +#pragma warning restore IDE0059 + entry = oldEntries[i]; + int bucket = hashCode % newSize; + // Value in _buckets is 1-based + entry.next = buckets[bucket] - 1; + // Value in _buckets is 1-based + buckets[bucket] = count + 1; + count++; + } + } + _count = count; + _size = newSize; + _freeCount = 0; + s_bucketPool.Return(oldBuckets); + s_entryPool.Return(entries, clearArray: _clearKeyOnFree || _clearValueOnFree); + } + + bool ICollection.IsSynchronized => false; + + object ICollection.SyncRoot + { + get + { + if (_syncRoot == null) + { + Interlocked.CompareExchange(ref _syncRoot, new object(), null); + } + return _syncRoot; + } + } + + bool IDictionary.IsFixedSize => false; + + bool IDictionary.IsReadOnly => false; + + ICollection IDictionary.Keys => Keys; + + ICollection IDictionary.Values => Values; + + object IDictionary.this[object key] + { + get + { + if (IsCompatibleKey(key)) + { + int i = FindEntry((TKey)key); + if (i >= 0) + { + return _entries[i].value; + } + } + return null; + } + set + { + if (key == null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key); + } + ThrowHelper.IfNullAndNullsAreIllegalThenThrow(value, ExceptionArgument.value); + + try + { + TKey tempKey = (TKey)key; + try + { + this[tempKey] = (TValue)value; + } + catch (InvalidCastException) + { + ThrowHelper.ThrowWrongValueTypeArgumentException(value, typeof(TValue)); + } + } + catch (InvalidCastException) + { + ThrowHelper.ThrowWrongKeyTypeArgumentException(key, typeof(TKey)); + } + } + } + + private void ReturnArrays() + { + if (_entries?.Length > 0) + { + try + { + s_entryPool.Return(_entries, clearArray: _clearKeyOnFree || _clearValueOnFree); + } + catch (ArgumentException) + { + // oh well, the array pool didn't like our array + } + } + + if (_buckets?.Length > 0) + { + try + { + s_bucketPool.Return(_buckets); + } + catch (ArgumentException) + { + // shucks + } + } + + _entries = null; + _buckets = null; + } + + private static bool ShouldClearKey(ClearMode mode) + { +#if NETCOREAPP2_1 + return mode == ClearMode.Always + || (mode == ClearMode.Auto && RuntimeHelpers.IsReferenceOrContainsReferences()); +#else + return mode != ClearMode.Never; +#endif + } + + private static bool ShouldClearValue(ClearMode mode) + { +#if NETCOREAPP2_1 + return mode == ClearMode.Always + || (mode == ClearMode.Auto && RuntimeHelpers.IsReferenceOrContainsReferences()); +#else + return mode != ClearMode.Never; +#endif + } + + private static bool IsCompatibleKey(object key) + { + if (key == null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key); + } + return key is TKey; + } + + void IDictionary.Add(object key, object value) + { + if (key == null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key); + } + ThrowHelper.IfNullAndNullsAreIllegalThenThrow(value, ExceptionArgument.value); + + try + { + TKey tempKey = (TKey)key; + + try + { + Add(tempKey, (TValue)value); + } + catch (InvalidCastException) + { + ThrowHelper.ThrowWrongValueTypeArgumentException(value, typeof(TValue)); + } + } + catch (InvalidCastException) + { + ThrowHelper.ThrowWrongKeyTypeArgumentException(key, typeof(TKey)); + } + } + + bool IDictionary.Contains(object key) + { + if (IsCompatibleKey(key)) + { + return ContainsKey((TKey)key); + } + + return false; + } + + IDictionaryEnumerator IDictionary.GetEnumerator() + => new Enumerator(this, Enumerator.DictEntry); + + void IDictionary.Remove(object key) + { + if (IsCompatibleKey(key)) + { + Remove((TKey)key); + } + } + + public void Dispose() + { + ReturnArrays(); + _count = 0; + _size = 0; + _freeList = -1; + _freeCount = 0; + } + + public struct Enumerator : IEnumerator>, IDictionaryEnumerator + { + private readonly PooledDictionary _dictionary; + private readonly int _version; + private int _index; + private KeyValuePair _current; + private readonly int _getEnumeratorRetType; // What should Enumerator.Current return? + + internal const int DictEntry = 1; + internal const int KeyValuePair = 2; + + internal Enumerator(PooledDictionary dictionary, int getEnumeratorRetType) + { + _dictionary = dictionary; + _version = dictionary._version; + _index = 0; + _getEnumeratorRetType = getEnumeratorRetType; + _current = new KeyValuePair(); + } + + public bool MoveNext() + { + if (_version != _dictionary._version) + { + ThrowHelper.ThrowInvalidOperationException_InvalidOperation_EnumFailedVersion(); + } + + // Use unsigned comparison since we set index to dictionary.count+1 when the enumeration ends. + // dictionary.count+1 could be negative if dictionary.count is int.MaxValue + while ((uint)_index < (uint)_dictionary._count) + { + ref Entry entry = ref _dictionary._entries[_index++]; + + if (entry.hashCode >= 0) + { + _current = new KeyValuePair(entry.key, entry.value); + return true; + } + } + + _index = _dictionary._count + 1; + _current = new KeyValuePair(); + return false; + } + + public KeyValuePair Current => _current; + + public void Dispose() + { + } + + object IEnumerator.Current + { + get + { + if (_index == 0 || (_index == _dictionary._count + 1)) + { + ThrowHelper.ThrowInvalidOperationException_InvalidOperation_EnumOpCantHappen(); + } + + if (_getEnumeratorRetType == DictEntry) + { + return new DictionaryEntry(_current.Key, _current.Value); + } + else + { + return new KeyValuePair(_current.Key, _current.Value); + } + } + } + + void IEnumerator.Reset() + { + if (_version != _dictionary._version) + { + ThrowHelper.ThrowInvalidOperationException_InvalidOperation_EnumFailedVersion(); + } + + _index = 0; + _current = new KeyValuePair(); + } + + DictionaryEntry IDictionaryEnumerator.Entry + { + get + { + if (_index == 0 || (_index == _dictionary._count + 1)) + { + ThrowHelper.ThrowInvalidOperationException_InvalidOperation_EnumOpCantHappen(); + } + + return new DictionaryEntry(_current.Key, _current.Value); + } + } + + object IDictionaryEnumerator.Key + { + get + { + if (_index == 0 || (_index == _dictionary._count + 1)) + { + ThrowHelper.ThrowInvalidOperationException_InvalidOperation_EnumOpCantHappen(); + } + + return _current.Key; + } + } + + object IDictionaryEnumerator.Value + { + get + { + if (_index == 0 || (_index == _dictionary._count + 1)) + { + ThrowHelper.ThrowInvalidOperationException_InvalidOperation_EnumOpCantHappen(); + } + + return _current.Value; + } + } + } + + [DebuggerTypeProxy(typeof(DictionaryKeyCollectionDebugView<,>))] + [DebuggerDisplay("Count = {Count}")] + public sealed class KeyCollection : ICollection, ICollection, IReadOnlyCollection + { + private readonly PooledDictionary _dictionary; + + public KeyCollection(PooledDictionary dictionary) + { + _dictionary = dictionary ?? throw new ArgumentNullException(nameof(dictionary)); + } + + public Enumerator GetEnumerator() + => new Enumerator(_dictionary); + + public void CopyTo(TKey[] array, int index) + { + if (array == null) + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.array); + + if (index < 0 || index > array.Length) + ThrowHelper.ThrowIndexArgumentOutOfRange_NeedNonNegNumException(); + + if (array.Length - index < _dictionary.Count) + ThrowHelper.ThrowArgumentException(ExceptionResource.Arg_ArrayPlusOffTooSmall); + + int count = _dictionary._count; + var entries = _dictionary._entries; + for (int i = 0; i < count; i++) + { + if (entries[i].hashCode >= 0) array[index++] = entries[i].key; + } + } + + public int Count => _dictionary.Count; + + bool ICollection.IsReadOnly => true; + + void ICollection.Add(TKey item) + => ThrowHelper.ThrowNotSupportedException(ExceptionResource.NotSupported_KeyCollectionSet); + + void ICollection.Clear() + => ThrowHelper.ThrowNotSupportedException(ExceptionResource.NotSupported_KeyCollectionSet); + + bool ICollection.Contains(TKey item) + => _dictionary.ContainsKey(item); + + bool ICollection.Remove(TKey item) + { + ThrowHelper.ThrowNotSupportedException(ExceptionResource.NotSupported_KeyCollectionSet); + return false; + } + + IEnumerator IEnumerable.GetEnumerator() + => new Enumerator(_dictionary); + + IEnumerator IEnumerable.GetEnumerator() + => new Enumerator(_dictionary); + + void ICollection.CopyTo(Array array, int index) + { + if (array == null) + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.array); + if (array.Rank != 1) + ThrowHelper.ThrowArgumentException(ExceptionResource.Arg_RankMultiDimNotSupported); + if (array.GetLowerBound(0) != 0) + ThrowHelper.ThrowArgumentException(ExceptionResource.Arg_NonZeroLowerBound); + if ((uint)index > (uint)array.Length) + ThrowHelper.ThrowIndexArgumentOutOfRange_NeedNonNegNumException(); + if (array.Length - index < _dictionary.Count) + ThrowHelper.ThrowArgumentException(ExceptionResource.Arg_ArrayPlusOffTooSmall); + + if (array is TKey[] keys) + { + CopyTo(keys, index); + } + else + { + object[] objects = array as object[]; + if (objects == null) + { + ThrowHelper.ThrowArgumentException_Argument_InvalidArrayType(); + } + + int count = _dictionary._count; + var entries = _dictionary._entries; + try + { + for (int i = 0; i < count; i++) + { + if (entries[i].hashCode >= 0) objects[index++] = entries[i].key; + } + } + catch (ArrayTypeMismatchException) + { + ThrowHelper.ThrowArgumentException_Argument_InvalidArrayType(); + } + } + } + + bool ICollection.IsSynchronized => false; + + object ICollection.SyncRoot => ((ICollection)_dictionary).SyncRoot; + + public struct Enumerator : IEnumerator, IEnumerator + { + private readonly PooledDictionary _dictionary; + private int _index; + private readonly int _version; + private TKey _currentKey; + + internal Enumerator(PooledDictionary dictionary) + { + _dictionary = dictionary; + _version = dictionary._version; + _index = 0; + _currentKey = default; + } + + public void Dispose() + { + } + + public bool MoveNext() + { + if (_version != _dictionary._version) + { + ThrowHelper.ThrowInvalidOperationException_InvalidOperation_EnumFailedVersion(); + } + + while ((uint)_index < (uint)_dictionary._count) + { + ref Entry entry = ref _dictionary._entries[_index++]; + + if (entry.hashCode >= 0) + { + _currentKey = entry.key; + return true; + } + } + + _index = _dictionary._count + 1; + _currentKey = default; + return false; + } + + public TKey Current => _currentKey; + + object IEnumerator.Current + { + get + { + if (_index == 0 || (_index == _dictionary._count + 1)) + { + ThrowHelper.ThrowInvalidOperationException_InvalidOperation_EnumOpCantHappen(); + } + + return _currentKey; + } + } + + void IEnumerator.Reset() + { + if (_version != _dictionary._version) + { + ThrowHelper.ThrowInvalidOperationException_InvalidOperation_EnumFailedVersion(); + } + + _index = 0; + _currentKey = default; + } + } + } + + [DebuggerTypeProxy(typeof(DictionaryValueCollectionDebugView<,>))] + [DebuggerDisplay("Count = {Count}")] + public sealed class ValueCollection : ICollection, ICollection, IReadOnlyCollection + { + private readonly PooledDictionary _dictionary; + + public ValueCollection(PooledDictionary dictionary) + { + if (dictionary == null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.dictionary); + } + _dictionary = dictionary; + } + + public Enumerator GetEnumerator() + => new Enumerator(_dictionary); + + public void CopyTo(TValue[] array, int index) + { + if (array == null) + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.array); + + if (index < 0 || index > array.Length) + ThrowHelper.ThrowIndexArgumentOutOfRange_NeedNonNegNumException(); + + if (array.Length - index < _dictionary.Count) + ThrowHelper.ThrowArgumentException(ExceptionResource.Arg_ArrayPlusOffTooSmall); + + int count = _dictionary._count; + var entries = _dictionary._entries; + for (int i = 0; i < count; i++) + { + if (entries[i].hashCode >= 0) array[index++] = entries[i].value; + } + } + + public int Count => _dictionary.Count; + + bool ICollection.IsReadOnly => true; + + void ICollection.Add(TValue item) + => ThrowHelper.ThrowNotSupportedException(ExceptionResource.NotSupported_ValueCollectionSet); + + bool ICollection.Remove(TValue item) + { + ThrowHelper.ThrowNotSupportedException(ExceptionResource.NotSupported_ValueCollectionSet); + return false; + } + + void ICollection.Clear() + => ThrowHelper.ThrowNotSupportedException(ExceptionResource.NotSupported_ValueCollectionSet); + + bool ICollection.Contains(TValue item) + => _dictionary.ContainsValue(item); + + IEnumerator IEnumerable.GetEnumerator() + => new Enumerator(_dictionary); + + IEnumerator IEnumerable.GetEnumerator() + => new Enumerator(_dictionary); + + void ICollection.CopyTo(Array array, int index) + { + if (array == null) + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.array); + if (array.Rank != 1) + ThrowHelper.ThrowArgumentException(ExceptionResource.Arg_RankMultiDimNotSupported); + if (array.GetLowerBound(0) != 0) + ThrowHelper.ThrowArgumentException(ExceptionResource.Arg_NonZeroLowerBound); + if ((uint)index > (uint)array.Length) + ThrowHelper.ThrowIndexArgumentOutOfRange_NeedNonNegNumException(); + if (array.Length - index < _dictionary.Count) + ThrowHelper.ThrowArgumentException(ExceptionResource.Arg_ArrayPlusOffTooSmall); + + if (array is TValue[] values) + { + CopyTo(values, index); + } + else if (array is object[] objects) + { + int count = _dictionary._count; + var entries = _dictionary._entries; + try + { + for (int i = 0; i < count; i++) + { + if (entries[i].hashCode >= 0) objects[index++] = entries[i].value; + } + } + catch (ArrayTypeMismatchException) + { + ThrowHelper.ThrowArgumentException_Argument_InvalidArrayType(); + } + } + else + { + ThrowHelper.ThrowArgumentException_Argument_InvalidArrayType(); + } + } + + bool ICollection.IsSynchronized => false; + + object ICollection.SyncRoot => ((ICollection)_dictionary).SyncRoot; + + public struct Enumerator : IEnumerator, IEnumerator + { + private readonly PooledDictionary _dictionary; + private int _index; + private readonly int _version; + private TValue _currentValue; + + internal Enumerator(PooledDictionary dictionary) + { + _dictionary = dictionary; + _version = dictionary._version; + _index = 0; + _currentValue = default; + } + + public void Dispose() + { + } + + public bool MoveNext() + { + if (_version != _dictionary._version) + { + ThrowHelper.ThrowInvalidOperationException_InvalidOperation_EnumFailedVersion(); + } + + while ((uint)_index < (uint)_dictionary._count) + { + ref Entry entry = ref _dictionary._entries[_index++]; + + if (entry.hashCode >= 0) + { + _currentValue = entry.value; + return true; + } + } + _index = _dictionary._count + 1; + _currentValue = default; + return false; + } + + public TValue Current => _currentValue; + + object IEnumerator.Current + { + get + { + if (_index == 0 || (_index == _dictionary._count + 1)) + { + ThrowHelper.ThrowInvalidOperationException_InvalidOperation_EnumOpCantHappen(); + } + + return _currentValue; + } + } + + void IEnumerator.Reset() + { + if (_version != _dictionary._version) + { + ThrowHelper.ThrowInvalidOperationException_InvalidOperation_EnumFailedVersion(); + } + _index = 0; + _currentValue = default; + } + } + } + } +} diff --git a/OctaneEngine/Collections.Pooled/PooledExtensions.cs b/OctaneEngine/Collections.Pooled/PooledExtensions.cs new file mode 100644 index 0000000..f7379f0 --- /dev/null +++ b/OctaneEngine/Collections.Pooled/PooledExtensions.cs @@ -0,0 +1,335 @@ +using System; +using System.Collections.Generic; + +namespace Collections.Pooled +{ + /// + /// Extension methods for creating pooled collections. + /// + public static class PooledExtensions + { + #region PooledList + + public static PooledList ToPooledList(this IEnumerable items) + => new PooledList(items); + + public static PooledList ToPooledList(this IEnumerable items, int suggestCapacity) + => new PooledList(items, suggestCapacity); + + public static PooledList ToPooledList(this T[] array) + => new PooledList(array.AsSpan()); + + public static PooledList ToPooledList(this ReadOnlySpan span) + => new PooledList(span); + + public static PooledList ToPooledList(this Span span) + => new PooledList(span); + + public static PooledList ToPooledList(this ReadOnlyMemory memory) + => new PooledList(memory.Span); + + public static PooledList ToPooledList(this Memory memory) + => new PooledList(memory.Span); + + #endregion + + #region PooledDictionary + + /// + /// Creates a from an according to specified + /// key selector and element selector functions, as well as a comparer. + /// + public static PooledDictionary ToPooledDictionary( + this IEnumerable source, + Func keySelector, Func valueSelector, + IEqualityComparer comparer = null) + { + if (source == null) + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source); + var dict = new PooledDictionary((source as ICollection)?.Count ?? 0, comparer); + foreach (var item in source) + { + dict.Add(keySelector(item), valueSelector(item)); + } + + return dict; + } + + /// + /// Creates a from a according to specified + /// key selector and element selector functions, as well as a comparer. + /// + public static PooledDictionary ToPooledDictionary( + this ReadOnlySpan source, + Func keySelector, Func valueSelector, + IEqualityComparer comparer = null) + { + var dict = new PooledDictionary(source.Length, comparer); + foreach (var item in source) + { + dict.Add(keySelector(item), valueSelector(item)); + } + + return dict; + } + + /// + /// Creates a from a according to specified + /// key selector and element selector functions, as well as a comparer. + /// + public static PooledDictionary ToPooledDictionary( + this Span source, + Func keySelector, Func valueSelector, IEqualityComparer comparer) + { + return ToPooledDictionary((ReadOnlySpan)source, keySelector, valueSelector, comparer); + } + + /// + /// Creates a from a according to specified + /// key selector and element selector functions, as well as a comparer. + /// + public static PooledDictionary ToPooledDictionary( + this ReadOnlyMemory source, + Func keySelector, Func valueSelector, IEqualityComparer comparer) + { + return ToPooledDictionary(source.Span, keySelector, valueSelector, comparer); + } + + /// + /// Creates a from a according to specified + /// key selector and element selector functions, as well as a comparer. + /// + public static PooledDictionary ToPooledDictionary( + this Memory source, + Func keySelector, Func valueSelector, IEqualityComparer comparer) + { + return ToPooledDictionary(source.Span, keySelector, valueSelector, comparer); + } + + /// + /// Creates a from an according to specified + /// key selector and comparer. + /// + public static PooledDictionary ToPooledDictionary( + this IEnumerable source, + Func keySelector, IEqualityComparer comparer = null) + { + if (source == null) + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source); + var dict = new PooledDictionary((source as ICollection)?.Count ?? 0, comparer); + foreach (var item in source) + { + dict.Add(keySelector(item), item); + } + + return dict; + } + + /// + /// Creates a from an according to specified + /// key selector and comparer. + /// + public static PooledDictionary ToPooledDictionary( + this ReadOnlySpan source, + Func keySelector, IEqualityComparer comparer = null) + { + var dict = new PooledDictionary(source.Length, comparer); + foreach (var item in source) + { + dict.Add(keySelector(item), item); + } + + return dict; + } + + /// + /// Creates a from an according to specified + /// key selector and comparer. + /// + public static PooledDictionary ToPooledDictionary(this Span source, + Func keySelector, IEqualityComparer comparer = null) + { + return ToPooledDictionary((ReadOnlySpan)source, keySelector, comparer); + } + + /// + /// Creates a from an according to specified + /// key selector and comparer. + /// + public static PooledDictionary ToPooledDictionary( + this ReadOnlyMemory source, + Func keySelector, IEqualityComparer comparer = null) + { + return ToPooledDictionary(source.Span, keySelector, comparer); + } + + /// + /// Creates a from an according to specified + /// key selector and comparer. + /// + public static PooledDictionary ToPooledDictionary(this Memory source, + Func keySelector, IEqualityComparer comparer = null) + { + return ToPooledDictionary(source.Span, keySelector, comparer); + } + + /// + /// Creates a from a sequence of key/value tuples. + /// + public static PooledDictionary ToPooledDictionary( + this IEnumerable<(TKey, TValue)> source, + IEqualityComparer comparer = null) + { + return new PooledDictionary(source, comparer); + } + + /// + /// Creates a from a sequence of KeyValuePair values. + /// + public static PooledDictionary ToPooledDictionary( + this IEnumerable> source, + IEqualityComparer comparer = null) + { + return new PooledDictionary(source, comparer); + } + + /// + /// Creates a from a sequence of key/value tuples. + /// + public static PooledDictionary ToPooledDictionary( + this IEnumerable> source, + IEqualityComparer comparer = null) + { + if (source == null) + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source); + var dict = new PooledDictionary((source as ICollection>)?.Count ?? 0, + comparer); + foreach (var pair in source) + { + dict.Add(pair.Item1, pair.Item2); + } + + return dict; + } + + /// + /// Creates a from a span of key/value tuples. + /// + public static PooledDictionary ToPooledDictionary( + this ReadOnlySpan<(TKey, TValue)> source, + IEqualityComparer comparer = null) + { + return new PooledDictionary(source, comparer); + } + + /// + /// Creates a from a span of key/value tuples. + /// + public static PooledDictionary ToPooledDictionary(this Span<(TKey, TValue)> source, + IEqualityComparer comparer = null) + { + return new PooledDictionary(source, comparer); + } + + #endregion + + #region PooledSet + + public static PooledSet ToPooledSet(this IEnumerable source, IEqualityComparer comparer = null) + => new PooledSet(source, comparer); + + public static PooledSet ToPooledSet(this Span source, IEqualityComparer comparer = null) + => new PooledSet(source, comparer); + + public static PooledSet ToPooledSet(this ReadOnlySpan source, IEqualityComparer comparer = null) + => new PooledSet(source, comparer); + + public static PooledSet ToPooledSet(this Memory source, IEqualityComparer comparer = null) + => new PooledSet(source.Span, comparer); + + public static PooledSet ToPooledSet(this ReadOnlyMemory source, IEqualityComparer comparer = null) + => new PooledSet(source.Span, comparer); + + #endregion + + #region PooledStack + + /// + /// Creates an instance of PooledStack from the given items. + /// + public static PooledStack ToPooledStack(this IEnumerable items) + => new PooledStack(items); + + /// + /// Creates an instance of PooledStack from the given items. + /// + public static PooledStack ToPooledStack(this T[] array) + => new PooledStack(array.AsSpan()); + + /// + /// Creates an instance of PooledStack from the given items. + /// + public static PooledStack ToPooledStack(this ReadOnlySpan span) + => new PooledStack(span); + + /// + /// Creates an instance of PooledStack from the given items. + /// + public static PooledStack ToPooledStack(this Span span) + => new PooledStack(span); + + /// + /// Creates an instance of PooledStack from the given items. + /// + public static PooledStack ToPooledStack(this ReadOnlyMemory memory) + => new PooledStack(memory.Span); + + /// + /// Creates an instance of PooledStack from the given items. + /// + public static PooledStack ToPooledStack(this Memory memory) + => new PooledStack(memory.Span); + + #endregion + + #region PooledQueue + + /// + /// Creates an instance of PooledQueue from the given items. + /// + public static PooledQueue ToPooledQueue(this IEnumerable items) + => new PooledQueue(items); + + /// + /// Creates an instance of PooledQueue from the given items. + /// + public static PooledQueue ToPooledQueue(this ReadOnlySpan span) + => new PooledQueue(span); + + /// + /// Creates an instance of PooledQueue from the given items. + /// + public static PooledQueue ToPooledQueue(this Span span) + => new PooledQueue(span); + + /// + /// Creates an instance of PooledQueue from the given items. + /// + public static PooledQueue ToPooledQueue(this ReadOnlyMemory memory) + => new PooledQueue(memory.Span); + + /// + /// Creates an instance of PooledQueue from the given items. + /// + public static PooledQueue ToPooledQueue(this Memory memory) + => new PooledQueue(memory.Span); + + /// + /// Creates an instance of PooledQueue from the given items. + /// + public static PooledQueue ToPooledQueue(this T[] array) + => new PooledQueue(array.AsSpan()); + + #endregion + } +} \ No newline at end of file diff --git a/OctaneEngine/Collections.Pooled/PooledList.cs b/OctaneEngine/Collections.Pooled/PooledList.cs new file mode 100644 index 0000000..5c092f9 --- /dev/null +++ b/OctaneEngine/Collections.Pooled/PooledList.cs @@ -0,0 +1,1665 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Buffers; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.Serialization; +using System.Threading; + +namespace Collections.Pooled +{ + /// + /// Implements a variable-size list that uses a pooled array to store the + /// elements. A PooledList has a capacity, which is the allocated length + /// of the internal array. As elements are added to a PooledList, the capacity + /// of the PooledList is automatically increased as required by reallocating the + /// internal array. + /// + /// + /// This class is based on the code for but it supports + /// and uses when allocating internal arrays. + /// + [DebuggerDisplay("Count = {Count}")] + [DebuggerTypeProxy(typeof(ICollectionDebugView<>))] + [Serializable] + public class PooledList : IList, IReadOnlyPooledList, IList, IDisposable, IDeserializationCallback + { + // internal constant copied from Array.MaxArrayLength + private const int MaxArrayLength = 0x7FEFFFFF; + private const int DefaultCapacity = 4; + private static readonly T[] s_emptyArray = Array.Empty(); + + [NonSerialized] private ArrayPool _pool; + [NonSerialized] private object _syncRoot; + + private T[] _items; // Do not rename (binary serialization) + private int _size; // Do not rename (binary serialization) + private int _version; // Do not rename (binary serialization) + private readonly bool _clearOnFree; + + #region Constructors + + /// + /// Constructs a PooledList. The list is initially empty and has a capacity + /// of zero. Upon adding the first element to the list the capacity is + /// increased to DefaultCapacity, and then increased in multiples of two + /// as required. + /// + public PooledList() : this(ClearMode.Auto, ArrayPool.Shared) + { + } + + /// + /// Constructs a PooledList. The list is initially empty and has a capacity + /// of zero. Upon adding the first element to the list the capacity is + /// increased to DefaultCapacity, and then increased in multiples of two + /// as required. + /// + public PooledList(ClearMode clearMode) : this(clearMode, ArrayPool.Shared) + { + } + + /// + /// Constructs a PooledList. The list is initially empty and has a capacity + /// of zero. Upon adding the first element to the list the capacity is + /// increased to DefaultCapacity, and then increased in multiples of two + /// as required. + /// + public PooledList(ArrayPool customPool) : this(ClearMode.Auto, customPool) + { + } + + /// + /// Constructs a PooledList. The list is initially empty and has a capacity + /// of zero. Upon adding the first element to the list the capacity is + /// increased to DefaultCapacity, and then increased in multiples of two + /// as required. + /// + public PooledList(ClearMode clearMode, ArrayPool customPool) + { + _items = s_emptyArray; + _pool = customPool ?? ArrayPool.Shared; + _clearOnFree = ShouldClear(clearMode); + } + + /// + /// Constructs a List with a given initial capacity. The list is + /// initially empty, but will have room for the given number of elements + /// before any reallocations are required. + /// + public PooledList(int capacity) : this(capacity, ClearMode.Auto, ArrayPool.Shared) + { + } + + /// + /// Constructs a List with a given initial capacity. The list is + /// initially empty, but will have room for the given number of elements + /// before any reallocations are required. + /// + public PooledList(int capacity, bool sizeToCapacity) : this(capacity, ClearMode.Auto, ArrayPool.Shared, + sizeToCapacity) + { + } + + /// + /// Constructs a List with a given initial capacity. The list is + /// initially empty, but will have room for the given number of elements + /// before any reallocations are required. + /// + public PooledList(int capacity, ClearMode clearMode) : this(capacity, clearMode, ArrayPool.Shared) + { + } + + /// + /// Constructs a List with a given initial capacity. The list is + /// initially empty, but will have room for the given number of elements + /// before any reallocations are required. + /// + public PooledList(int capacity, ClearMode clearMode, bool sizeToCapacity) : this(capacity, clearMode, + ArrayPool.Shared, sizeToCapacity) + { + } + + /// + /// Constructs a List with a given initial capacity. The list is + /// initially empty, but will have room for the given number of elements + /// before any reallocations are required. + /// + public PooledList(int capacity, ArrayPool customPool) : this(capacity, ClearMode.Auto, customPool) + { + } + + /// + /// Constructs a List with a given initial capacity. The list is + /// initially empty, but will have room for the given number of elements + /// before any reallocations are required. + /// + public PooledList(int capacity, ArrayPool customPool, bool sizeToCapacity) : this(capacity, ClearMode.Auto, + customPool, sizeToCapacity) + { + } + + /// + /// Constructs a List with a given initial capacity. The list is + /// initially empty, but will have room for the given number of elements + /// before any reallocations are required. + /// + public PooledList(int capacity, ClearMode clearMode, ArrayPool customPool) : this(capacity, clearMode, + customPool, false) + { + } + + /// + /// Constructs a List with a given initial capacity. The list is + /// initially empty, but will have room for the given number of elements + /// before any reallocations are required. + /// + /// If true, Count of list equals capacity. Depending on ClearMode, rented items may or may not hold dirty values. + public PooledList(int capacity, ClearMode clearMode, ArrayPool customPool, bool sizeToCapacity) + { + if (capacity < 0) + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.capacity, + ExceptionResource.ArgumentOutOfRange_NeedNonNegNum); + + _pool = customPool ?? ArrayPool.Shared; + _clearOnFree = ShouldClear(clearMode); + + if (capacity == 0) + { + _items = s_emptyArray; + } + else + { + _items = _pool.Rent(capacity); + } + + if (sizeToCapacity) + { + _size = capacity; + if (clearMode != ClearMode.Never) + { + Array.Clear(_items, 0, _size); + } + } + } + + /// + /// Constructs a PooledList, copying the contents of the given collection. The + /// size and capacity of the new list will both be equal to the size of the + /// given collection. + /// + public PooledList(T[] array) : this(array.AsSpan(), ClearMode.Auto, ArrayPool.Shared) + { + } + + /// + /// Constructs a PooledList, copying the contents of the given collection. The + /// size and capacity of the new list will both be equal to the size of the + /// given collection. + /// + public PooledList(T[] array, ClearMode clearMode) : this(array.AsSpan(), clearMode, ArrayPool.Shared) + { + } + + /// + /// Constructs a PooledList, copying the contents of the given collection. The + /// size and capacity of the new list will both be equal to the size of the + /// given collection. + /// + public PooledList(T[] array, ArrayPool customPool) : this(array.AsSpan(), ClearMode.Auto, customPool) + { + } + + /// + /// Constructs a PooledList, copying the contents of the given collection. The + /// size and capacity of the new list will both be equal to the size of the + /// given collection. + /// + public PooledList(T[] array, ClearMode clearMode, ArrayPool customPool) : this(array.AsSpan(), clearMode, + customPool) + { + } + + /// + /// Constructs a PooledList, copying the contents of the given collection. The + /// size and capacity of the new list will both be equal to the size of the + /// given collection. + /// + public PooledList(ReadOnlySpan span) : this(span, ClearMode.Auto, ArrayPool.Shared) + { + } + + /// + /// Constructs a PooledList, copying the contents of the given collection. The + /// size and capacity of the new list will both be equal to the size of the + /// given collection. + /// + public PooledList(ReadOnlySpan span, ClearMode clearMode) : this(span, clearMode, ArrayPool.Shared) + { + } + + /// + /// Constructs a PooledList, copying the contents of the given collection. The + /// size and capacity of the new list will both be equal to the size of the + /// given collection. + /// + public PooledList(ReadOnlySpan span, ArrayPool customPool) : this(span, ClearMode.Auto, customPool) + { + } + + /// + /// Constructs a PooledList, copying the contents of the given collection. The + /// size and capacity of the new list will both be equal to the size of the + /// given collection. + /// + public PooledList(ReadOnlySpan span, ClearMode clearMode, ArrayPool customPool) + { + _pool = customPool ?? ArrayPool.Shared; + _clearOnFree = ShouldClear(clearMode); + + int count = span.Length; + if (count == 0) + { + _items = s_emptyArray; + } + else + { + _items = _pool.Rent(count); + span.CopyTo(_items); + _size = count; + } + } + + /// + /// Constructs a PooledList, copying the contents of the given collection. The + /// size and capacity of the new list will both be equal to the size of the + /// given collection. + /// + public PooledList(IEnumerable collection) : this(collection, ClearMode.Auto, ArrayPool.Shared) + { + } + + /// + /// Constructs a PooledList, copying the contents of the given collection. The + /// size of the new list will be equal to the size of the given collection + /// and the capacity will be equal to suggestCapacity + /// + public PooledList(IEnumerable collection, int suggestCapacity) : this(collection, ClearMode.Auto, + ArrayPool.Shared, suggestCapacity) + { + } + + /// + /// Constructs a PooledList, copying the contents of the given collection. The + /// size and capacity of the new list will both be equal to the size of the + /// given collection. + /// + public PooledList(IEnumerable collection, ClearMode clearMode) : this(collection, clearMode, + ArrayPool.Shared) + { + } + + /// + /// Constructs a PooledList, copying the contents of the given collection. The + /// size and capacity of the new list will both be equal to the size of the + /// given collection. + /// + public PooledList(IEnumerable collection, ArrayPool customPool) : this(collection, ClearMode.Auto, + customPool) + { + } + + /// + /// Constructs a PooledList, copying the contents of the given collection. The + /// size and capacity of the new list will both be equal to the size of the + /// given collection. + /// + public PooledList(IEnumerable collection, ClearMode clearMode, ArrayPool customPool, + int suggestCapacity = 0) + { + _pool = customPool ?? ArrayPool.Shared; + _clearOnFree = ShouldClear(clearMode); + + switch (collection) + { + case null: + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.collection); + break; + + case ICollection c: + { + int count = c.Count; + if (count == 0) + { + _items = s_emptyArray; + } + else + { + _items = _pool.Rent(count); + c.CopyTo(_items, 0); + _size = count; + } + + break; + } + + case ICollection c: + { + int count = c.Count; + if (count == 0) + { + _items = s_emptyArray; + } + else + { + _items = _pool.Rent(count); + c.CopyTo(_items, 0); + _size = count; + } + + break; + } + + case IReadOnlyCollection c: + { + int count = c.Count; + if (count == 0) + { + _items = s_emptyArray; + } + else + { + _items = _pool.Rent(count); + _size = 0; + using (var en = c.GetEnumerator()) + { + while (en.MoveNext()) + Add(en.Current); + } + } + + break; + } + + default: + + if (suggestCapacity < 0) + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.capacity, + ExceptionResource.ArgumentOutOfRange_NeedNonNegNum); + + if (suggestCapacity == 0) + { + _items = s_emptyArray; + } + else + { + _items = _pool.Rent(suggestCapacity); + } + + using (var en = collection.GetEnumerator()) + { + while (en.MoveNext()) + Add(en.Current); + } + + break; + } + } + + #endregion + + /// + /// Gets a for the items currently in the collection. + /// + public Span Span => _items.AsSpan(0, _size); + + /// + ReadOnlySpan IReadOnlyPooledList.Span => Span; + + /// + /// Gets and sets the capacity of this list. The capacity is the size of + /// the internal array used to hold items. When set, the internal + /// Memory of the list is reallocated to the given capacity. + /// Note that the return value for this property may be larger than the property was set to. + /// + public int Capacity + { + get => _items.Length; + set + { + if (value < _size) + { + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.value, + ExceptionResource.ArgumentOutOfRange_SmallCapacity); + } + + if (value != _items.Length) + { + if (value > 0) + { + var newItems = _pool.Rent(value); + if (_size > 0) + { + Array.Copy(_items, newItems, _size); + } + + ReturnArray(); + _items = newItems; + } + else + { + ReturnArray(); + _size = 0; + } + } + } + } + + /// + /// Read-only property describing how many elements are in the List. + /// + public int Count => _size; + + /// + /// Returns the ClearMode behavior for the collection, denoting whether values are + /// cleared from internal arrays before returning them to the pool. + /// + public ClearMode ClearMode => _clearOnFree ? ClearMode.Always : ClearMode.Never; + + bool IList.IsFixedSize => false; + + bool ICollection.IsReadOnly => false; + + bool IList.IsReadOnly => false; + + int ICollection.Count => _size; + + bool ICollection.IsSynchronized => false; + + // Synchronization root for this object. + object ICollection.SyncRoot + { + get + { + if (_syncRoot == null) + { + Interlocked.CompareExchange(ref _syncRoot, new object(), null); + } + + return _syncRoot; + } + } + + /// + /// Gets or sets the element at the given index. + /// + public T this[int index] + { + get + { + // Following trick can reduce the range check by one + if ((uint)index >= (uint)_size) + { + ThrowHelper.ThrowArgumentOutOfRange_IndexException(); + } + + return _items[index]; + } + + set + { + if ((uint)index >= (uint)_size) + { + ThrowHelper.ThrowArgumentOutOfRange_IndexException(); + } + + _items[index] = value; + _version++; + } + } + + private static bool IsCompatibleObject(object value) + { + // Non-null values are fine. Only accept nulls if T is a class or Nullable. + // Note that default(T) is not equal to null for value types except when T is Nullable. + return ((value is T) || (value == null && default(T) == null)); + } + + object IList.this[int index] + { + get { return this[index]; } + set + { + ThrowHelper.IfNullAndNullsAreIllegalThenThrow(value, ExceptionArgument.value); + + try + { + this[index] = (T)value; + } + catch (InvalidCastException) + { + ThrowHelper.ThrowWrongValueTypeArgumentException(value, typeof(T)); + } + } + } + + /// + /// Adds the given object to the end of this list. The size of the list is + /// increased by one. If required, the capacity of the list is doubled + /// before adding the new element. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Add(T item) + { + _version++; + int size = _size; + if ((uint)size < (uint)_items.Length) + { + _size = size + 1; + _items[size] = item; + } + else + { + AddWithResize(item); + } + } + + // Non-inline from List.Add to improve its code quality as uncommon path + [MethodImpl(MethodImplOptions.NoInlining)] + private void AddWithResize(T item) + { + int size = _size; + EnsureCapacity(size + 1); + _size = size + 1; + _items[size] = item; + } + + int IList.Add(object item) + { + ThrowHelper.IfNullAndNullsAreIllegalThenThrow(item, ExceptionArgument.item); + + try + { + Add((T)item); + } + catch (InvalidCastException) + { + ThrowHelper.ThrowWrongValueTypeArgumentException(item, typeof(T)); + } + + return Count - 1; + } + + /// + /// Adds the elements of the given collection to the end of this list. If + /// required, the capacity of the list is increased to twice the previous + /// capacity or the new size, whichever is larger. + /// + public void AddRange(IEnumerable collection) + => InsertRange(_size, collection); + + /// + /// Adds the elements of the given array to the end of this list. If + /// required, the capacity of the list is increased to twice the previous + /// capacity or the new size, whichever is larger. + /// + public void AddRange(T[] array) + => AddRange(array.AsSpan()); + + /// + /// Adds the elements of the given to the end of this list. If + /// required, the capacity of the list is increased to twice the previous + /// capacity or the new size, whichever is larger. + /// + public void AddRange(ReadOnlySpan span) + { + var newSpan = InsertSpan(_size, span.Length, false); + span.CopyTo(newSpan); + } + + /// + /// Advances the by the number of items specified, + /// increasing the capacity if required, then returns a Span representing + /// the set of items to be added, allowing direct writes to that section + /// of the collection. + /// + /// The number of items to add. + public Span AddSpan(int count) + => InsertSpan(_size, count); + + public ReadOnlyCollection AsReadOnly() + => new ReadOnlyCollection(this); + + /// + /// Searches a section of the list for a given element using a binary search + /// algorithm. + /// + /// + /// Elements of the list are compared to the search value using + /// the given IComparer interface. If comparer is null, elements of + /// the list are compared to the search value using the IComparable + /// interface, which in that case must be implemented by all elements of the + /// list and the given search value. This method assumes that the given + /// section of the list is already sorted; if this is not the case, the + /// result will be incorrect. + /// + /// The method returns the index of the given value in the list. If the + /// list does not contain the given value, the method returns a negative + /// integer. The bitwise complement operator (~) can be applied to a + /// negative result to produce the index of the first element (if any) that + /// is larger than the given search value. This is also the index at which + /// the search value should be inserted into the list in order for the list + /// to remain sorted. + /// + public int BinarySearch(int index, int count, T item, IComparer comparer) + { + if (index < 0) + ThrowHelper.ThrowIndexArgumentOutOfRange_NeedNonNegNumException(); + if (count < 0) + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.count, + ExceptionResource.ArgumentOutOfRange_NeedNonNegNum); + if (_size - index < count) + ThrowHelper.ThrowArgumentException(ExceptionResource.Argument_InvalidOffLen); + + return Array.BinarySearch(_items, index, count, item, comparer); + } + + /// + /// Searches the list for a given element using a binary search + /// algorithm. If the item implements + /// then that is used for comparison, otherwise is used. + /// + public int BinarySearch(T item) + => BinarySearch(0, Count, item, null); + + /// + /// Searches the list for a given element using a binary search + /// algorithm. If the item implements + /// then that is used for comparison, otherwise is used. + /// + public int BinarySearch(T item, IComparer comparer) + => BinarySearch(0, Count, item, comparer); + + /// + /// Clears the contents of the PooledList. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Clear() + { + _version++; + int size = _size; + _size = 0; + + if (size > 0 && _clearOnFree) + { + // Clear the elements so that the gc can reclaim the references. + Array.Clear(_items, 0, size); + } + } + + /// + /// Contains returns true if the specified element is in the List. + /// It does a linear, O(n) search. Equality is determined by calling + /// EqualityComparer{T}.Default.Equals. + /// + public bool Contains(T item) + { + // PERF: IndexOf calls Array.IndexOf, which internally + // calls EqualityComparer.Default.IndexOf, which + // is specialized for different types. This + // boosts performance since instead of making a + // virtual method call each iteration of the loop, + // via EqualityComparer.Default.Equals, we + // only make one virtual call to EqualityComparer.IndexOf. + + return _size != 0 && IndexOf(item) != -1; + } + + bool IList.Contains(object item) + { + if (IsCompatibleObject(item)) + { + return Contains((T)item); + } + + return false; + } + + public PooledList ConvertAll(Func converter) + { + if (converter == null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.converter); + } + + var list = new PooledList(_size); + for (int i = 0; i < _size; i++) + { + list._items[i] = converter(_items[i]); + } + + list._size = _size; + return list; + } + + /// + /// Copies this list to the given span. + /// + public void CopyTo(Span span) + { + if (span.Length < Count) + throw new ArgumentException("Destination span is shorter than the list to be copied."); + + Span.CopyTo(span); + } + + void ICollection.CopyTo(T[] array, int arrayIndex) + { + Array.Copy(_items, 0, array, arrayIndex, _size); + } + + // Copies this List into array, which must be of a + // compatible array type. + void ICollection.CopyTo(Array array, int arrayIndex) + { + if ((array != null) && (array.Rank != 1)) + { + ThrowHelper.ThrowArgumentException(ExceptionResource.Arg_RankMultiDimNotSupported); + } + + try + { + // Array.Copy will check for NULL. + Array.Copy(_items, 0, array, arrayIndex, _size); + } + catch (ArrayTypeMismatchException) + { + ThrowHelper.ThrowArgumentException_Argument_InvalidArrayType(); + } + } + + /// + /// Ensures that the capacity of this list is at least the given minimum + /// value. If the current capacity of the list is less than min, the + /// capacity is increased to twice the current capacity or to min, + /// whichever is larger. + /// + private void EnsureCapacity(int min) + { + if (_items.Length < min) + { + int newCapacity = _items.Length == 0 ? DefaultCapacity : _items.Length * 2; + // Allow the list to grow to maximum possible capacity (~2G elements) before encountering overflow. + // Note that this check works even when _items.Length overflowed thanks to the (uint) cast + if ((uint)newCapacity > MaxArrayLength) newCapacity = MaxArrayLength; + if (newCapacity < min) newCapacity = min; + Capacity = newCapacity; + } + } + + public bool Exists(Func match) + => FindIndex(match) != -1; + + public bool TryFind(Func match, out T result) + { + if (match == null) + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match); + + for (int i = 0; i < _size; i++) + { + if (match(_items[i])) + { + result = _items[i]; + return true; + } + } + + result = default; + return false; + } + + public PooledList FindAll(Func match) + { + if (match == null) + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match); + + var list = new PooledList(); + for (int i = 0; i < _size; i++) + { + if (match(_items[i])) + { + list.Add(_items[i]); + } + } + + return list; + } + + public int FindIndex(Func match) + => FindIndex(0, _size, match); + + public int FindIndex(int startIndex, Func match) + => FindIndex(startIndex, _size - startIndex, match); + + public int FindIndex(int startIndex, int count, Func match) + { + if ((uint)startIndex > (uint)_size) + ThrowHelper.ThrowStartIndexArgumentOutOfRange_ArgumentOutOfRange_Index(); + + if (count < 0 || startIndex > _size - count) + ThrowHelper.ThrowCountArgumentOutOfRange_ArgumentOutOfRange_Count(); + + if (match is null) + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match); + + int endIndex = startIndex + count; + for (int i = startIndex; i < endIndex; i++) + { + if (match(_items[i])) return i; + } + + return -1; + } + + public bool TryFindLast(Func match, out T result) + { + if (match is null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match); + } + + for (int i = _size - 1; i >= 0; i--) + { + if (match(_items[i])) + { + result = _items[i]; + return true; + } + } + + result = default; + return false; + } + + public int FindLastIndex(Func match) + => FindLastIndex(_size - 1, _size, match); + + public int FindLastIndex(int startIndex, Func match) + => FindLastIndex(startIndex, startIndex + 1, match); + + public int FindLastIndex(int startIndex, int count, Func match) + { + if (match == null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match); + } + + if (_size == 0) + { + // Special case for 0 length List + if (startIndex != -1) + { + ThrowHelper.ThrowStartIndexArgumentOutOfRange_ArgumentOutOfRange_Index(); + } + } + else + { + // Make sure we're not out of range + if ((uint)startIndex >= (uint)_size) + { + ThrowHelper.ThrowStartIndexArgumentOutOfRange_ArgumentOutOfRange_Index(); + } + } + + // 2nd half of this also catches when startIndex == MAXINT, so MAXINT - 0 + 1 == -1, which is < 0. + if (count < 0 || startIndex - count + 1 < 0) + { + ThrowHelper.ThrowCountArgumentOutOfRange_ArgumentOutOfRange_Count(); + } + + int endIndex = startIndex - count; + for (int i = startIndex; i > endIndex; i--) + { + if (match(_items[i])) + { + return i; + } + } + + return -1; + } + + public void ForEach(Action action) + { + if (action == null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.action); + } + + int version = _version; + for (int i = 0; i < _size; i++) + { + if (version != _version) + { + break; + } + + action(_items[i]); + } + + if (version != _version) + ThrowHelper.ThrowInvalidOperationException_InvalidOperation_EnumFailedVersion(); + } + + /// + /// Returns an enumerator for this list with the given + /// permission for removal of elements. If modifications made to the list + /// while an enumeration is in progress, the MoveNext and + /// GetObject methods of the enumerator will throw an exception. + /// + public Enumerator GetEnumerator() + => new Enumerator(this); + + IEnumerator IEnumerable.GetEnumerator() + => new Enumerator(this); + + IEnumerator IEnumerable.GetEnumerator() + => new Enumerator(this); + + /// + /// Equivalent to PooledList.Span.Slice(index, count). + /// + public Span GetRange(int index, int count) + { + if (index < 0) + { + ThrowHelper.ThrowIndexArgumentOutOfRange_NeedNonNegNumException(); + } + + if (count < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.count, + ExceptionResource.ArgumentOutOfRange_NeedNonNegNum); + } + + if (_size - index < count) + { + ThrowHelper.ThrowArgumentException(ExceptionResource.Argument_InvalidOffLen); + } + + return Span.Slice(index, count); + } + + /// + /// Returns the index of the first occurrence of a given value in + /// this list. The list is searched forwards from beginning to end. + /// + public int IndexOf(T item) + => Array.IndexOf(_items, item, 0, _size); + + int IList.IndexOf(object item) + { + if (IsCompatibleObject(item)) + { + return IndexOf((T)item); + } + + return -1; + } + + /// + /// Returns the index of the first occurrence of a given value in a range of + /// this list. The list is searched forwards, starting at index + /// index and ending at count number of elements. + /// + public int IndexOf(T item, int index) + { + if (index > _size) + ThrowHelper.ThrowArgumentOutOfRange_IndexException(); + return Array.IndexOf(_items, item, index, _size - index); + } + + /// + /// Returns the index of the first occurrence of a given value in a range of + /// this list. The list is searched forwards, starting at index + /// index and upto count number of elements. + /// + public int IndexOf(T item, int index, int count) + { + if (index > _size) + ThrowHelper.ThrowArgumentOutOfRange_IndexException(); + + if (count < 0 || index > _size - count) + ThrowHelper.ThrowCountArgumentOutOfRange_ArgumentOutOfRange_Count(); + + return Array.IndexOf(_items, item, index, count); + } + + /// + /// Inserts an element into this list at a given index. The size of the list + /// is increased by one. If required, the capacity of the list is doubled + /// before inserting the new element. + /// + public void Insert(int index, T item) + { + // Note that insertions at the end are legal. + if ((uint)index > (uint)_size) + { + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.index, + ExceptionResource.ArgumentOutOfRange_ListInsert); + } + + if (_size == _items.Length) EnsureCapacity(_size + 1); + if (index < _size) + { + Array.Copy(_items, index, _items, index + 1, _size - index); + } + + _items[index] = item; + _size++; + _version++; + } + + void IList.Insert(int index, object item) + { + ThrowHelper.IfNullAndNullsAreIllegalThenThrow(item, ExceptionArgument.item); + + try + { + Insert(index, (T)item); + } + catch (InvalidCastException) + { + ThrowHelper.ThrowWrongValueTypeArgumentException(item, typeof(T)); + } + } + + /// + /// Inserts the elements of the given collection at a given index. If + /// required, the capacity of the list is increased to twice the previous + /// capacity or the new size, whichever is larger. Ranges may be added + /// to the end of the list by setting index to the List's size. + /// + public void InsertRange(int index, IEnumerable collection) + { + if ((uint)index > (uint)_size) + { + ThrowHelper.ThrowArgumentOutOfRange_IndexException(); + } + + switch (collection) + { + case null: + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.collection); + break; + + case ICollection c: + int count = c.Count; + if (count > 0) + { + EnsureCapacity(_size + count); + if (index < _size) + { + Array.Copy(_items, index, _items, index + count, _size - index); + } + + // If we're inserting a List into itself, we want to be able to deal with that. + if (this == c) + { + // Copy first part of _items to insert location + Array.Copy(_items, 0, _items, index, index); + // Copy last part of _items back to inserted location + Array.Copy(_items, index + count, _items, index * 2, _size - index); + } + else + { + c.CopyTo(_items, index); + } + + _size += count; + } + + break; + + default: + using (var en = collection.GetEnumerator()) + { + while (en.MoveNext()) + { + Insert(index++, en.Current); + } + } + + break; + } + + _version++; + } + + /// + /// Inserts the elements of the given collection at a given index. If + /// required, the capacity of the list is increased to twice the previous + /// capacity or the new size, whichever is larger. Ranges may be added + /// to the end of the list by setting index to the List's size. + /// + public void InsertRange(int index, ReadOnlySpan span) + { + var newSpan = InsertSpan(index, span.Length, false); + span.CopyTo(newSpan); + } + + /// + /// Inserts the elements of the given collection at a given index. If + /// required, the capacity of the list is increased to twice the previous + /// capacity or the new size, whichever is larger. Ranges may be added + /// to the end of the list by setting index to the List's size. + /// + public void InsertRange(int index, T[] array) + { + if (array is null) + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.array); + InsertRange(index, array.AsSpan()); + } + + /// + /// Advances the by the number of items specified, + /// increasing the capacity if required, then returns a Span representing + /// the set of items to be added, allowing direct writes to that section + /// of the collection. + /// + public Span InsertSpan(int index, int count) + => InsertSpan(index, count, true); + + private Span InsertSpan(int index, int count, bool clearOutput) + { + EnsureCapacity(_size + count); + + if (index < _size) + { + Array.Copy(_items, index, _items, index + count, _size - index); + } + + _size += count; + _version++; + + var output = _items.AsSpan(index, count); + + if (clearOutput && _clearOnFree) + { + output.Clear(); + } + + return output; + } + + /// + /// Returns the index of the last occurrence of a given value in a range of + /// this list. The list is searched backwards, starting at the end + /// and ending at the first element in the list. + /// + public int LastIndexOf(T item) + { + if (_size == 0) + { + // Special case for empty list + return -1; + } + else + { + return LastIndexOf(item, _size - 1, _size); + } + } + + /// + /// Returns the index of the last occurrence of a given value in a range of + /// this list. The list is searched backwards, starting at index + /// index and ending at the first element in the list. + /// + public int LastIndexOf(T item, int index) + { + if (index >= _size) + ThrowHelper.ThrowArgumentOutOfRange_IndexException(); + return LastIndexOf(item, index, index + 1); + } + + /// + /// Returns the index of the last occurrence of a given value in a range of + /// this list. The list is searched backwards, starting at index + /// index and upto count elements + /// + public int LastIndexOf(T item, int index, int count) + { + if (Count != 0 && index < 0) + { + ThrowHelper.ThrowIndexArgumentOutOfRange_NeedNonNegNumException(); + } + + if (Count != 0 && count < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.count, + ExceptionResource.ArgumentOutOfRange_NeedNonNegNum); + } + + if (_size == 0) + { + // Special case for empty list + return -1; + } + + if (index >= _size) + { + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.index, + ExceptionResource.ArgumentOutOfRange_BiggerThanCollection); + } + + if (count > index + 1) + { + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.count, + ExceptionResource.ArgumentOutOfRange_BiggerThanCollection); + } + + return Array.LastIndexOf(_items, item, index, count); + } + + // Removes the element at the given index. The size of the list is + // decreased by one. + public bool Remove(T item) + { + int index = IndexOf(item); + if (index >= 0) + { + RemoveAt(index); + return true; + } + + return false; + } + + void IList.Remove(object item) + { + if (IsCompatibleObject(item)) + { + Remove((T)item); + } + } + + /// + /// This method removes all items which match the predicate. + /// The complexity is O(n). + /// + public int RemoveAll(Func match) + { + if (match == null) + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match); + + int freeIndex = 0; // the first free slot in items array + + // Find the first item which needs to be removed. + while (freeIndex < _size && !match(_items[freeIndex])) freeIndex++; + if (freeIndex >= _size) return 0; + + int current = freeIndex + 1; + while (current < _size) + { + // Find the first item which needs to be kept. + while (current < _size && match(_items[current])) current++; + + if (current < _size) + { + // copy item to the free slot. + _items[freeIndex++] = _items[current++]; + } + } + + if (_clearOnFree) + { + // Clear the removed elements so that the gc can reclaim the references. + Array.Clear(_items, freeIndex, _size - freeIndex); + } + + int result = _size - freeIndex; + _size = freeIndex; + _version++; + return result; + } + + /// + /// Removes the element at the given index. The size of the list is + /// decreased by one. + /// + public void RemoveAt(int index) + { + if ((uint)index >= (uint)_size) + ThrowHelper.ThrowArgumentOutOfRange_IndexException(); + + _size--; + if (index < _size) + { + Array.Copy(_items, index + 1, _items, index, _size - index); + } + + _version++; + + if (_clearOnFree) + { + // Clear the removed element so that the gc can reclaim the reference. + _items[_size] = default; + } + } + + /// + /// Removes a range of elements from this list. + /// + public void RemoveRange(int index, int count) + { + if (index < 0) + ThrowHelper.ThrowIndexArgumentOutOfRange_NeedNonNegNumException(); + + if (count < 0) + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.count, + ExceptionResource.ArgumentOutOfRange_NeedNonNegNum); + + if (_size - index < count) + ThrowHelper.ThrowArgumentException(ExceptionResource.Argument_InvalidOffLen); + + if (count > 0) + { + _size -= count; + if (index < _size) + { + Array.Copy(_items, index + count, _items, index, _size - index); + } + + _version++; + + if (_clearOnFree) + { + // Clear the removed elements so that the gc can reclaim the references. + Array.Clear(_items, _size, count); + } + } + } + + /// + /// Reverses the elements in this list. + /// + public void Reverse() + => Reverse(0, _size); + + /// + /// Reverses the elements in a range of this list. Following a call to this + /// method, an element in the range given by index and count + /// which was previously located at index i will now be located at + /// index index + (index + count - i - 1). + /// + public void Reverse(int index, int count) + { + if (index < 0) + ThrowHelper.ThrowIndexArgumentOutOfRange_NeedNonNegNumException(); + + if (count < 0) + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.count, + ExceptionResource.ArgumentOutOfRange_NeedNonNegNum); + + if (_size - index < count) + ThrowHelper.ThrowArgumentException(ExceptionResource.Argument_InvalidOffLen); + + if (count > 1) + { + Array.Reverse(_items, index, count); + } + + _version++; + } + + /// + /// Sorts the elements in this list. Uses the default comparer and + /// Array.Sort. + /// + public void Sort() + => Sort(0, Count, null); + + /// + /// Sorts the elements in this list. Uses Array.Sort with the + /// provided comparer. + /// + /// + public void Sort(IComparer comparer) + => Sort(0, Count, comparer); + + /// + /// Sorts the elements in a section of this list. The sort compares the + /// elements to each other using the given IComparer interface. If + /// comparer is null, the elements are compared to each other using + /// the IComparable interface, which in that case must be implemented by all + /// elements of the list. + /// + /// This method uses the Array.Sort method to sort the elements. + /// + public void Sort(int index, int count, IComparer comparer) + { + if (index < 0) + ThrowHelper.ThrowIndexArgumentOutOfRange_NeedNonNegNumException(); + + if (count < 0) + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.count, + ExceptionResource.ArgumentOutOfRange_NeedNonNegNum); + + if (_size - index < count) + ThrowHelper.ThrowArgumentException(ExceptionResource.Argument_InvalidOffLen); + + if (count > 1) + { + Array.Sort(_items, index, count, comparer); + } + + _version++; + } + + public void Sort(Func comparison) + { + if (comparison == null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.comparison); + } + + if (_size > 1) + { + // List uses ArraySortHelper here but since it's an internal class, + // we're creating an IComparer using the comparison function to avoid + // duplicating all that code. + Array.Sort(_items, 0, _size, new Comparer(comparison)); + } + + _version++; + } + + /// + /// ToArray returns an array containing the contents of the List. + /// This requires copying the List, which is an O(n) operation. + /// + public T[] ToArray() + { + if (_size == 0) + { + return s_emptyArray; + } + + return Span.ToArray(); + } + + /// + /// Sets the capacity of this list to the size of the list. This method can + /// be used to minimize a list's memory overhead once it is known that no + /// new elements will be added to the list. To completely clear a list and + /// release all memory referenced by the list, execute the following + /// statements: + /// + /// list.Clear(); + /// list.TrimExcess(); + /// + /// + public void TrimExcess() + { + int threshold = (int)(_items.Length * 0.9); + if (_size < threshold) + { + Capacity = _size; + } + } + + public bool TrueForAll(Func match) + { + if (match == null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match); + } + + for (int i = 0; i < _size; i++) + { + if (!match(_items[i])) + { + return false; + } + } + + return true; + } + + private void ReturnArray() + { + if (_items.Length == 0) + return; + + try + { + // Clear the elements so that the gc can reclaim the references. + _pool.Return(_items, clearArray: _clearOnFree); + } + catch (ArgumentException) + { + // oh well, the array pool didn't like our array + } + + _items = s_emptyArray; + } + + private static bool ShouldClear(ClearMode mode) + { +#if NETCOREAPP2_1 + return mode == ClearMode.Always + || (mode == ClearMode.Auto && RuntimeHelpers.IsReferenceOrContainsReferences()); +#else + return mode != ClearMode.Never; +#endif + } + + /// + /// Returns the internal buffers to the ArrayPool. + /// + public void Dispose() + { + ReturnArray(); + _size = 0; + _version++; + } + + void IDeserializationCallback.OnDeserialization(object sender) + { + // We can't serialize array pools, so deserialized PooledLists will + // have to use the shared pool, even if they were using a custom pool + // before serialization. + _pool = ArrayPool.Shared; + } + + public struct Enumerator : IEnumerator, IEnumerator + { + private readonly PooledList _list; + private int _index; + private readonly int _version; + private T _current; + + internal Enumerator(PooledList list) + { + _list = list; + _index = 0; + _version = list._version; + _current = default; + } + + public void Dispose() + { + } + + public bool MoveNext() + { + var localList = _list; + + if (_version == localList._version && ((uint)_index < (uint)localList._size)) + { + _current = localList._items[_index]; + _index++; + return true; + } + + return MoveNextRare(); + } + + private bool MoveNextRare() + { + if (_version != _list._version) + { + ThrowHelper.ThrowInvalidOperationException_InvalidOperation_EnumFailedVersion(); + } + + _index = _list._size + 1; + _current = default; + return false; + } + + public T Current => _current; + + object IEnumerator.Current + { + get + { + if (_index == 0 || _index == _list._size + 1) + { + ThrowHelper.ThrowInvalidOperationException_InvalidOperation_EnumOpCantHappen(); + } + + return Current; + } + } + + void IEnumerator.Reset() + { + if (_version != _list._version) + { + ThrowHelper.ThrowInvalidOperationException_InvalidOperation_EnumFailedVersion(); + } + + _index = 0; + _current = default; + } + } + + private readonly struct Comparer : IComparer + { + private readonly Func _comparison; + + public Comparer(Func comparison) + { + _comparison = comparison; + } + + public int Compare(T x, T y) => _comparison(x, y); + } + } +} \ No newline at end of file diff --git a/OctaneEngine/Collections.Pooled/PooledQueue.cs b/OctaneEngine/Collections.Pooled/PooledQueue.cs new file mode 100644 index 0000000..d03dc25 --- /dev/null +++ b/OctaneEngine/Collections.Pooled/PooledQueue.cs @@ -0,0 +1,768 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +/*============================================================================= +** +** +** Purpose: A circular-array implementation of a generic queue. +** +** +=============================================================================*/ + +using System; +using System.Buffers; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Runtime.Serialization; +using System.Threading; + +namespace Collections.Pooled +{ + /// + /// A simple Queue of generic objects. Internally it is implemented as a + /// circular buffer, so Enqueue can be O(n). Dequeue is O(1). + /// + /// The type to store in the queue. + [DebuggerTypeProxy(typeof(QueueDebugView<>))] + [DebuggerDisplay("Count = {Count}")] + [Serializable] + public class PooledQueue : IEnumerable, ICollection, IReadOnlyCollection, IDisposable, IDeserializationCallback + { + private const int MinimumGrow = 4; + private const int GrowFactor = 200; // double each time + + [NonSerialized] + private ArrayPool _pool; + [NonSerialized] + private object _syncRoot; + + private T[] _array; + private int _head; // The index from which to dequeue if the queue isn't empty. + private int _tail; // The index at which to enqueue if the queue isn't full. + private int _size; // Number of elements. + private int _version; + private readonly bool _clearOnFree; + + #region Constructors + + /// + /// Initializes a new instance of the class that is empty and has the default initial capacity. + /// + public PooledQueue() : this(ClearMode.Auto, ArrayPool.Shared) { } + + /// + /// Initializes a new instance of the class that is empty and has the default initial capacity. + /// + public PooledQueue(ClearMode clearMode) : this(clearMode, ArrayPool.Shared) { } + + /// + /// Initializes a new instance of the class that is empty and has the default initial capacity. + /// + public PooledQueue(ArrayPool customPool) : this(ClearMode.Auto, customPool) { } + + /// + /// Initializes a new instance of the class that is empty and has the default initial capacity. + /// + public PooledQueue(ClearMode clearMode, ArrayPool customPool) + { + _pool = customPool ?? ArrayPool.Shared; + _array = Array.Empty(); + _clearOnFree = ShouldClear(clearMode); + } + + /// + /// Initializes a new instance of the class that is empty and has the specified initial capacity. + /// + public PooledQueue(int capacity) : this(capacity, ClearMode.Auto, ArrayPool.Shared) { } + + /// + /// Initializes a new instance of the class that is empty and has the specified initial capacity. + /// + public PooledQueue(int capacity, ClearMode clearMode) : this(capacity, clearMode, ArrayPool.Shared) { } + + /// + /// Initializes a new instance of the class that is empty and has the specified initial capacity. + /// + public PooledQueue(int capacity, ArrayPool customPool) : this(capacity, ClearMode.Auto, customPool) { } + + /// + /// Initializes a new instance of the class that is empty and has the specified initial capacity. + /// + public PooledQueue(int capacity, ClearMode clearMode, ArrayPool customPool) + { + if (capacity < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.capacity, + ExceptionResource.ArgumentOutOfRange_NeedNonNegNum); + } + _pool = customPool ?? ArrayPool.Shared; + _array = _pool.Rent(capacity); + _clearOnFree = ShouldClear(clearMode); + } + + /// + /// Initializes a new instance of the class that contains elements copied from the specified + /// collection and has sufficient capacity to accommodate the number of elements copied. + /// + public PooledQueue(IEnumerable enumerable) : this(enumerable, ClearMode.Auto, ArrayPool.Shared) { } + + /// + /// Initializes a new instance of the class that contains elements copied from the specified + /// collection and has sufficient capacity to accommodate the number of elements copied. + /// + public PooledQueue(IEnumerable enumerable, ClearMode clearMode) : this(enumerable, clearMode, ArrayPool.Shared) { } + + /// + /// Initializes a new instance of the class that contains elements copied from the specified + /// collection and has sufficient capacity to accommodate the number of elements copied. + /// + public PooledQueue(IEnumerable enumerable, ArrayPool customPool) : this(enumerable, ClearMode.Auto, customPool) { } + + /// + /// Initializes a new instance of the class that contains elements copied from the specified + /// collection and has sufficient capacity to accommodate the number of elements copied. + /// + public PooledQueue(IEnumerable enumerable, ClearMode clearMode, ArrayPool customPool) + { + _pool = customPool ?? ArrayPool.Shared; + _clearOnFree = ShouldClear(clearMode); + + switch (enumerable) + { + case null: + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.enumerable); + break; + + case ICollection collection: + if (collection.Count == 0) + { + _array = Array.Empty(); + } + else + { + _array = _pool.Rent(collection.Count); + collection.CopyTo(_array, 0); + _size = collection.Count; + if (_size != _array.Length) _tail = _size; + } + break; + + default: + using (var list = new PooledList(enumerable)) + { + _array = _pool.Rent(list.Count); + list.Span.CopyTo(_array); + _size = list.Count; + if (_size != _array.Length) _tail = _size; + } + break; + } + } + + /// + /// Initializes a new instance of the class that contains elements copied from the specified + /// array and has sufficient capacity to accommodate the number of elements copied. + /// + public PooledQueue(T[] array) : this(array.AsSpan(), ClearMode.Auto, ArrayPool.Shared) { } + + /// + /// Initializes a new instance of the class that contains elements copied from the specified + /// array and has sufficient capacity to accommodate the number of elements copied. + /// + public PooledQueue(T[] array, ClearMode clearMode) : this(array.AsSpan(), clearMode, ArrayPool.Shared) { } + + /// + /// Initializes a new instance of the class that contains elements copied from the specified + /// array and has sufficient capacity to accommodate the number of elements copied. + /// + public PooledQueue(T[] array, ArrayPool customPool) : this(array.AsSpan(), ClearMode.Auto, customPool) { } + + /// + /// Initializes a new instance of the class that contains elements copied from the specified + /// array and has sufficient capacity to accommodate the number of elements copied. + /// + public PooledQueue(T[] array, ClearMode clearMode, ArrayPool customPool) : this(array.AsSpan(), clearMode, customPool) { } + + /// + /// Initializes a new instance of the class that contains elements copied from the specified + /// span and has sufficient capacity to accommodate the number of elements copied. + /// + public PooledQueue(ReadOnlySpan span) : this(span, ClearMode.Auto, ArrayPool.Shared) { } + + /// + /// Initializes a new instance of the class that contains elements copied from the specified + /// span and has sufficient capacity to accommodate the number of elements copied. + /// + public PooledQueue(ReadOnlySpan span, ClearMode clearMode) : this(span, clearMode, ArrayPool.Shared) { } + + /// + /// Initializes a new instance of the class that contains elements copied from the specified + /// span and has sufficient capacity to accommodate the number of elements copied. + /// + public PooledQueue(ReadOnlySpan span, ArrayPool customPool) : this(span, ClearMode.Auto, customPool) { } + + /// + /// Initializes a new instance of the class that contains elements copied from the specified + /// span and has sufficient capacity to accommodate the number of elements copied. + /// + public PooledQueue(ReadOnlySpan span, ClearMode clearMode, ArrayPool customPool) + { + _pool = customPool ?? ArrayPool.Shared; + _clearOnFree = ShouldClear(clearMode); + _array = _pool.Rent(span.Length); + span.CopyTo(_array); + _size = span.Length; + if (_size != _array.Length) _tail = _size; + } + + #endregion + + /// + /// The number of items in the queue. + /// + public int Count => _size; + + /// + /// Returns the ClearMode behavior for the collection, denoting whether values are + /// cleared from internal arrays before returning them to the pool. + /// + public ClearMode ClearMode => _clearOnFree ? ClearMode.Always : ClearMode.Never; + + bool ICollection.IsSynchronized => false; + + object ICollection.SyncRoot + { + get + { + if (_syncRoot == null) + { + Interlocked.CompareExchange(ref _syncRoot, new object(), null); + } + return _syncRoot; + } + } + + /// + /// Removes all objects from the queue. + /// + public void Clear() + { + if (_size != 0) + { + if (_clearOnFree) + { + if (_head < _tail) + { + Array.Clear(_array, _head, _size); + } + else + { + Array.Clear(_array, _head, _array.Length - _head); + Array.Clear(_array, 0, _tail); + } + } + _size = 0; + } + + _head = 0; + _tail = 0; + _version++; + } + + /// + /// CopyTo copies a collection into an Array, starting at a particular + /// index into the array. + /// + public void CopyTo(T[] array, int arrayIndex) + { + if (array == null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.array); + } + + if ((uint)arrayIndex > (uint)array.Length) + { + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.arrayIndex, + ExceptionResource.ArgumentOutOfRange_Index); + } + + int arrayLen = array.Length; + if (arrayLen - arrayIndex < _size) + { + ThrowHelper.ThrowArgumentException(ExceptionResource.Argument_InvalidOffLen); + } + + int numToCopy = _size; + if (numToCopy == 0) return; + + int firstPart = Math.Min(_array.Length - _head, numToCopy); + Array.Copy(_array, _head, array, arrayIndex, firstPart); + numToCopy -= firstPart; + if (numToCopy > 0) + { + Array.Copy(_array, 0, array, arrayIndex + _array.Length - _head, numToCopy); + } + } + + void ICollection.CopyTo(Array array, int index) + { + if (array == null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.array); + } + + if (array.Rank != 1) + { + ThrowHelper.ThrowArgumentException(ExceptionResource.Rank_MultiDimNotSupported, + ExceptionArgument.array); + } + + if (array.GetLowerBound(0) != 0) + { + ThrowHelper.ThrowArgumentException(ExceptionResource.Arg_NonZeroLowerBound, + ExceptionArgument.array); + } + + int arrayLen = array.Length; + if ((uint)index > (uint)arrayLen) + { + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.index, + ExceptionResource.ArgumentOutOfRange_Index); + } + + if (arrayLen - index < _size) + { + ThrowHelper.ThrowArgumentException(ExceptionResource.Argument_InvalidOffLen); + } + + int numToCopy = _size; + if (numToCopy == 0) return; + + try + { + int firstPart = (_array.Length - _head < numToCopy) ? _array.Length - _head : numToCopy; + Array.Copy(_array, _head, array, index, firstPart); + numToCopy -= firstPart; + + if (numToCopy > 0) + { + Array.Copy(_array, 0, array, index + _array.Length - _head, numToCopy); + } + } + catch (ArrayTypeMismatchException) + { + ThrowHelper.ThrowArgumentException_Argument_InvalidArrayType(); + } + } + + /// + /// Adds to the tail of the queue. + /// + public void Enqueue(T item) + { + if (_size == _array.Length) + { + int newcapacity = (int)(_array.Length * (long)GrowFactor / 100); + if (newcapacity < _array.Length + MinimumGrow) + { + newcapacity = _array.Length + MinimumGrow; + } + SetCapacity(newcapacity); + } + + _array[_tail] = item; + MoveNext(ref _tail); + _size++; + _version++; + } + + /// + /// GetEnumerator returns an IEnumerator over this Queue. This + /// Enumerator will support removing. + /// + public Enumerator GetEnumerator() + => new Enumerator(this); + + /// + IEnumerator IEnumerable.GetEnumerator() + => new Enumerator(this); + + IEnumerator IEnumerable.GetEnumerator() + => new Enumerator(this); + + /// + /// Removes the object at the head of the queue and returns it. If the queue + /// is empty, this method throws an + /// . + /// + public T Dequeue() + { + int head = _head; + T[] array = _array; + + if (_size == 0) + { + ThrowForEmptyQueue(); + } + + T removed = array[head]; + if (_clearOnFree) + { + array[head] = default; + } + MoveNext(ref _head); + _size--; + _version++; + return removed; + } + + public bool TryDequeue(out T result) + { + int head = _head; + T[] array = _array; + + if (_size == 0) + { + result = default; + return false; + } + + result = array[head]; + if (_clearOnFree) + { + array[head] = default; + } + MoveNext(ref _head); + _size--; + _version++; + return true; + } + + /// + /// Returns the object at the head of the queue. The object remains in the + /// queue. If the queue is empty, this method throws an + /// . + /// + public T Peek() + { + if (_size == 0) + { + ThrowForEmptyQueue(); + } + + return _array[_head]; + } + + public bool TryPeek(out T result) + { + if (_size == 0) + { + result = default; + return false; + } + + result = _array[_head]; + return true; + } + + /// + /// Returns true if the queue contains at least one object equal to item. + /// Equality is determined using . + /// + /// + /// + public bool Contains(T item) + { + if (_size == 0) + { + return false; + } + + if (_head < _tail) + { + return Array.IndexOf(_array, item, _head, _size) >= 0; + } + + // We've wrapped around. Check both partitions, the least recently enqueued first. + return + Array.IndexOf(_array, item, _head, _array.Length - _head) >= 0 || + Array.IndexOf(_array, item, 0, _tail) >= 0; + } + + /// + /// This method removes all items from the queue which match the predicate. + /// + public int RemoveWhere(Func match) + { + if (match == null) + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match); + + if (_size == 0) + return 0; + + T[] newArray = _pool.Rent(_size); + int removeCount = 0; + + if (_head < _tail) + { + int copyIdx = 0; + for (int i = _head; i < _size; i++) + { + if (match(_array[i])) + removeCount++; + else + newArray[copyIdx++] = _array[i]; + } + } + else + { + int copyIdx = 0; + for (int i = _head; i < _array.Length - _head; i++) + { + if (match(_array[i])) + removeCount++; + else + newArray[copyIdx++] = _array[i]; + } + + for (int i = 0; i < _tail; i++) + { + if (match(_array[i])) + removeCount++; + else + newArray[copyIdx++] = _array[i]; + } + } + + ReturnArray(replaceWith: newArray); + _size -= removeCount; + _head = _tail = 0; + if (_size != _array.Length) _tail = _size; + _version++; + + return removeCount; + } + + /// + /// Iterates over the objects in the queue, returning an array of the + /// objects in the Queue, or an empty array if the queue is empty. + /// The order of elements in the array is first in to last in, the same + /// order produced by successive calls to Dequeue. + /// + public T[] ToArray() + { + if (_size == 0) + { + return Array.Empty(); + } + + T[] arr = new T[_size]; + + if (_head < _tail) + { + Array.Copy(_array, _head, arr, 0, _size); + } + else + { + Array.Copy(_array, _head, arr, 0, _array.Length - _head); + Array.Copy(_array, 0, arr, _array.Length - _head, _tail); + } + + return arr; + } + + // PRIVATE Grows or shrinks the buffer to hold capacity objects. Capacity + // must be >= _size. + private void SetCapacity(int capacity) + { + T[] newarray = _pool.Rent(capacity); + if (_size > 0) + { + if (_head < _tail) + { + Array.Copy(_array, _head, newarray, 0, _size); + } + else + { + Array.Copy(_array, _head, newarray, 0, _array.Length - _head); + Array.Copy(_array, 0, newarray, _array.Length - _head, _tail); + } + } + + ReturnArray(replaceWith: newarray); + _head = 0; + _tail = (_size == newarray.Length) ? 0 : _size; + _version++; + } + + // Increments the index wrapping it if necessary. + private void MoveNext(ref int index) + { + // It is tempting to use the remainder operator here but it is actually much slower + // than a simple comparison and a rarely taken branch. + // JIT produces better code than with ternary operator ?: + int tmp = index + 1; + if (tmp == _array.Length) + { + tmp = 0; + } + index = tmp; + } + + private void ThrowForEmptyQueue() + { + Debug.Assert(_size == 0); + throw new InvalidOperationException("Queue is empty."); + } + + public void TrimExcess() + { + int threshold = (int)(_array.Length * 0.9); + if (_size < threshold) + { + SetCapacity(_size); + } + } + + private void ReturnArray(T[] replaceWith) + { + if (_array.Length > 0) + { + try + { + _pool.Return(_array, clearArray: _clearOnFree); + } + catch (ArgumentException) + { + // oh well, the array pool didn't like our array + } + } + _array = replaceWith; + } + + private static bool ShouldClear(ClearMode mode) + { +#if NETCOREAPP2_1 + return mode == ClearMode.Always + || (mode == ClearMode.Auto && RuntimeHelpers.IsReferenceOrContainsReferences()); +#else + return mode != ClearMode.Never; +#endif + } + + public void Dispose() + { + ReturnArray(replaceWith: Array.Empty()); + _head = _tail = _size = 0; + _version++; + } + + void IDeserializationCallback.OnDeserialization(object sender) + { + // We can't serialize array pools, so deserialized PooledQueue will + // have to use the shared pool, even if they were using a custom pool + // before serialization. + _pool = ArrayPool.Shared; + } + + // Implements an enumerator for a Queue. The enumerator uses the + // internal version number of the list to ensure that no modifications are + // made to the list while an enumeration is in progress. + [SuppressMessage("Microsoft.Performance", "CA1815:OverrideEqualsAndOperatorEqualsOnValueTypes", Justification = "not an expected scenario")] + public struct Enumerator : IEnumerator, IEnumerator + { + private readonly PooledQueue _q; + private readonly int _version; + private int _index; // -1 = not started, -2 = ended/disposed + private T _currentElement; + + internal Enumerator(PooledQueue q) + { + _q = q; + _version = q._version; + _index = -1; + _currentElement = default; + } + + public void Dispose() + { + _index = -2; + _currentElement = default; + } + + public bool MoveNext() + { + if (_version != _q._version) + ThrowHelper.ThrowInvalidOperationException_InvalidOperation_EnumFailedVersion(); + + if (_index == -2) + return false; + + _index++; + + if (_index == _q._size) + { + // We've run past the last element + _index = -2; + _currentElement = default; + return false; + } + + // Cache some fields in locals to decrease code size + T[] array = _q._array; + int capacity = array.Length; + + // _index represents the 0-based index into the queue, however the queue + // doesn't have to start from 0 and it may not even be stored contiguously in memory. + + int arrayIndex = _q._head + _index; // this is the actual index into the queue's backing array + if (arrayIndex >= capacity) + { + // NOTE: Originally we were using the modulo operator here, however + // on Intel processors it has a very high instruction latency which + // was slowing down the loop quite a bit. + // Replacing it with simple comparison/subtraction operations sped up + // the average foreach loop by 2x. + + arrayIndex -= capacity; // wrap around if needed + } + + _currentElement = array[arrayIndex]; + return true; + } + + public T Current + { + get + { + if (_index < 0) + ThrowEnumerationNotStartedOrEnded(); + return _currentElement; + } + } + + private void ThrowEnumerationNotStartedOrEnded() + { + Debug.Assert(_index == -1 || _index == -2); + if (_index == -1) + ThrowHelper.ThrowInvalidOperationException_InvalidOperation_EnumNotStarted(); + else + ThrowHelper.ThrowInvalidOperationException_InvalidOperation_EnumEnded(); + } + + object IEnumerator.Current => Current; + + void IEnumerator.Reset() + { + if (_version != _q._version) + ThrowHelper.ThrowInvalidOperationException_InvalidOperation_EnumFailedVersion(); + _index = -1; + _currentElement = default; + } + } + } +} diff --git a/OctaneEngine/Collections.Pooled/PooledSet.cs b/OctaneEngine/Collections.Pooled/PooledSet.cs new file mode 100644 index 0000000..880d43e --- /dev/null +++ b/OctaneEngine/Collections.Pooled/PooledSet.cs @@ -0,0 +1,2724 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Buffers; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Runtime.Serialization; + +namespace Collections.Pooled +{ + /// + /// Represents a set of values. + /// + /// + /// Implementation notes: + /// This uses an array-based implementation similar to , using a buckets array + /// to map hash values to the Slots array. Items in the Slots array that hash to the same value + /// are chained together through the "next" indices. + /// + /// The capacity is always prime; so during resizing, the capacity is chosen as the next prime + /// greater than double the last capacity. + /// + /// The underlying data structures are lazily initialized. Because of the observation that, + /// in practice, hashtables tend to contain only a few elements, the initial capacity is + /// set very small (3 elements) unless the ctor with a collection is used. + /// + /// The +/- 1 modifications in methods that add, check for containment, etc allow us to + /// distinguish a hash code of 0 from an uninitialized bucket. This saves us from having to + /// reset each bucket to -1 when resizing. See Contains, for example. + /// + /// Set methods such as UnionWith, IntersectWith, ExceptWith, and SymmetricExceptWith modify + /// this set. + /// + /// Some operations can perform faster if we can assume "other" contains unique elements + /// according to this equality comparer. The only times this is efficient to check is if + /// other is a hashset. Note that checking that it's a hashset alone doesn't suffice; we + /// also have to check that the hashset is using the same equality comparer. If other + /// has a different equality comparer, it will have unique elements according to its own + /// equality comparer, but not necessarily according to ours. Therefore, to go these + /// optimized routes we check that other is a hashset using the same equality comparer. + /// + /// A HashSet with no elements has the properties of the empty set. (See IsSubset, etc. for + /// special empty set checks.) + /// + /// A couple of methods have a special case if other is this (e.g. SymmetricExceptWith). + /// If we didn't have these checks, we could be iterating over the set and modifying at + /// the same time. + /// + /// + [DebuggerTypeProxy(typeof(ICollectionDebugView<>))] + [DebuggerDisplay("Count = {Count}")] + [SuppressMessage("Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix", Justification = "By design")] + [Serializable] + public class PooledSet : ICollection, ISet, IReadOnlyCollection, ISerializable, IDeserializationCallback, IDisposable + { + // store lower 31 bits of hash code + private const int Lower31BitMask = 0x7FFFFFFF; + // cutoff point, above which we won't do stackallocs. This corresponds to 100 integers. + private const int StackAllocThreshold = 100; + // when constructing a hashset from an existing collection, it may contain duplicates, + // so this is used as the max acceptable excess ratio of capacity to count. Note that + // this is only used on the ctor and not to automatically shrink if the hashset has, e.g, + // a lot of adds followed by removes. Users must explicitly shrink by calling TrimExcess. + // This is set to 3 because capacity is acceptable as 2x rounded up to nearest prime. + private const int ShrinkThreshold = 3; + + // constants for serialization + private const string CapacityName = "Capacity"; // Do not rename (binary serialization) + private const string ElementsName = "Elements"; // Do not rename (binary serialization) + private const string ComparerName = "Comparer"; // Do not rename (binary serialization) + private const string VersionName = "Version"; // Do not rename (binary serialization) + + private static readonly ArrayPool s_bucketPool = ArrayPool.Shared; + private static readonly ArrayPool s_slotPool = ArrayPool.Shared; + + // WARNING: + // It's important that the number of buckets be prime, and these arrays could exceed + // that size as they come from ArrayPool. Be careful not to index past _size or bad + // things will happen. + // Alternatively, use the private properties Buckets and Slots, which slice the + // arrays down to the correct length. + private int[] _buckets; + private Slot[] _slots; + private int _size; + + private int _count; + private int _lastIndex; + private int _freeList; + private IEqualityComparer _comparer; + private int _version; + private readonly bool _clearOnFree; + + private SerializationInfo _siInfo; // temporary variable needed during deserialization + + #region Constructors + + /// + /// Creates a new instance of PooledSet. + /// + public PooledSet() : this(ClearMode.Auto, EqualityComparer.Default) { } + + /// + /// Creates a new instance of PooledSet. + /// + public PooledSet(ClearMode clearMode) : this(clearMode, EqualityComparer.Default) { } + + /// + /// Creates a new instance of PooledSet. + /// + public PooledSet(IEqualityComparer comparer) : this(ClearMode.Auto, comparer) { } + + /// + /// Creates a new instance of PooledSet. + /// + public PooledSet(ClearMode clearMode, IEqualityComparer comparer) + { + _comparer = comparer ?? EqualityComparer.Default; + _lastIndex = 0; + _count = 0; + _freeList = -1; + _version = 0; + _size = 0; + _clearOnFree = ShouldClear(clearMode); + } + + /// + /// Creates a new instance of PooledSet. + /// + public PooledSet(int capacity) : this(capacity, ClearMode.Auto, EqualityComparer.Default) { } + + /// + /// Creates a new instance of PooledSet. + /// + public PooledSet(int capacity, ClearMode clearMode) : this(capacity, clearMode, EqualityComparer.Default) { } + + /// + /// Creates a new instance of PooledSet. + /// + public PooledSet(int capacity, IEqualityComparer comparer) : this(capacity, ClearMode.Auto, comparer) { } + + /// + /// Creates a new instance of PooledSet. + /// + public PooledSet(int capacity, ClearMode clearMode, IEqualityComparer comparer) : this(clearMode, comparer) + { + if (capacity < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.capacity, ExceptionResource.ArgumentOutOfRange_NeedNonNegNum); + } + + if (capacity > 0) + { + Initialize(capacity); + } + } + + /// + /// Creates a new instance of PooledSet. + /// + public PooledSet(IEnumerable collection) + : this(collection, ClearMode.Auto, + collection is PooledSet ps ? ps.Comparer : collection is HashSet hs ? hs.Comparer : EqualityComparer.Default) + { } + + /// + /// Creates a new instance of PooledSet. + /// + public PooledSet(IEnumerable collection, ClearMode clearMode) + : this(collection, clearMode, + collection is PooledSet ps ? ps.Comparer : collection is HashSet hs ? hs.Comparer : EqualityComparer.Default) + { } + + /// + /// Creates a new instance of PooledSet. + /// + public PooledSet(IEnumerable collection, IEqualityComparer comparer) + : this(collection, ClearMode.Auto, comparer) + { } + + /// + /// Implementation Notes: + /// Since resizes are relatively expensive (require rehashing), this attempts to minimize + /// the need to resize by setting the initial capacity based on size of collection. + /// + public PooledSet(IEnumerable collection, ClearMode clearMode, IEqualityComparer comparer) : this(clearMode, comparer) + { + if (collection == null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.collection); + } + + if (collection is PooledSet otherAsSet && AreEqualityComparersEqual(this, otherAsSet)) + { + CopyFrom(otherAsSet); + } + else + { + // to avoid excess resizes, first set size based on collection's count. Collection + // may contain duplicates, so call TrimExcess if resulting hashset is larger than + // threshold + int suggestedCapacity = (collection is ICollection coll) ? coll.Count : 0; + Initialize(suggestedCapacity); + + UnionWith(collection); + + if (_count > 0 && _size / _count > ShrinkThreshold) + { + TrimExcess(); + } + } + } + + /// + /// Creates a new instance of PooledSet. + /// + public PooledSet(T[] array) : this(array.AsSpan(), ClearMode.Auto, null) { } + + /// + /// Creates a new instance of PooledSet. + /// + public PooledSet(T[] array, ClearMode clearMode) : this(array.AsSpan(), clearMode, null) { } + + /// + /// Creates a new instance of PooledSet. + /// + public PooledSet(T[] array, IEqualityComparer comparer) : this(array.AsSpan(), ClearMode.Auto, comparer) { } + + /// + /// Creates a new instance of PooledSet. + /// + public PooledSet(T[] array, ClearMode clearMode, IEqualityComparer comparer) : this(array.AsSpan(), clearMode, comparer) { } + + /// + /// Creates a new instance of PooledSet. + /// + public PooledSet(ReadOnlySpan span) : this(span, ClearMode.Auto, null) { } + + /// + /// Creates a new instance of PooledSet. + /// + public PooledSet(ReadOnlySpan span, ClearMode clearMode) : this(span, clearMode, null) { } + + /// + /// Creates a new instance of PooledSet. + /// + public PooledSet(ReadOnlySpan span, IEqualityComparer comparer) : this(span, ClearMode.Auto, comparer) { } + + /// + /// Creates a new instance of PooledSet. + /// + public PooledSet(ReadOnlySpan span, ClearMode clearMode, IEqualityComparer comparer) : this(clearMode, comparer) + { + // to avoid excess resizes, first set size based on collection's count. Collection + // may contain duplicates, so call TrimExcess if resulting hashset is larger than + // threshold + Initialize(span.Length); + + UnionWith(span); + + if (_count > 0 && _size / _count > ShrinkThreshold) + { + TrimExcess(); + } + } + + /// + /// Creates a new instance of PooledSet. + /// +#pragma warning disable IDE0060 // Remove unused parameter + protected PooledSet(SerializationInfo info, StreamingContext context) +#pragma warning restore IDE0060 + { + // We can't do anything with the keys and values until the entire graph has been + // deserialized and we have a reasonable estimate that GetHashCode is not going to + // fail. For the time being, we'll just cache this. The graph is not valid until + // OnDeserialization has been called. + _siInfo = info; + } + + // Initializes the HashSet from another HashSet with the same element type and + // equality comparer. + private void CopyFrom(PooledSet source) + { + int count = source._count; + if (count == 0) + { + // As well as short-circuiting on the rest of the work done, + // this avoids errors from trying to access otherAsHashSet._buckets + // or otherAsHashSet._slots when they aren't initialized. + return; + } + + int capacity = _size = source._size; + int threshold = HashHelpers.ExpandPrime(count + 1); + + if (threshold >= capacity) + { + _buckets = s_bucketPool.Rent(capacity); + Array.Clear(_buckets, 0, _buckets.Length); + Array.Copy(source._buckets, _buckets, capacity); + _slots = s_slotPool.Rent(capacity); + Array.Copy(source._slots, _slots, capacity); + + _lastIndex = source._lastIndex; + _freeList = source._freeList; + } + else + { + int lastIndex = source._lastIndex; + Slot[] slots = source._slots; + Initialize(count); + int index = 0; + for (int i = 0; i < lastIndex; ++i) + { + int hashCode = slots[i].hashCode; + if (hashCode >= 0) + { + AddValue(index, hashCode, slots[i].value); + ++index; + } + } + Debug.Assert(index == count); + _lastIndex = index; + } + _count = count; + } + + #endregion + + #region ICollection methods + + /// + /// Add item to this hashset. This is the explicit implementation of the + /// interface. The other Add method returns bool indicating whether item was added. + /// + /// item to add + void ICollection.Add(T item) + => AddIfNotPresent(item); + + /// + /// Remove all items from this set. This clears the elements but not the underlying + /// buckets and slots array. Follow this call by TrimExcess to release these. + /// + public void Clear() + { + if (_lastIndex > 0) + { + Debug.Assert(_buckets != null, "_buckets was null but _lastIndex > 0"); + + // clear the elements so that the gc can reclaim the references. + // clear only up to _lastIndex for _slots + Array.Clear(_slots, 0, _lastIndex); + Array.Clear(_buckets, 0, _buckets.Length); + _lastIndex = 0; + _count = 0; + _freeList = -1; + } + _version++; + } + + /// + /// Checks if this hashset contains the item + /// + /// item to check for containment + /// true if item contained; false if not + public bool Contains(T item) + { + if (_buckets != null) + { + int collisionCount = 0; + int hashCode = InternalGetHashCode(item); + Slot[] slots = _slots; + // see note at "HashSet" level describing why "- 1" appears in for loop + for (int i = _buckets[hashCode % _size] - 1; i >= 0; i = slots[i].next) + { + if (slots[i].hashCode == hashCode && _comparer.Equals(slots[i].value, item)) + { + return true; + } + + if (collisionCount >= _size) + { + // The chain of entries forms a loop, which means a concurrent update has happened. + ThrowHelper.ThrowInvalidOperationException_ConcurrentOperationsNotSupported(); + } + collisionCount++; + } + } + // either _buckets is null or wasn't found + return false; + } + + /// + /// Copy items in this hashset to array, starting at arrayIndex + /// + /// array to add items to + /// index to start at + public void CopyTo(T[] array, int arrayIndex) + => CopyTo(array, arrayIndex, _count); + + /// + /// Remove item from this hashset + /// + /// item to remove + /// true if removed; false if not (i.e. if the item wasn't in the HashSet) + public bool Remove(T item) + { + if (_buckets != null) + { + int hashCode = InternalGetHashCode(item); + int bucket = hashCode % _size; + int last = -1; + int collisionCount = 0; + Slot[] slots = _slots; + for (int i = _buckets[bucket] - 1; i >= 0; last = i, i = slots[i].next) + { + if (slots[i].hashCode == hashCode && _comparer.Equals(slots[i].value, item)) + { + if (last < 0) + { + // first iteration; update buckets + _buckets[bucket] = slots[i].next + 1; + } + else + { + // subsequent iterations; update 'next' pointers + slots[last].next = slots[i].next; + } + slots[i].hashCode = -1; + if (_clearOnFree) + { + slots[i].value = default; + } + slots[i].next = _freeList; + + _count--; + _version++; + if (_count == 0) + { + _lastIndex = 0; + _freeList = -1; + } + else + { + _freeList = i; + } + return true; + } + + if (collisionCount >= _size) + { + // The chain of entries forms a loop, which means a concurrent update has happened. + ThrowHelper.ThrowInvalidOperationException_ConcurrentOperationsNotSupported(); + } + collisionCount++; + } + } + // either _buckets is null or wasn't found + return false; + } + + /// + /// Number of elements in this set + /// + public int Count => _count; + + /// + /// Returns the ClearMode behavior for the collection, denoting whether values are + /// cleared from internal arrays before returning them to the pool. + /// + public ClearMode ClearMode => _clearOnFree ? ClearMode.Always : ClearMode.Never; + + /// + /// Whether this is readonly + /// + bool ICollection.IsReadOnly => false; + + #endregion + + #region IEnumerable methods + + /// + /// Gets an enumerator with which to enumerate the set. + /// + public Enumerator GetEnumerator() + => new Enumerator(this); + + IEnumerator IEnumerable.GetEnumerator() + => new Enumerator(this); + + IEnumerator IEnumerable.GetEnumerator() + => new Enumerator(this); + + #endregion + + #region ISerializable methods + + /// + /// Gets object data for serialization. + /// + public virtual void GetObjectData(SerializationInfo info, StreamingContext context) + { + if (info == null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.info); + } + + info.AddValue(VersionName, _version); // need to serialize version to avoid problems with serializing while enumerating + info.AddValue(ComparerName, _comparer, typeof(IEqualityComparer)); + info.AddValue(CapacityName, _buckets == null ? 0 : _size); + + if (_buckets != null) + { + T[] array = new T[_count]; + CopyTo(array); + info.AddValue(ElementsName, array, typeof(T[])); + } + } + + #endregion + + #region IDeserializationCallback methods + + /// + /// Deserialization callback. + /// + public virtual void OnDeserialization(object sender) + { + if (_siInfo == null) + { + // It might be necessary to call OnDeserialization from a container if the + // container object also implements OnDeserialization. We can return immediately + // if this function is called twice. Note we set _siInfo to null at the end of this method. + return; + } + + int capacity = _siInfo.GetInt32(CapacityName); + _comparer = (IEqualityComparer)_siInfo.GetValue(ComparerName, typeof(IEqualityComparer)); + _freeList = -1; + + if (capacity != 0) + { + Initialize(capacity); + + T[] array = (T[])_siInfo.GetValue(ElementsName, typeof(T[])); + + if (array == null) + { + ThrowHelper.ThrowSerializationException(ExceptionResource.Serialization_MissingKeys); + } + + // there are no resizes here because we already set capacity above + for (int i = 0; i < array.Length; i++) + { + AddIfNotPresent(array[i]); + } + } + else + { + _buckets = null; + } + + _version = _siInfo.GetInt32(VersionName); + _siInfo = null; + } + + #endregion + + #region HashSet methods + + /// + /// Add item to this PooledSet. Returns bool indicating whether item was added (won't be + /// added if already present) + /// + /// + /// true if added, false if already present + public bool Add(T item) + => AddIfNotPresent(item); + + /// + /// Searches the set for a given value and returns the equal value it finds, if any. + /// + /// The value to search for. + /// The value from the set that the search found, or the default value of when the search yielded no match. + /// A value indicating whether the search was successful. + /// + /// This can be useful when you want to reuse a previously stored reference instead of + /// a newly constructed one (so that more sharing of references can occur) or to look up + /// a value that has more complete data than the value you currently have, although their + /// comparer functions indicate they are equal. + /// + public bool TryGetValue(T equalValue, out T actualValue) + { + if (_buckets != null) + { + int i = InternalIndexOf(equalValue); + if (i >= 0) + { + actualValue = _slots[i].value; + return true; + } + } + actualValue = default; + return false; + } + + /// + /// Take the union of this HashSet with other. Modifies this set. + /// + /// + /// Implementation note: GetSuggestedCapacity (to increase capacity in advance avoiding + /// multiple resizes ended up not being useful in practice; quickly gets to the + /// point where it's a wasteful check. + /// + /// enumerable with items to add + public void UnionWith(IEnumerable other) + { + if (other == null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.other); + } + + foreach (T item in other) + { + AddIfNotPresent(item); + } + } + + /// + /// Take the union of this PooledSet with other. Modifies this set. + /// + /// + public void UnionWith(T[] other) => UnionWith((ReadOnlySpan)other); + + + /// + /// Take the union of this PooledSet with other. Modifies this set. + /// + /// enumerable with items to add + public void UnionWith(ReadOnlySpan other) + { + for (int i = 0, len = other.Length; i < len; i++) + { + AddIfNotPresent(other[i]); + } + } + + /// + /// Takes the intersection of this set with other. Modifies this set. + /// + /// + /// Implementation Notes: + /// We get better perf if other is a hashset using same equality comparer, because we + /// get constant contains check in other. Resulting cost is O(n1) to iterate over this. + /// + /// If we can't go above route, iterate over the other and mark intersection by checking + /// contains in this. Then loop over and delete any unmarked elements. Total cost is n2+n1. + /// + /// Attempts to return early based on counts alone, using the property that the + /// intersection of anything with the empty set is the empty set. + /// + /// enumerable with items to add + public void IntersectWith(IEnumerable other) + { + if (other == null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.other); + } + + // intersection of anything with empty set is empty set, so return if count is 0 + if (_count == 0) + { + return; + } + + // set intersecting with itself is the same set + if (other == this) + { + return; + } + + // if other is empty, intersection is empty set; remove all elements and we're done + // can only figure this out if implements ICollection. (IEnumerable has no count) + if (other is ICollection otherAsCollection) + { + if (otherAsCollection.Count == 0) + { + Clear(); + return; + } + + // faster if other is a hashset using same equality comparer; so check + // that other is a hashset using the same equality comparer. + if (other is PooledSet otherAsSet && AreEqualityComparersEqual(this, otherAsSet)) + { + IntersectWithHashSetWithSameEC(otherAsSet); + return; + } + + if (other is HashSet otherAsHs && AreEqualityComparersEqual(this, otherAsHs)) + { + IntersectWithHashSetWithSameEC(otherAsHs); + return; + } + } + + IntersectWithEnumerable(other); + } + + /// + /// Takes the intersection of this set with other. Modifies this set. + /// + /// + /// Implementation Notes: + /// Iterate over the other and mark intersection by checking + /// contains in this. Then loop over and delete any unmarked elements. Total cost is n2+n1. + /// + /// Attempts to return early based on counts alone, using the property that the + /// intersection of anything with the empty set is the empty set. + /// + /// enumerable with items to add + public void IntersectWith(T[] other) => IntersectWith((ReadOnlySpan)other); + + /// + /// Takes the intersection of this set with other. Modifies this set. + /// + /// + /// Implementation Notes: + /// Iterate over the other and mark intersection by checking + /// contains in this. Then loop over and delete any unmarked elements. Total cost is n2+n1. + /// + /// Attempts to return early based on counts alone, using the property that the + /// intersection of anything with the empty set is the empty set. + /// + /// enumerable with items to add + public void IntersectWith(ReadOnlySpan other) + { + // intersection of anything with empty set is empty set, so return if count is 0 + if (_count == 0) + { + return; + } + + // if other is empty, intersection is empty set; remove all elements and we're done + if (other.Length == 0) + { + Clear(); + return; + } + + IntersectWithSpan(other); + } + + /// + /// Remove items in other from this set. Modifies this set. + /// + /// enumerable with items to remove + public void ExceptWith(IEnumerable other) + { + if (other == null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.other); + } + + // this is already the empty set; return + if (_count == 0) + { + return; + } + + // special case if other is this; a set minus itself is the empty set + if (other == this) + { + Clear(); + return; + } + + // remove every element in other from this + foreach (T element in other) + { + Remove(element); + } + } + + /// + /// Remove items in other from this set. Modifies this set. + /// + /// enumerable with items to remove + public void ExceptWith(T[] other) => ExceptWith((ReadOnlySpan)other); + + /// + /// Remove items in other from this set. Modifies this set. + /// + /// enumerable with items to remove + public void ExceptWith(ReadOnlySpan other) + { + if (other == null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.other); + } + + // this is already the empty set; return + if (_count == 0) + { + return; + } + + // remove every element in other from this + for (int i = 0, len = other.Length; i < len; i++) + { + Remove(other[i]); + } + } + + /// + /// Takes symmetric difference (XOR) with other and this set. Modifies this set. + /// + /// enumerable with items to XOR + public void SymmetricExceptWith(IEnumerable other) + { + if (other == null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.other); + } + + // if set is empty, then symmetric difference is other + if (_count == 0) + { + UnionWith(other); + return; + } + + // special case this; the symmetric difference of a set with itself is the empty set + if (other == this) + { + Clear(); + return; + } + + // If other is a HashSet, it has unique elements according to its equality comparer, + // but if they're using different equality comparers, then assumption of uniqueness + // will fail. So first check if other is a hashset using the same equality comparer; + // symmetric except is a lot faster and avoids bit array allocations if we can assume + // uniqueness + if (other is PooledSet otherAsSet && AreEqualityComparersEqual(this, otherAsSet)) + { + SymmetricExceptWithUniqueHashSet(otherAsSet); + } + else if (other is HashSet otherAsHs && AreEqualityComparersEqual(this, otherAsHs)) + { + SymmetricExceptWithUniqueHashSet(otherAsHs); + } + else + { + SymmetricExceptWithEnumerable(other); + } + } + + /// + /// Takes symmetric difference (XOR) with other and this set. Modifies this set. + /// + /// array with items to XOR + public void SymmetricExceptWith(T[] other) => SymmetricExceptWith((ReadOnlySpan)other); + + /// + /// Takes symmetric difference (XOR) with other and this set. Modifies this set. + /// + /// span with items to XOR + public void SymmetricExceptWith(ReadOnlySpan other) + { + // if set is empty, then symmetric difference is other + if (_count == 0) + { + UnionWith(other); + return; + } + + SymmetricExceptWithSpan(other); + } + + /// + /// Checks if this is a subset of other. + /// + /// + /// Implementation Notes: + /// The following properties are used up-front to avoid element-wise checks: + /// 1. If this is the empty set, then it's a subset of anything, including the empty set + /// 2. If other has unique elements according to this equality comparer, and this has more + /// elements than other, then it can't be a subset. + /// + /// Furthermore, if other is a hashset using the same equality comparer, we can use a + /// faster element-wise check. + /// + /// + /// true if this is a subset of other; false if not + public bool IsSubsetOf(IEnumerable other) + { + if (other == null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.other); + } + + // The empty set is a subset of any set + if (_count == 0) + { + return true; + } + + // Set is always a subset of itself + if (other == this) + { + return true; + } + + // faster if other has unique elements according to this equality comparer; so check + // that other is a hashset using the same equality comparer. + if (other is PooledSet otherAsSet && AreEqualityComparersEqual(this, otherAsSet)) + { + // if this has more elements then it can't be a subset + if (_count > otherAsSet.Count) + { + return false; + } + + // already checked that we're using same equality comparer. simply check that + // each element in this is contained in other. + return IsSubsetOfHashSetWithSameEC(otherAsSet); + } + + if (other is HashSet otherAsHs && AreEqualityComparersEqual(this, otherAsHs)) + { + // if this has more elements then it can't be a subset + if (_count > otherAsHs.Count) + { + return false; + } + + // already checked that we're using same equality comparer. simply check that + // each element in this is contained in other. + return IsSubsetOfHashSetWithSameEC(otherAsHs); + } + + ElementCount result = CheckUniqueAndUnfoundElements(other, false); + return (result.uniqueCount == _count && result.unfoundCount >= 0); + } + + /// + /// Checks if this is a subset of other. + /// + /// + /// true if this is a subset of other; false if not + public bool IsSubsetOf(T[] other) => IsSubsetOf((ReadOnlySpan)other); + + /// + /// Checks if this is a subset of other. + /// + /// + /// true if this is a subset of other; false if not + public bool IsSubsetOf(ReadOnlySpan other) + { + // The empty set is a subset of any set + if (_count == 0) + { + return true; + } + + ElementCount result = CheckUniqueAndUnfoundElements(other, false); + return (result.uniqueCount == _count && result.unfoundCount >= 0); + } + + /// + /// Checks if this is a proper subset of other (i.e. strictly contained in) + /// + /// + /// Implementation Notes: + /// The following properties are used up-front to avoid element-wise checks: + /// 1. If this is the empty set, then it's a proper subset of a set that contains at least + /// one element, but it's not a proper subset of the empty set. + /// 2. If other has unique elements according to this equality comparer, and this has >= + /// the number of elements in other, then this can't be a proper subset. + /// + /// Furthermore, if other is a hashset using the same equality comparer, we can use a + /// faster element-wise check. + /// + /// + /// true if this is a proper subset of other; false if not + public bool IsProperSubsetOf(IEnumerable other) + { + if (other == null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.other); + } + + // no set is a proper subset of itself. + if (other == this) + { + return false; + } + + if (other is ICollection otherAsCollection) + { + // no set is a proper subset of an empty set + if (otherAsCollection.Count == 0) + { + return false; + } + + // the empty set is a proper subset of anything but the empty set + if (_count == 0) + { + return otherAsCollection.Count > 0; + } + // faster if other is a hashset (and we're using same equality comparer) + if (other is PooledSet otherAsSet && AreEqualityComparersEqual(this, otherAsSet)) + { + if (_count >= otherAsSet.Count) + { + return false; + } + // this has strictly less than number of items in other, so the following + // check suffices for proper subset. + return IsSubsetOfHashSetWithSameEC(otherAsSet); + } + if (other is HashSet otherAsHs && AreEqualityComparersEqual(this, otherAsHs)) + { + // if this has more elements then it can't be a subset + if (_count > otherAsHs.Count) + { + return false; + } + + // already checked that we're using same equality comparer. simply check that + // each element in this is contained in other. + return IsSubsetOfHashSetWithSameEC(otherAsHs); + } + } + + ElementCount result = CheckUniqueAndUnfoundElements(other, false); + return (result.uniqueCount == _count && result.unfoundCount > 0); + } + + /// + /// Checks if this is a proper subset of other (i.e. strictly contained in) + /// + /// + /// Implementation Notes: + /// The following properties are used up-front to avoid element-wise checks: + /// 1. If this is the empty set, then it's a proper subset of a set that contains at least + /// one element, but it's not a proper subset of the empty set. + /// + /// + /// true if this is a proper subset of other; false if not + public bool IsProperSubsetOf(T[] other) => IsProperSubsetOf((ReadOnlySpan)other); + + /// + /// Checks if this is a proper subset of other (i.e. strictly contained in) + /// + /// + /// Implementation Notes: + /// The following properties are used up-front to avoid element-wise checks: + /// 1. If this is the empty set, then it's a proper subset of a set that contains at least + /// one element, but it's not a proper subset of the empty set. + /// + /// + /// true if this is a proper subset of other; false if not + public bool IsProperSubsetOf(ReadOnlySpan other) + { + // no set is a proper subset of an empty set + if (other.Length == 0) + { + return false; + } + + // the empty set is a proper subset of anything but the empty set + if (_count == 0) + { + return other.Length > 0; + } + + ElementCount result = CheckUniqueAndUnfoundElements(other, false); + return (result.uniqueCount == _count && result.unfoundCount > 0); + } + + /// + /// Checks if this is a superset of other + /// + /// + /// Implementation Notes: + /// The following properties are used up-front to avoid element-wise checks: + /// 1. If other has no elements (it's the empty set), then this is a superset, even if this + /// is also the empty set. + /// 2. If other has unique elements according to this equality comparer, and this has less + /// than the number of elements in other, then this can't be a superset + /// + /// + /// true if this is a superset of other; false if not + public bool IsSupersetOf(IEnumerable other) + { + if (other == null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.other); + } + + // a set is always a superset of itself + if (other == this) + { + return true; + } + + // try to fall out early based on counts + if (other is ICollection otherAsCollection) + { + // if other is the empty set then this is a superset + if (otherAsCollection.Count == 0) + { + return true; + } + // try to compare based on counts alone if other is a hashset with + // same equality comparer + if (other is PooledSet otherAsSet && AreEqualityComparersEqual(this, otherAsSet)) + { + if (otherAsSet.Count > _count) + { + return false; + } + } + + if (other is HashSet otherAsHs && AreEqualityComparersEqual(this, otherAsHs)) + { + if (otherAsHs.Count > _count) + { + return false; + } + } + } + + return ContainsAllElements(other); + } + + /// + /// Checks if this is a superset of other + /// + /// + /// Implementation Notes: + /// The following properties are used up-front to avoid element-wise checks: + /// 1. If other has no elements (it's the empty set), then this is a superset, even if this + /// is also the empty set. + /// + /// + /// true if this is a superset of other; false if not + public bool IsSupersetOf(T[] other) => IsSupersetOf((ReadOnlySpan)other); + + /// + /// Checks if this is a superset of other + /// + /// + /// Implementation Notes: + /// The following properties are used up-front to avoid element-wise checks: + /// 1. If other has no elements (it's the empty set), then this is a superset, even if this + /// is also the empty set. + /// + /// + /// true if this is a superset of other; false if not + public bool IsSupersetOf(ReadOnlySpan other) + { + // if other is the empty set then this is a superset + if (other.Length == 0) + { + return true; + } + + return ContainsAllElements(other); + } + + /// + /// Checks if this is a proper superset of other (i.e. other strictly contained in this) + /// + /// + /// Implementation Notes: + /// This is slightly more complicated than IsSupersetOf because we have to keep track if there + /// was at least one element not contained in other. + /// + /// The following properties are used up-front to avoid element-wise checks: + /// 1. If this is the empty set, then it can't be a proper superset of any set, even if + /// other is the empty set. + /// 2. If other is an empty set and this contains at least 1 element, then this is a proper + /// superset. + /// 3. If other has unique elements according to this equality comparer, and other's count + /// is greater than or equal to this count, then this can't be a proper superset + /// + /// Furthermore, if other has unique elements according to this equality comparer, we can + /// use a faster element-wise check. + /// + /// + /// true if this is a proper superset of other; false if not + public bool IsProperSupersetOf(IEnumerable other) + { + if (other == null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.other); + } + + // the empty set isn't a proper superset of any set. + if (_count == 0) + { + return false; + } + + // a set is never a strict superset of itself + if (other == this) + { + return false; + } + + if (other is ICollection otherAsCollection) + { + // if other is the empty set then this is a superset + if (otherAsCollection.Count == 0) + { + // note that this has at least one element, based on above check + return true; + } + // faster if other is a hashset with the same equality comparer + if (other is PooledSet otherAsSet && AreEqualityComparersEqual(this, otherAsSet)) + { + if (otherAsSet.Count >= _count) + { + return false; + } + // now perform element check + return ContainsAllElements(otherAsSet); + } + + if (other is HashSet otherAsHs && AreEqualityComparersEqual(this, otherAsHs)) + { + if (otherAsHs.Count >= _count) + { + return false; + } + // now perform element check + return ContainsAllElements(otherAsHs); + } + } + // couldn't fall out in the above cases; do it the long way + ElementCount result = CheckUniqueAndUnfoundElements(other, true); + return (result.uniqueCount < _count && result.unfoundCount == 0); + } + + /// + /// Checks if this is a proper superset of other (i.e. other strictly contained in this) + /// + /// + /// Implementation Notes: + /// This is slightly more complicated than IsSupersetOf because we have to keep track if there + /// was at least one element not contained in other. + /// + /// The following properties are used up-front to avoid element-wise checks: + /// 1. If this is the empty set, then it can't be a proper superset of any set, even if + /// other is the empty set. + /// 2. If other is an empty set and this contains at least 1 element, then this is a proper + /// superset. + /// + /// + /// true if this is a proper superset of other; false if not + public bool IsProperSupersetOf(T[] other) => IsProperSupersetOf((ReadOnlySpan)other); + + /// + /// Checks if this is a proper superset of other (i.e. other strictly contained in this) + /// + /// + /// Implementation Notes: + /// This is slightly more complicated than IsSupersetOf because we have to keep track if there + /// was at least one element not contained in other. + /// + /// The following properties are used up-front to avoid element-wise checks: + /// 1. If this is the empty set, then it can't be a proper superset of any set, even if + /// other is the empty set. + /// 2. If other is an empty set and this contains at least 1 element, then this is a proper + /// superset. + /// + /// + /// true if this is a proper superset of other; false if not + public bool IsProperSupersetOf(ReadOnlySpan other) + { + // the empty set isn't a proper superset of any set. + if (_count == 0) + { + return false; + } + + if (other.Length == 0) + { + // note that this has at least one element, based on above check + return true; + } + + // couldn't fall out in the above cases; do it the long way + ElementCount result = CheckUniqueAndUnfoundElements(other, true); + return (result.uniqueCount < _count && result.unfoundCount == 0); + } + + /// + /// Checks if this set overlaps other (i.e. they share at least one item) + /// + /// + /// true if these have at least one common element; false if disjoint + public bool Overlaps(IEnumerable other) + { + if (other == null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.other); + } + + if (_count == 0) + { + return false; + } + + // set overlaps itself + if (other == this) + { + return true; + } + + foreach (T element in other) + { + if (Contains(element)) + { + return true; + } + } + return false; + } + + /// + /// Checks if this set overlaps other (i.e. they share at least one item) + /// + /// + /// true if these have at least one common element; false if disjoint + public bool Overlaps(T[] other) => Overlaps((ReadOnlySpan)other); + + /// + /// Checks if this set overlaps other (i.e. they share at least one item) + /// + /// + /// true if these have at least one common element; false if disjoint + public bool Overlaps(ReadOnlySpan other) + { + if (_count == 0) + { + return false; + } + + for (int i = 0, len = other.Length; i < len; i++) + { + if (Contains(other[i])) + { + return true; + } + } + return false; + } + + /// + /// Checks if this and other contain the same elements. This is set equality: + /// duplicates and order are ignored + /// + /// + /// + public bool SetEquals(IEnumerable other) + { + if (other == null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.other); + } + + // a set is equal to itself + if (other == this) + { + return true; + } + + // faster if other is a hashset and we're using same equality comparer + if (other is PooledSet otherAsSet && AreEqualityComparersEqual(this, otherAsSet)) + { + // attempt to return early: since both contain unique elements, if they have + // different counts, then they can't be equal + if (_count != otherAsSet.Count) + { + return false; + } + + // already confirmed that the sets have the same number of distinct elements, so if + // one is a superset of the other then they must be equal + return ContainsAllElements(otherAsSet); + } + if (other is HashSet otherAsHs && AreEqualityComparersEqual(this, otherAsHs)) + { + // attempt to return early: since both contain unique elements, if they have + // different counts, then they can't be equal + if (_count != otherAsHs.Count) + { + return false; + } + + // already confirmed that the sets have the same number of distinct elements, so if + // one is a superset of the other then they must be equal + return ContainsAllElements(otherAsHs); + } + else + { + if (other is ICollection otherAsCollection) + { + // if this count is 0 but other contains at least one element, they can't be equal + if (_count == 0 && otherAsCollection.Count > 0) + { + return false; + } + } + ElementCount result = CheckUniqueAndUnfoundElements(other, true); + return (result.uniqueCount == _count && result.unfoundCount == 0); + } + } + + /// + /// Checks if this and other contain the same elements. This is set equality: + /// duplicates and order are ignored + /// + /// + /// + public bool SetEquals(T[] other) => SetEquals((ReadOnlySpan)other); + + /// + /// Checks if this and other contain the same elements. This is set equality: + /// duplicates and order are ignored + /// + /// + /// + public bool SetEquals(ReadOnlySpan other) + { + // if this count is 0 but other contains at least one element, they can't be equal + if (_count == 0 && other.Length > 0) + { + return false; + } + + ElementCount result = CheckUniqueAndUnfoundElements(other, true); + return (result.uniqueCount == _count && result.unfoundCount == 0); + } + + /// + /// Copies the set to the given array. + /// + public void CopyTo(T[] array) => CopyTo(array, 0, _count); + + /// + /// Copies items of the set to the given array, starting + /// at in the destination array. + /// + public void CopyTo(T[] array, int arrayIndex, int count) + { + if (array == null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.array); + } + + // check array index valid index into array + if (arrayIndex < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.arrayIndex, ExceptionResource.ArgumentOutOfRange_NeedNonNegNum); + } + + // also throw if count less than 0 + if (count < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.count, ExceptionResource.ArgumentOutOfRange_NeedNonNegNum); + } + + // will array, starting at arrayIndex, be able to hold elements? Note: not + // checking arrayIndex >= array.Length (consistency with list of allowing + // count of 0; subsequent check takes care of the rest) + if (arrayIndex > array.Length || count > array.Length - arrayIndex) + { + ThrowHelper.ThrowArgumentException(ExceptionResource.Arg_ArrayPlusOffTooSmall); + } + + int numCopied = 0; + for (int i = 0; i < _lastIndex && numCopied < count; i++) + { + if (_slots[i].hashCode >= 0) + { + array[arrayIndex + numCopied] = _slots[i].value; + numCopied++; + } + } + } + + /// + /// Copies the set to the given span. + /// + public void CopyTo(Span span) => CopyTo(span, _count); + + /// + /// Copies items from the set to the given span. + /// + public void CopyTo(Span span, int count) + { + if (span.Length < _count || span.Length < count) + { + ThrowHelper.ThrowArgumentException_DestinationTooShort(); + } + + int numCopied = 0; + for (int i = 0; i < _lastIndex && numCopied < count; i++) + { + if (_slots[i].hashCode >= 0) + { + span[numCopied] = _slots[i].value; + numCopied++; + } + } + } + + /// + /// Remove elements that match specified predicate. Returns the number of elements removed + /// + /// + /// + public int RemoveWhere(Func match) + { + if (match == null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match); + } + + int numRemoved = 0; + for (int i = 0; i < _lastIndex; i++) + { + if (_slots[i].hashCode >= 0) + { + // cache value in case delegate removes it + T value = _slots[i].value; + if (match(value)) + { + // check again that remove actually removed it + if (Remove(value)) + { + numRemoved++; + } + } + } + } + return numRemoved; + } + + /// + /// Gets the IEqualityComparer that is used to determine equality of keys for + /// the HashSet. + /// + public IEqualityComparer Comparer => _comparer; + + /// + /// Ensures that the hash set can hold up to 'capacity' entries without any further expansion of its backing storage. + /// + public int EnsureCapacity(int capacity) + { + if (capacity < 0) + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.capacity, ExceptionResource.ArgumentOutOfRange_NeedNonNegNum); + int currentCapacity = _slots == null ? 0 : _size; + if (currentCapacity >= capacity) + return currentCapacity; + if (_buckets == null) + return Initialize(capacity); + + int newSize = HashHelpers.GetPrime(capacity); + SetCapacity(newSize); + return newSize; + } + + /// + /// Sets the capacity of this list to the size of the list (rounded up to nearest prime), + /// unless count is 0, in which case we release references. + /// + /// This method can be used to minimize a list's memory overhead once it is known that no + /// new elements will be added to the list. To completely clear a list and release all + /// memory referenced by the list, execute the following statements: + /// + /// list.Clear(); + /// list.TrimExcess(); + /// + public void TrimExcess() + { + Debug.Assert(_count >= 0, "_count is negative"); + + if (_count == 0) + { + // if count is zero, clear references + ReturnArrays(); + _version++; + } + else + { + Debug.Assert(_buckets != null, "_buckets was null but _count > 0"); + + // similar to IncreaseCapacity but moves down elements in case add/remove/etc + // caused fragmentation + int newSize = HashHelpers.GetPrime(_count); + Slot[] newSlots = s_slotPool.Rent(newSize); + int[] newBuckets = s_bucketPool.Rent(newSize); + + if (newSlots.Length >= _slots.Length || newBuckets.Length >= _buckets.Length) + { + // ArrayPool treats "newSize" as a minimum - if it gives us arrays that are as-big or bigger + // that what we already have, we're not really trimming any excess and may as well quit. + // We can't manually create exact-sized arrays unless we track that we did and avoid returning + // them to the ArrayPool, as that would throw an exception. + s_slotPool.Return(newSlots); + s_bucketPool.Return(newBuckets); + return; + } + + Array.Clear(newBuckets, 0, newBuckets.Length); + + // move down slots and rehash at the same time. newIndex keeps track of current + // position in newSlots array + int newIndex = 0; + for (int i = 0; i < _lastIndex; i++) + { + if (_slots[i].hashCode >= 0) + { + newSlots[newIndex] = _slots[i]; + + // rehash + int bucket = newSlots[newIndex].hashCode % newSize; + newSlots[newIndex].next = newBuckets[bucket] - 1; + newBuckets[bucket] = newIndex + 1; + + newIndex++; + } + } + + Debug.Assert(newSize <= _size, "capacity increased after TrimExcess"); + + _lastIndex = newIndex; + ReturnArrays(); + _slots = newSlots; + _buckets = newBuckets; + _size = newSize; + _freeList = -1; + _version++; + } + } + + #endregion + + #region Helper methods + + /// + /// Used for deep equality of HashSet testing + /// + public static IEqualityComparer> CreateSetComparer() + => new PooledSetEqualityComparer(); + + /// + /// Initializes buckets and slots arrays. Uses suggested capacity by finding next prime + /// greater than or equal to capacity. + /// + /// + private int Initialize(int capacity) + { + Debug.Assert(_buckets == null, "Initialize was called but _buckets was non-null"); + + _size = HashHelpers.GetPrime(capacity); + _buckets = s_bucketPool.Rent(_size); + Array.Clear(_buckets, 0, _buckets.Length); + _slots = s_slotPool.Rent(_size); + + return _size; + } + + /// + /// Expand to new capacity. New capacity is next prime greater than or equal to suggested + /// size. This is called when the underlying array is filled. This performs no + /// defragmentation, allowing faster execution; note that this is reasonable since + /// AddIfNotPresent attempts to insert new elements in re-opened spots. + /// + private void IncreaseCapacity() + { + Debug.Assert(_buckets != null, "IncreaseCapacity called on a set with no elements"); + + int newSize = HashHelpers.ExpandPrime(_count); + if (newSize <= _count) + { + ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_HSCapacityOverflow); + } + + // Able to increase capacity; copy elements to larger array and rehash + SetCapacity(newSize); + } + + /// + /// Set the underlying buckets array to size newSize and rehash. Note that newSize + /// *must* be a prime. It is very likely that you want to call IncreaseCapacity() + /// instead of this method. + /// + private void SetCapacity(int newSize) + { + Debug.Assert(HashHelpers.IsPrime(newSize), "New size is not prime!"); + Debug.Assert(_buckets != null, "SetCapacity called on a set with no elements"); + + int[] newBuckets; + Slot[] newSlots; + bool replaceArrays; + + // Because ArrayPool might have given us larger arrays than we asked for, see if we can + // use the existing capacity without actually resizing. + if (_buckets?.Length >= newSize && _slots?.Length >= newSize) + { + Array.Clear(_buckets, 0, _buckets.Length); + Array.Clear(_slots, _size, newSize - _size); + newBuckets = _buckets; + newSlots = _slots; + replaceArrays = false; + } + else + { + newSlots = s_slotPool.Rent(newSize); + newBuckets = s_bucketPool.Rent(newSize); + + Array.Clear(newBuckets, 0, newBuckets.Length); + if (_slots != null) + { + Array.Copy(_slots, 0, newSlots, 0, _lastIndex); + } + replaceArrays = true; + } + + for (int i = 0; i < _lastIndex; i++) + { + int bucket = newSlots[i].hashCode % newSize; + newSlots[i].next = newBuckets[bucket] - 1; + newBuckets[bucket] = i + 1; + } + + if (replaceArrays) + { + ReturnArrays(); + _slots = newSlots; + _buckets = newBuckets; + } + + _size = newSize; + } + + private void ReturnArrays() + { + if (_slots?.Length > 0) + { + try + { + s_slotPool.Return(_slots, clearArray: _clearOnFree); + } + catch (ArgumentException) + { + // oh well, the array pool didn't like our array + } + } + + if (_buckets?.Length > 0) + { + try + { + s_bucketPool.Return(_buckets); + } + catch (ArgumentException) + { + // shucks + } + } + + _slots = null; + _buckets = null; + } + + private static bool ShouldClear(ClearMode mode) + { +#if NETCOREAPP2_1 + return mode == ClearMode.Always + || (mode == ClearMode.Auto && RuntimeHelpers.IsReferenceOrContainsReferences()); +#else + return mode != ClearMode.Never; +#endif + } + + /// + /// Adds value to HashSet if not contained already + /// Returns true if added and false if already present + /// + /// value to find + /// + private bool AddIfNotPresent(T value) + { + if (_buckets == null) + { + Initialize(0); + } + + int hashCode = InternalGetHashCode(value); + int bucket = hashCode % _size; + int collisionCount = 0; + Slot[] slots = _slots; + for (int i = _buckets[bucket] - 1; i >= 0; i = slots[i].next) + { + if (slots[i].hashCode == hashCode && _comparer.Equals(slots[i].value, value)) + { + return false; + } + + if (collisionCount >= _size) + { + // The chain of entries forms a loop, which means a concurrent update has happened. + ThrowHelper.ThrowInvalidOperationException_ConcurrentOperationsNotSupported(); + } + collisionCount++; + } + + int index; + if (_freeList >= 0) + { + index = _freeList; + _freeList = slots[index].next; + } + else + { + if (_lastIndex == _size) + { + IncreaseCapacity(); + // this will change during resize + slots = _slots; + bucket = hashCode % _size; + } + index = _lastIndex; + _lastIndex++; + } + slots[index].hashCode = hashCode; + slots[index].value = value; + slots[index].next = _buckets[bucket] - 1; + _buckets[bucket] = index + 1; + _count++; + _version++; + + return true; + } + + // Add value at known index with known hash code. Used only + // when constructing from another HashSet. + private void AddValue(int index, int hashCode, T value) + { + int bucket = hashCode % _size; + +#if DEBUG + Debug.Assert(InternalGetHashCode(value) == hashCode); + for (int i = _buckets[bucket] - 1; i >= 0; i = _slots[i].next) + { + Debug.Assert(!_comparer.Equals(_slots[i].value, value)); + } +#endif + + Debug.Assert(_freeList == -1); + _slots[index].hashCode = hashCode; + _slots[index].value = value; + _slots[index].next = _buckets[bucket] - 1; + _buckets[bucket] = index + 1; + } + + /// + /// Checks if this contains of other's elements. Iterates over other's elements and + /// returns false as soon as it finds an element in other that's not in this. + /// Used by SupersetOf, ProperSupersetOf, and SetEquals. + /// + /// + /// + private bool ContainsAllElements(IEnumerable other) + { + foreach (T element in other) + { + if (!Contains(element)) + { + return false; + } + } + return true; + } + + /// + /// Checks if this contains of other's elements. Iterates over other's elements and + /// returns false as soon as it finds an element in other that's not in this. + /// Used by SupersetOf, ProperSupersetOf, and SetEquals. + /// + /// + /// + private bool ContainsAllElements(ReadOnlySpan other) + { + foreach (T element in other) + { + if (!Contains(element)) + { + return false; + } + } + return true; + } + + /// + /// Implementation Notes: + /// If other is a hashset and is using same equality comparer, then checking subset is + /// faster. Simply check that each element in this is in other. + /// + /// Note: if other doesn't use same equality comparer, then Contains check is invalid, + /// which is why callers must take are of this. + /// + /// If callers are concerned about whether this is a proper subset, they take care of that. + /// + /// + /// + /// + private bool IsSubsetOfHashSetWithSameEC(PooledSet other) + { + foreach (T item in this) + { + if (!other.Contains(item)) + { + return false; + } + } + return true; + } + + /// + /// Implementation Notes: + /// If other is a hashset and is using same equality comparer, then checking subset is + /// faster. Simply check that each element in this is in other. + /// + /// Note: if other doesn't use same equality comparer, then Contains check is invalid, + /// which is why callers must take are of this. + /// + /// If callers are concerned about whether this is a proper subset, they take care of that. + /// + /// + /// + /// + private bool IsSubsetOfHashSetWithSameEC(HashSet other) + { + foreach (T item in this) + { + if (!other.Contains(item)) + { + return false; + } + } + return true; + } + + /// + /// If other is a hashset that uses same equality comparer, intersect is much faster + /// because we can use other's Contains + /// + /// + private void IntersectWithHashSetWithSameEC(PooledSet other) + { + for (int i = 0; i < _lastIndex; i++) + { + if (_slots[i].hashCode >= 0) + { + T item = _slots[i].value; + if (!other.Contains(item)) + { + Remove(item); + } + } + } + } + + /// + /// If other is a hashset that uses same equality comparer, intersect is much faster + /// because we can use other's Contains + /// + /// + private void IntersectWithHashSetWithSameEC(HashSet other) + { + for (int i = 0; i < _lastIndex; i++) + { + if (_slots[i].hashCode >= 0) + { + T item = _slots[i].value; + if (!other.Contains(item)) + { + Remove(item); + } + } + } + } + + /// + /// Iterate over other. If contained in this, mark an element in bit array corresponding to + /// its position in _slots. If anything is unmarked (in bit array), remove it. + /// + /// This attempts to allocate on the stack, if below StackAllocThreshold. + /// + /// + private void IntersectWithEnumerable(IEnumerable other) + { + Debug.Assert(_buckets != null, "_buckets shouldn't be null; callers should check first"); + + // keep track of current last index; don't want to move past the end of our bit array + // (could happen if another thread is modifying the collection) + int originalLastIndex = _lastIndex; + int intArrayLength = BitHelper.ToIntArrayLength(originalLastIndex); + + Span span = stackalloc int[StackAllocThreshold]; + BitHelper bitHelper = intArrayLength <= StackAllocThreshold + ? new BitHelper(span.Slice(0, intArrayLength), clear: true) + : new BitHelper(new int[intArrayLength], clear: false); + + // mark if contains: find index of in slots array and mark corresponding element in bit array + foreach (T item in other) + { + int index = InternalIndexOf(item); + if (index >= 0) + { + bitHelper.MarkBit(index); + } + } + + // if anything unmarked, remove it. + for (int i = bitHelper.FindFirstUnmarked(); (uint)i < (uint)originalLastIndex; i = bitHelper.FindFirstUnmarked(i + 1)) + { + if (_slots[i].hashCode >= 0) + { + Remove(_slots[i].value); + } + } + } + + /// + /// Iterate over other. If contained in this, mark an element in bit array corresponding to + /// its position in _slots. If anything is unmarked (in bit array), remove it. + /// + /// This attempts to allocate on the stack, if below StackAllocThreshold. + /// + /// + private void IntersectWithSpan(ReadOnlySpan other) + { + Debug.Assert(_buckets != null, "_buckets shouldn't be null; callers should check first"); + + // keep track of current last index; don't want to move past the end of our bit array + // (could happen if another thread is modifying the collection) + int originalLastIndex = _lastIndex; + int intArrayLength = BitHelper.ToIntArrayLength(originalLastIndex); + + Span span = stackalloc int[StackAllocThreshold]; + BitHelper bitHelper = intArrayLength <= StackAllocThreshold + ? new BitHelper(span.Slice(0, intArrayLength), clear: true) + : new BitHelper(new int[intArrayLength], clear: false); + + // mark if contains: find index of in slots array and mark corresponding element in bit array + for (int i = 0, len = other.Length; i < len; i++) + { + int index = InternalIndexOf(other[i]); + if (index >= 0) + { + bitHelper.MarkBit(index); + } + } + + // if anything unmarked, remove it. + for (int i = bitHelper.FindFirstUnmarked(); (uint)i < (uint)originalLastIndex; i = bitHelper.FindFirstUnmarked(i + 1)) + { + if (_slots[i].hashCode >= 0) + { + Remove(_slots[i].value); + } + } + } + + /// + /// Used internally by set operations which have to rely on bit array marking. This is like + /// Contains but returns index in slots array. + /// + /// + /// + private int InternalIndexOf(T item) + { + Debug.Assert(_buckets != null, "_buckets was null; callers should check first"); + + int collisionCount = 0; + int hashCode = InternalGetHashCode(item); + Slot[] slots = _slots; + for (int i = _buckets[hashCode % _size] - 1; i >= 0; i = slots[i].next) + { + if ((slots[i].hashCode) == hashCode && _comparer.Equals(slots[i].value, item)) + { + return i; + } + + if (collisionCount >= _size) + { + // The chain of entries forms a loop, which means a concurrent update has happened. + ThrowHelper.ThrowInvalidOperationException_ConcurrentOperationsNotSupported(); + } + collisionCount++; + } + // wasn't found + return -1; + } + + /// + /// if other is a set, we can assume it doesn't have duplicate elements, so use this + /// technique: if can't remove, then it wasn't present in this set, so add. + /// + /// As with other methods, callers take care of ensuring that other is a hashset using the + /// same equality comparer. + /// + /// + private void SymmetricExceptWithUniqueHashSet(PooledSet other) + { + foreach (T item in other) + { + if (!Remove(item)) + { + AddIfNotPresent(item); + } + } + } + + /// + /// if other is a set, we can assume it doesn't have duplicate elements, so use this + /// technique: if can't remove, then it wasn't present in this set, so add. + /// + /// As with other methods, callers take care of ensuring that other is a hashset using the + /// same equality comparer. + /// + /// + private void SymmetricExceptWithUniqueHashSet(HashSet other) + { + foreach (T item in other) + { + if (!Remove(item)) + { + AddIfNotPresent(item); + } + } + } + + /// + /// Implementation notes: + /// + /// Used for symmetric except when other isn't a HashSet. This is more tedious because + /// other may contain duplicates. HashSet technique could fail in these situations: + /// 1. Other has a duplicate that's not in this: HashSet technique would add then + /// remove it. + /// 2. Other has a duplicate that's in this: HashSet technique would remove then add it + /// back. + /// In general, its presence would be toggled each time it appears in other. + /// + /// This technique uses bit marking to indicate whether to add/remove the item. If already + /// present in collection, it will get marked for deletion. If added from other, it will + /// get marked as something not to remove. + /// + /// + /// + private void SymmetricExceptWithEnumerable(IEnumerable other) + { + int originalLastIndex = _lastIndex; + int intArrayLength = BitHelper.ToIntArrayLength(originalLastIndex); + + Span itemsToRemoveSpan = stackalloc int[StackAllocThreshold / 2]; + BitHelper itemsToRemove = intArrayLength <= StackAllocThreshold / 2 + ? new BitHelper(itemsToRemoveSpan.Slice(0, intArrayLength), clear: true) + : new BitHelper(new int[intArrayLength], clear: false); + + Span itemsAddedFromOtherSpan = stackalloc int[StackAllocThreshold / 2]; + BitHelper itemsAddedFromOther = intArrayLength <= StackAllocThreshold / 2 + ? new BitHelper(itemsAddedFromOtherSpan.Slice(0, intArrayLength), clear: true) + : new BitHelper(new int[intArrayLength], clear: false); + + foreach (T item in other) + { + bool added = AddOrGetLocation(item, out int location); + if (added) + { + // wasn't already present in collection; flag it as something not to remove + // *NOTE* if location is out of range, we should ignore. BitHelper will + // detect that it's out of bounds and not try to mark it. But it's + // expected that location could be out of bounds because adding the item + // will increase _lastIndex as soon as all the free spots are filled. + itemsAddedFromOther.MarkBit(location); + } + else + { + // already there...if not added from other, mark for remove. + // *NOTE* Even though BitHelper will check that location is in range, we want + // to check here. There's no point in checking items beyond originalLastIndex + // because they could not have been in the original collection + if (location < originalLastIndex && !itemsAddedFromOther.IsMarked(location)) + { + itemsToRemove.MarkBit(location); + } + } + } + + // if anything marked, remove it + for (int i = itemsToRemove.FindFirstMarked(); (uint)i < (uint)originalLastIndex; i = itemsToRemove.FindFirstMarked(i + 1)) + { + if (_slots[i].hashCode >= 0) + { + Remove(_slots[i].value); + } + } + } + + /// + /// Implementation notes: + /// + /// Used for symmetric except when other isn't a HashSet. This is more tedious because + /// other may contain duplicates. HashSet technique could fail in these situations: + /// 1. Other has a duplicate that's not in this: HashSet technique would add then + /// remove it. + /// 2. Other has a duplicate that's in this: HashSet technique would remove then add it + /// back. + /// In general, its presence would be toggled each time it appears in other. + /// + /// This technique uses bit marking to indicate whether to add/remove the item. If already + /// present in collection, it will get marked for deletion. If added from other, it will + /// get marked as something not to remove. + /// + /// + /// + private void SymmetricExceptWithSpan(ReadOnlySpan other) + { + int originalLastIndex = _lastIndex; + int intArrayLength = BitHelper.ToIntArrayLength(originalLastIndex); + + Span itemsToRemoveSpan = stackalloc int[StackAllocThreshold / 2]; + BitHelper itemsToRemove = intArrayLength <= StackAllocThreshold / 2 + ? new BitHelper(itemsToRemoveSpan.Slice(0, intArrayLength), clear: true) + : new BitHelper(new int[intArrayLength], clear: false); + + Span itemsAddedFromOtherSpan = stackalloc int[StackAllocThreshold / 2]; + BitHelper itemsAddedFromOther = intArrayLength <= StackAllocThreshold / 2 + ? new BitHelper(itemsAddedFromOtherSpan.Slice(0, intArrayLength), clear: true) + : new BitHelper(new int[intArrayLength], clear: false); + + for (int i = 0, len = other.Length; i < len; i++) + { + bool added = AddOrGetLocation(other[i], out int location); + if (added) + { + // wasn't already present in collection; flag it as something not to remove + // *NOTE* if location is out of range, we should ignore. BitHelper will + // detect that it's out of bounds and not try to mark it. But it's + // expected that location could be out of bounds because adding the item + // will increase _lastIndex as soon as all the free spots are filled. + itemsAddedFromOther.MarkBit(location); + } + else + { + // already there...if not added from other, mark for remove. + // *NOTE* Even though BitHelper will check that location is in range, we want + // to check here. There's no point in checking items beyond originalLastIndex + // because they could not have been in the original collection + if (location < originalLastIndex && !itemsAddedFromOther.IsMarked(location)) + { + itemsToRemove.MarkBit(location); + } + } + } + + // if anything marked, remove it + for (int i = itemsToRemove.FindFirstMarked(); (uint)i < (uint)originalLastIndex; i = itemsToRemove.FindFirstMarked(i + 1)) + { + if (_slots[i].hashCode >= 0) + { + Remove(_slots[i].value); + } + } + } + + /// + /// Add if not already in hashset. Returns an out param indicating index where added. This + /// is used by SymmetricExcept because it needs to know the following things: + /// - whether the item was already present in the collection or added from other + /// - where it's located (if already present, it will get marked for removal, otherwise + /// marked for keeping) + /// + /// + /// + /// + private bool AddOrGetLocation(T value, out int location) + { + Debug.Assert(_buckets != null, "_buckets is null, callers should have checked"); + + int hashCode = InternalGetHashCode(value); + int bucket = hashCode % _size; + int collisionCount = 0; + Slot[] slots = _slots; + for (int i = _buckets[bucket] - 1; i >= 0; i = slots[i].next) + { + if (slots[i].hashCode == hashCode && _comparer.Equals(slots[i].value, value)) + { + location = i; + return false; //already present + } + + if (collisionCount >= _size) + { + // The chain of entries forms a loop, which means a concurrent update has happened. + ThrowHelper.ThrowInvalidOperationException_ConcurrentOperationsNotSupported(); + } + collisionCount++; + } + int index; + if (_freeList >= 0) + { + index = _freeList; + _freeList = slots[index].next; + } + else + { + if (_lastIndex == _size) + { + IncreaseCapacity(); + // this will change during resize + slots = _slots; + bucket = hashCode % _size; + } + index = _lastIndex; + _lastIndex++; + } + slots[index].hashCode = hashCode; + slots[index].value = value; + slots[index].next = _buckets[bucket] - 1; + _buckets[bucket] = index + 1; + _count++; + _version++; + location = index; + return true; + } + + /// + /// Determines counts that can be used to determine equality, subset, and superset. This + /// is only used when other is an IEnumerable and not a HashSet. If other is a HashSet + /// these properties can be checked faster without use of marking because we can assume + /// other has no duplicates. + /// + /// The following count checks are performed by callers: + /// 1. Equals: checks if unfoundCount = 0 and uniqueFoundCount = _count; i.e. everything + /// in other is in this and everything in this is in other + /// 2. Subset: checks if unfoundCount >= 0 and uniqueFoundCount = _count; i.e. other may + /// have elements not in this and everything in this is in other + /// 3. Proper subset: checks if unfoundCount > 0 and uniqueFoundCount = _count; i.e + /// other must have at least one element not in this and everything in this is in other + /// 4. Proper superset: checks if unfound count = 0 and uniqueFoundCount strictly less + /// than _count; i.e. everything in other was in this and this had at least one element + /// not contained in other. + /// + /// An earlier implementation used delegates to perform these checks rather than returning + /// an ElementCount struct; however this was changed due to the perf overhead of delegates. + /// + /// + /// Allows us to finish faster for equals and proper superset + /// because unfoundCount must be 0. + /// + private ElementCount CheckUniqueAndUnfoundElements(IEnumerable other, bool returnIfUnfound) + { + ElementCount result; + + // need special case in case this has no elements. + if (_count == 0) + { + int numElementsInOther = 0; + foreach (T item in other) + { + numElementsInOther++; + // break right away, all we want to know is whether other has 0 or 1 elements + break; + } + result.uniqueCount = 0; + result.unfoundCount = numElementsInOther; + return result; + } + + Debug.Assert((_buckets != null) && (_count > 0), "_buckets was null but count greater than 0"); + + int originalLastIndex = _lastIndex; + int intArrayLength = BitHelper.ToIntArrayLength(originalLastIndex); + + Span span = stackalloc int[StackAllocThreshold]; + BitHelper bitHelper = intArrayLength <= StackAllocThreshold + ? new BitHelper(span.Slice(0, intArrayLength), clear: true) + : new BitHelper(new int[intArrayLength], clear: false); + + // count of items in other not found in this + int unfoundCount = 0; + // count of unique items in other found in this + int uniqueFoundCount = 0; + + foreach (T item in other) + { + int index = InternalIndexOf(item); + if (index >= 0) + { + if (!bitHelper.IsMarked(index)) + { + // item hasn't been seen yet + bitHelper.MarkBit(index); + uniqueFoundCount++; + } + } + else + { + unfoundCount++; + if (returnIfUnfound) + { + break; + } + } + } + + result.uniqueCount = uniqueFoundCount; + result.unfoundCount = unfoundCount; + return result; + } + + /// + /// Determines counts that can be used to determine equality, subset, and superset. This + /// is only used when other is a Span and not a HashSet. If other is a HashSet + /// these properties can be checked faster without use of marking because we can assume + /// other has no duplicates. + /// + /// The following count checks are performed by callers: + /// 1. Equals: checks if unfoundCount = 0 and uniqueFoundCount = _count; i.e. everything + /// in other is in this and everything in this is in other + /// 2. Subset: checks if unfoundCount >= 0 and uniqueFoundCount = _count; i.e. other may + /// have elements not in this and everything in this is in other + /// 3. Proper subset: checks if unfoundCount > 0 and uniqueFoundCount = _count; i.e + /// other must have at least one element not in this and everything in this is in other + /// 4. Proper superset: checks if unfound count = 0 and uniqueFoundCount strictly less + /// than _count; i.e. everything in other was in this and this had at least one element + /// not contained in other. + /// + /// An earlier implementation used delegates to perform these checks rather than returning + /// an ElementCount struct; however this was changed due to the perf overhead of delegates. + /// + /// + /// Allows us to finish faster for equals and proper superset + /// because unfoundCount must be 0. + /// + private ElementCount CheckUniqueAndUnfoundElements(ReadOnlySpan other, bool returnIfUnfound) + { + ElementCount result; + + // need special case in case this has no elements. + if (_count == 0) + { + result.uniqueCount = 0; + result.unfoundCount = other.Length; + return result; + } + + Debug.Assert((_buckets != null) && (_count > 0), "_buckets was null but count greater than 0"); + + int originalLastIndex = _lastIndex; + int intArrayLength = BitHelper.ToIntArrayLength(originalLastIndex); + + Span span = stackalloc int[StackAllocThreshold]; + BitHelper bitHelper = intArrayLength <= StackAllocThreshold + ? new BitHelper(span.Slice(0, intArrayLength), clear: true) + : new BitHelper(new int[intArrayLength], clear: false); + + // count of items in other not found in this + int unfoundCount = 0; + // count of unique items in other found in this + int uniqueFoundCount = 0; + + for (int i = 0, len = other.Length; i < len; i++) + { + int index = InternalIndexOf(other[i]); + if (index >= 0) + { + if (!bitHelper.IsMarked(index)) + { + // item hasn't been seen yet + bitHelper.MarkBit(index); + uniqueFoundCount++; + } + } + else + { + unfoundCount++; + if (returnIfUnfound) + { + break; + } + } + } + + result.uniqueCount = uniqueFoundCount; + result.unfoundCount = unfoundCount; + return result; + } + + /// + /// Internal method used for HashSetEqualityComparer. Compares set1 and set2 according + /// to specified comparer. + /// + /// Because items are hashed according to a specific equality comparer, we have to resort + /// to n^2 search if they're using different equality comparers. + /// + /// + /// + /// + /// + internal static bool PooledSetEquals(PooledSet set1, PooledSet set2, IEqualityComparer comparer) + { + // handle null cases first + if (set1 == null) + { + return (set2 == null); + } + else if (set2 == null) + { + // set1 != null + return false; + } + + // all comparers are the same; this is faster + if (AreEqualityComparersEqual(set1, set2)) + { + if (set1.Count != set2.Count) + { + return false; + } + // suffices to check subset + foreach (T item in set2) + { + if (!set1.Contains(item)) + { + return false; + } + } + return true; + } + else + { // n^2 search because items are hashed according to their respective ECs + foreach (T set2Item in set2) + { + bool found = false; + foreach (T set1Item in set1) + { + if (comparer.Equals(set2Item, set1Item)) + { + found = true; + break; + } + } + if (!found) + { + return false; + } + } + return true; + } + } + + /// + /// Checks if equality comparers are equal. This is used for algorithms that can + /// speed up if it knows the other item has unique elements. I.e. if they're using + /// different equality comparers, then uniqueness assumption between sets break. + /// + /// + /// + private static bool AreEqualityComparersEqual(PooledSet set1, PooledSet set2) + { + return set1.Comparer.Equals(set2.Comparer); + } + + /// + /// Checks if equality comparers are equal. This is used for algorithms that can + /// speed up if it knows the other item has unique elements. I.e. if they're using + /// different equality comparers, then uniqueness assumption between sets break. + /// + /// + /// + private static bool AreEqualityComparersEqual(PooledSet set1, HashSet set2) + { + return set1.Comparer.Equals(set2.Comparer); + } + + /// + /// Workaround Comparers that throw ArgumentNullException for GetHashCode(null). + /// + /// + /// hash code + private int InternalGetHashCode(T item) + { + if (item == null) + { + return 0; + } + return _comparer.GetHashCode(item) & Lower31BitMask; + } + + /// + /// Clears all values and returns internal arrays to the ArrayPool. + /// + public void Dispose() + { + ReturnArrays(); + _size = 0; + _lastIndex = 0; + _count = 0; + _freeList = -1; + _version++; + } + + #endregion + + // used for set checking operations (using enumerables) that rely on counting + internal struct ElementCount + { + internal int uniqueCount; + internal int unfoundCount; + } + + internal struct Slot + { + internal int hashCode; // Lower 31 bits of hash code, -1 if unused + internal int next; // Index of next entry, -1 if last + internal T value; + } + + /// + /// Enumerates the PooledSet. + /// + public struct Enumerator : IEnumerator, IEnumerator + { + private readonly PooledSet _set; + private int _index; + private readonly int _version; + private T _current; + + internal Enumerator(PooledSet set) + { + _set = set; + _index = 0; + _version = set._version; + _current = default; + } + + void IDisposable.Dispose() + { + } + + /// + /// Moves to the next item in the set. + /// + public bool MoveNext() + { + if (_version != _set._version) + { + ThrowHelper.ThrowInvalidOperationException_InvalidOperation_EnumFailedVersion(); + } + + while (_index < _set._lastIndex) + { + if (_set._slots[_index].hashCode >= 0) + { + _current = _set._slots[_index].value; + _index++; + return true; + } + _index++; + } + _index = _set._lastIndex + 1; + _current = default; + return false; + } + + /// + /// Gets the current element in the set. + /// + public T Current => _current; + + object IEnumerator.Current + { + get + { + if (_index == 0 || _index == _set._lastIndex + 1) + { + ThrowHelper.ThrowInvalidOperationException_InvalidOperation_EnumOpCantHappen(); + } + return Current; + } + } + + void IEnumerator.Reset() + { + if (_version != _set._version) + { + ThrowHelper.ThrowInvalidOperationException_InvalidOperation_EnumFailedVersion(); + } + + _index = 0; + _current = default; + } + } + } +} diff --git a/OctaneEngine/Collections.Pooled/PooledSetEqualityComparer.cs b/OctaneEngine/Collections.Pooled/PooledSetEqualityComparer.cs new file mode 100644 index 0000000..92327c8 --- /dev/null +++ b/OctaneEngine/Collections.Pooled/PooledSetEqualityComparer.cs @@ -0,0 +1,63 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections; +using System.Collections.Generic; + +namespace Collections.Pooled +{ + /// + /// Equality comparer for hashsets of hashsets + /// + /// + internal sealed class PooledSetEqualityComparer : IEqualityComparer> + { + private readonly IEqualityComparer _comparer; + + public PooledSetEqualityComparer() + { + _comparer = EqualityComparer.Default; + } + + // using m_comparer to keep equals properties intact; don't want to choose one of the comparers + public bool Equals(PooledSet x, PooledSet y) + { + return PooledSet.PooledSetEquals(x, y, _comparer); + } + + public int GetHashCode(PooledSet obj) + { + int hashCode = 0; + if (obj != null) + { + foreach (T t in obj) + { + hashCode ^= (_comparer.GetHashCode(t) & 0x7FFFFFFF); + } + } // else returns hashcode of 0 for null hashsets + return hashCode; + } + + // Equals method for the comparer itself. + public override bool Equals(object obj) + { + if (obj is PooledSetEqualityComparer comparer) + { + return (_comparer == comparer._comparer); + } + else if (obj is IEqualityComparer ieq) + { + return _comparer == ieq; + } + return false; + } + + public override int GetHashCode() + { + return _comparer.GetHashCode(); + } + } +} + diff --git a/OctaneEngine/Collections.Pooled/PooledStack.cs b/OctaneEngine/Collections.Pooled/PooledStack.cs new file mode 100644 index 0000000..d1a3a6a --- /dev/null +++ b/OctaneEngine/Collections.Pooled/PooledStack.cs @@ -0,0 +1,694 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +/*============================================================================= +** +** +** Purpose: An array implementation of a generic stack. +** +** +=============================================================================*/ + +using System; +using System.Buffers; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Runtime.Serialization; +using System.Threading; + +namespace Collections.Pooled +{ + /// + /// A simple stack of objects. Internally it is implemented as an array, + /// so Push can be O(n). Pop is O(1). + /// + [DebuggerTypeProxy(typeof(StackDebugView<>))] + [DebuggerDisplay("Count = {Count}")] + [Serializable] + public class PooledStack : IEnumerable, ICollection, IReadOnlyCollection, IDisposable, IDeserializationCallback + { + [NonSerialized] + private ArrayPool _pool; + [NonSerialized] + private object _syncRoot; + + private T[] _array; // Storage for stack elements. Do not rename (binary serialization) + private int _size; // Number of items in the stack. Do not rename (binary serialization) + private int _version; // Used to keep enumerator in sync w/ collection. Do not rename (binary serialization) + private readonly bool _clearOnFree; + + private const int DefaultCapacity = 4; + + #region Constructors + + /// + /// Create a stack with the default initial capacity. + /// + public PooledStack() : this(ClearMode.Auto, ArrayPool.Shared) { } + + /// + /// Create a stack with the default initial capacity. + /// + public PooledStack(ClearMode clearMode) : this(clearMode, ArrayPool.Shared) { } + + /// + /// Create a stack with the default initial capacity. + /// + public PooledStack(ArrayPool customPool) : this(ClearMode.Auto, customPool) { } + + /// + /// Create a stack with the default initial capacity and a custom ArrayPool. + /// + public PooledStack(ClearMode clearMode, ArrayPool customPool) + { + _pool = customPool ?? ArrayPool.Shared; + _array = Array.Empty(); + _clearOnFree = ShouldClear(clearMode); + } + + /// + /// Create a stack with a specific initial capacity. The initial capacity + /// must be a non-negative number. + /// + public PooledStack(int capacity) : this(capacity, ClearMode.Auto, ArrayPool.Shared) { } + + /// + /// Create a stack with a specific initial capacity. The initial capacity + /// must be a non-negative number. + /// + public PooledStack(int capacity, ClearMode clearMode) : this(capacity, clearMode, ArrayPool.Shared) { } + + /// + /// Create a stack with a specific initial capacity. The initial capacity + /// must be a non-negative number. + /// + public PooledStack(int capacity, ArrayPool customPool) : this(capacity, ClearMode.Auto, customPool) { } + + /// + /// Create a stack with a specific initial capacity. The initial capacity + /// must be a non-negative number. + /// + public PooledStack(int capacity, ClearMode clearMode, ArrayPool customPool) + { + if (capacity < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.capacity, + ExceptionResource.ArgumentOutOfRange_NeedNonNegNum); + } + _pool = customPool ?? ArrayPool.Shared; + _array = _pool.Rent(capacity); + _clearOnFree = ShouldClear(clearMode); + } + + /// + /// Fills a Stack with the contents of a particular collection. The items are + /// pushed onto the stack in the same order they are read by the enumerator. + /// + public PooledStack(IEnumerable enumerable) : this(enumerable, ClearMode.Auto, ArrayPool.Shared) { } + + /// + /// Fills a Stack with the contents of a particular collection. The items are + /// pushed onto the stack in the same order they are read by the enumerator. + /// + public PooledStack(IEnumerable enumerable, ClearMode clearMode) : this(enumerable, clearMode, ArrayPool.Shared) { } + + /// + /// Fills a Stack with the contents of a particular collection. The items are + /// pushed onto the stack in the same order they are read by the enumerator. + /// + public PooledStack(IEnumerable enumerable, ArrayPool customPool) : this(enumerable, ClearMode.Auto, customPool) { } + + /// + /// Fills a Stack with the contents of a particular collection. The items are + /// pushed onto the stack in the same order they are read by the enumerator. + /// + public PooledStack(IEnumerable enumerable, ClearMode clearMode, ArrayPool customPool) + { + _pool = customPool ?? ArrayPool.Shared; + _clearOnFree = ShouldClear(clearMode); + + switch (enumerable) + { + case null: + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.enumerable); + break; + + case ICollection collection: + if (collection.Count == 0) + { + _array = Array.Empty(); + } + else + { + _array = _pool.Rent(collection.Count); + collection.CopyTo(_array, 0); + _size = collection.Count; + } + break; + + default: + using (var list = new PooledList(enumerable)) + { + _array = _pool.Rent(list.Count); + list.Span.CopyTo(_array); + _size = list.Count; + } + break; + } + } + + /// + /// Fills a Stack with the contents of a particular collection. The items are + /// pushed onto the stack in the same order they are read by the enumerator. + /// + public PooledStack(T[] array) : this(array.AsSpan(), ClearMode.Auto, ArrayPool.Shared) { } + + /// + /// Fills a Stack with the contents of a particular collection. The items are + /// pushed onto the stack in the same order they are read by the enumerator. + /// + public PooledStack(T[] array, ClearMode clearMode) : this(array.AsSpan(), clearMode, ArrayPool.Shared) { } + + /// + /// Fills a Stack with the contents of a particular collection. The items are + /// pushed onto the stack in the same order they are read by the enumerator. + /// + public PooledStack(T[] array, ArrayPool customPool) : this(array.AsSpan(), ClearMode.Auto, customPool) { } + + /// + /// Fills a Stack with the contents of a particular collection. The items are + /// pushed onto the stack in the same order they are read by the enumerator. + /// + public PooledStack(T[] array, ClearMode clearMode, ArrayPool customPool) : this(array.AsSpan(), clearMode, customPool) { } + + /// + /// Fills a Stack with the contents of a particular collection. The items are + /// pushed onto the stack in the same order they are read by the enumerator. + /// + public PooledStack(ReadOnlySpan span) : this(span, ClearMode.Auto, ArrayPool.Shared) { } + + /// + /// Fills a Stack with the contents of a particular collection. The items are + /// pushed onto the stack in the same order they are read by the enumerator. + /// + public PooledStack(ReadOnlySpan span, ClearMode clearMode) : this(span, clearMode, ArrayPool.Shared) { } + + /// + /// Fills a Stack with the contents of a particular collection. The items are + /// pushed onto the stack in the same order they are read by the enumerator. + /// + public PooledStack(ReadOnlySpan span, ArrayPool customPool) : this(span, ClearMode.Auto, customPool) { } + + /// + /// Fills a Stack with the contents of a particular collection. The items are + /// pushed onto the stack in the same order they are read by the enumerator. + /// + public PooledStack(ReadOnlySpan span, ClearMode clearMode, ArrayPool customPool) + { + _pool = customPool ?? ArrayPool.Shared; + _clearOnFree = ShouldClear(clearMode); + _array = _pool.Rent(span.Length); + span.CopyTo(_array); + _size = span.Length; + } + + #endregion + + /// + /// The number of items in the stack. + /// + public int Count => _size; + + /// + /// Returns the ClearMode behavior for the collection, denoting whether values are + /// cleared from internal arrays before returning them to the pool. + /// + public ClearMode ClearMode => _clearOnFree ? ClearMode.Always : ClearMode.Never; + + bool ICollection.IsSynchronized => false; + + object ICollection.SyncRoot + { + get + { + if (_syncRoot == null) + { + Interlocked.CompareExchange(ref _syncRoot, new object(), null); + } + return _syncRoot; + } + } + + /// + /// Removes all Objects from the Stack. + /// + public void Clear() + { + if (_clearOnFree) + { + Array.Clear(_array, 0, _size); // clear the elements so that the gc can reclaim the references. + } + _size = 0; + _version++; + } + + /// + /// Compares items using the default equality comparer + /// + public bool Contains(T item) + { + // PERF: Internally Array.LastIndexOf calls + // EqualityComparer.Default.LastIndexOf, which + // is specialized for different types. This + // boosts performance since instead of making a + // virtual method call each iteration of the loop, + // via EqualityComparer.Default.Equals, we + // only make one virtual call to EqualityComparer.LastIndexOf. + + return _size != 0 && Array.LastIndexOf(_array, item, _size - 1) != -1; + } + + /// + /// This method removes all items which match the predicate. + /// The complexity is O(n). + /// + public int RemoveWhere(Func match) + { + if (match == null) + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match); + + int freeIndex = 0; // the first free slot in items array + + // Find the first item which needs to be removed. + while (freeIndex < _size && !match(_array[freeIndex])) freeIndex++; + if (freeIndex >= _size) return 0; + + int current = freeIndex + 1; + while (current < _size) + { + // Find the first item which needs to be kept. + while (current < _size && match(_array[current])) current++; + + if (current < _size) + { + // copy item to the free slot. + _array[freeIndex++] = _array[current++]; + } + } + + if (_clearOnFree) + { + // Clear the removed elements so that the gc can reclaim the references. + Array.Clear(_array, freeIndex, _size - freeIndex); + } + + int result = _size - freeIndex; + _size = freeIndex; + _version++; + return result; + } + + // Copies the stack into an array. + public void CopyTo(T[] array, int arrayIndex) + { + if (array == null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.array); + } + + if (arrayIndex < 0 || arrayIndex > array.Length) + { + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.arrayIndex); + } + + if (array.Length - arrayIndex < _size) + { + ThrowHelper.ThrowArgumentException(ExceptionResource.Arg_ArrayPlusOffTooSmall); + } + + Debug.Assert(array != _array); + int srcIndex = 0; + int dstIndex = arrayIndex + _size; + while (srcIndex < _size) + { + array[--dstIndex] = _array[srcIndex++]; + } + } + + public void CopyTo(Span span) + { + if (span.Length < _size) + { + ThrowHelper.ThrowArgumentException_DestinationTooShort(); + } + + int srcIndex = 0; + int dstIndex = _size; + while (srcIndex < _size) + { + span[--dstIndex] = _array[srcIndex++]; + } + } + + void ICollection.CopyTo(Array array, int arrayIndex) + { + if (array == null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.array); + } + + if (array.Rank != 1) + { + ThrowHelper.ThrowArgumentException(ExceptionResource.Arg_RankMultiDimNotSupported); + } + + if (array.GetLowerBound(0) != 0) + { + ThrowHelper.ThrowArgumentException(ExceptionResource.Arg_NonZeroLowerBound, ExceptionArgument.array); + } + + if (arrayIndex < 0 || arrayIndex > array.Length) + { + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.arrayIndex); + } + + if (array.Length - arrayIndex < _size) + { + ThrowHelper.ThrowArgumentException(ExceptionResource.Argument_InvalidOffLen); + } + + try + { + Array.Copy(_array, 0, array, arrayIndex, _size); + Array.Reverse(array, arrayIndex, _size); + } + catch (ArrayTypeMismatchException) + { + ThrowHelper.ThrowArgumentException_Argument_InvalidArrayType(); + } + } + + /// + /// Returns an IEnumerator for this PooledStack. + /// + /// + public Enumerator GetEnumerator() + => new Enumerator(this); + + /// + IEnumerator IEnumerable.GetEnumerator() + => new Enumerator(this); + + IEnumerator IEnumerable.GetEnumerator() + => new Enumerator(this); + + public void TrimExcess() + { + if (_size == 0) + { + ReturnArray(replaceWith: Array.Empty()); + _version++; + return; + } + + int threshold = (int)(_array.Length * 0.9); + if (_size < threshold) + { + var newArray = _pool.Rent(_size); + if (newArray.Length < _array.Length) + { + Array.Copy(_array, newArray, _size); + ReturnArray(replaceWith: newArray); + _version++; + } + else + { + // The array from the pool wasn't any smaller than the one we already had, + // (we can only control minimum size) so return it and do nothing. + // If we create an exact-sized array not from the pool, we'll + // get an exception when returning it to the pool. + _pool.Return(newArray); + } + } + } + + /// + /// Returns the top object on the stack without removing it. If the stack + /// is empty, Peek throws an InvalidOperationException. + /// + public T Peek() + { + int size = _size - 1; + T[] array = _array; + + if ((uint)size >= (uint)array.Length) + { + ThrowForEmptyStack(); + } + + return array[size]; + } + + public bool TryPeek(out T result) + { + int size = _size - 1; + T[] array = _array; + + if ((uint)size >= (uint)array.Length) + { + result = default; + return false; + } + result = array[size]; + return true; + } + + /// + /// Pops an item from the top of the stack. If the stack is empty, Pop + /// throws an InvalidOperationException. + /// + public T Pop() + { + int size = _size - 1; + T[] array = _array; + + // if (_size == 0) is equivalent to if (size == -1), and this case + // is covered with (uint)size, thus allowing bounds check elimination + // https://github.com/dotnet/coreclr/pull/9773 + if ((uint)size >= (uint)array.Length) + { + ThrowForEmptyStack(); + } + + _version++; + _size = size; + T item = array[size]; + if (_clearOnFree) + { + array[size] = default; // Free memory quicker. + } + return item; + } + + public bool TryPop(out T result) + { + int size = _size - 1; + T[] array = _array; + + if ((uint)size >= (uint)array.Length) + { + result = default; + return false; + } + + _version++; + _size = size; + result = array[size]; + if (_clearOnFree) + { + array[size] = default; // Free memory quicker. + } + return true; + } + + /// + /// Pushes an item to the top of the stack. + /// + public void Push(T item) + { + int size = _size; + T[] array = _array; + + if ((uint)size < (uint)array.Length) + { + array[size] = item; + _version++; + _size = size + 1; + } + else + { + PushWithResize(item); + } + } + + // Non-inline from Stack.Push to improve its code quality as uncommon path + [MethodImpl(MethodImplOptions.NoInlining)] + private void PushWithResize(T item) + { + var newArray = _pool.Rent((_array.Length == 0) ? DefaultCapacity : 2 * _array.Length); + Array.Copy(_array, newArray, _size); + ReturnArray(replaceWith: newArray); + _array[_size] = item; + _version++; + _size++; + } + + /// + /// Copies the Stack to an array, in the same order Pop would return the items. + /// + public T[] ToArray() + { + if (_size == 0) + return Array.Empty(); + + T[] objArray = new T[_size]; + int i = 0; + while (i < _size) + { + objArray[i] = _array[_size - i - 1]; + i++; + } + return objArray; + } + + private void ThrowForEmptyStack() + { + Debug.Assert(_size == 0); + throw new InvalidOperationException("Stack was empty."); + } + + private void ReturnArray(T[] replaceWith = null) + { + if (_array?.Length > 0) + { + try + { + _pool.Return(_array, clearArray: _clearOnFree); + } + catch (ArgumentException) + { + // oh well, the array pool didn't like our array + } + } + + if (!(replaceWith is null)) + { + _array = replaceWith; + } + } + + private static bool ShouldClear(ClearMode mode) + { +#if NETCOREAPP2_1 + return mode == ClearMode.Always + || (mode == ClearMode.Auto && RuntimeHelpers.IsReferenceOrContainsReferences()); +#else + return mode != ClearMode.Never; +#endif + } + + public void Dispose() + { + ReturnArray(replaceWith: Array.Empty()); + _size = 0; + _version++; + } + + void IDeserializationCallback.OnDeserialization(object sender) + { + // We can't serialize array pools, so deserialized PooledStacks will + // have to use the shared pool, even if they were using a custom pool + // before serialization. + _pool = ArrayPool.Shared; + } + + [SuppressMessage("Microsoft.Performance", "CA1815:OverrideEqualsAndOperatorEqualsOnValueTypes", Justification = "not an expected scenario")] + public struct Enumerator : IEnumerator, IEnumerator + { + private readonly PooledStack _stack; + private readonly int _version; + private int _index; + private T _currentElement; + + internal Enumerator(PooledStack stack) + { + _stack = stack; + _version = stack._version; + _index = -2; + _currentElement = default; + } + + public void Dispose() + { + _index = -1; + } + + public bool MoveNext() + { + bool retval; + if (_version != _stack._version) throw new InvalidOperationException("Collection was modified during enumeration."); + if (_index == -2) + { // First call to enumerator. + _index = _stack._size - 1; + retval = (_index >= 0); + if (retval) + _currentElement = _stack._array[_index]; + return retval; + } + if (_index == -1) + { // End of enumeration. + return false; + } + + retval = (--_index >= 0); + if (retval) + _currentElement = _stack._array[_index]; + else + _currentElement = default; + return retval; + } + + public T Current + { + get + { + if (_index < 0) + ThrowEnumerationNotStartedOrEnded(); + return _currentElement; + } + } + + private void ThrowEnumerationNotStartedOrEnded() + { + Debug.Assert(_index == -1 || _index == -2); + throw new InvalidOperationException(_index == -2 ? "Enumeration was not started." : "Enumeration has ended."); + } + + object IEnumerator.Current + { + get { return Current; } + } + + void IEnumerator.Reset() + { + if (_version != _stack._version) throw new InvalidOperationException("Collection was modified during enumeration."); + _index = -2; + _currentElement = default; + } + } + } +} diff --git a/OctaneEngine/Collections.Pooled/QueueDebugView.cs b/OctaneEngine/Collections.Pooled/QueueDebugView.cs new file mode 100644 index 0000000..e8b16da --- /dev/null +++ b/OctaneEngine/Collections.Pooled/QueueDebugView.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Diagnostics; + +namespace Collections.Pooled +{ + internal sealed class QueueDebugView + { + private readonly PooledQueue _queue; + + public QueueDebugView(PooledQueue queue) + { + _queue = queue ?? throw new ArgumentNullException(nameof(queue)); + } + + [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] + public T[] Items + { + get + { + return _queue.ToArray(); + } + } + } +} diff --git a/OctaneEngine/Collections.Pooled/StackDebugView.cs b/OctaneEngine/Collections.Pooled/StackDebugView.cs new file mode 100644 index 0000000..598e0f7 --- /dev/null +++ b/OctaneEngine/Collections.Pooled/StackDebugView.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Diagnostics; + +namespace Collections.Pooled +{ + internal sealed class StackDebugView + { + private readonly PooledStack _stack; + + public StackDebugView(PooledStack stack) + { + _stack = stack ?? throw new ArgumentNullException(nameof(stack)); + } + + [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] + public T[] Items + { + get + { + return _stack.ToArray(); + } + } + } +} diff --git a/OctaneEngine/Collections.Pooled/ThrowHelper.cs b/OctaneEngine/Collections.Pooled/ThrowHelper.cs new file mode 100644 index 0000000..939a9ac --- /dev/null +++ b/OctaneEngine/Collections.Pooled/ThrowHelper.cs @@ -0,0 +1,692 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +// This file defines an internal class used to throw exceptions in BCL code. +// The main purpose is to reduce code size. +// +// The old way to throw an exception generates quite a lot IL code and assembly code. +// Following is an example: +// C# source +// throw new ArgumentNullException(nameof(key), SR.ArgumentNull_Key); +// IL code: +// IL_0003: ldstr "key" +// IL_0008: ldstr "ArgumentNull_Key" +// IL_000d: call string System.Environment::GetResourceString(string) +// IL_0012: newobj instance void System.ArgumentNullException::.ctor(string,string) +// IL_0017: throw +// which is 21bytes in IL. +// +// So we want to get rid of the ldstr and call to Environment.GetResource in IL. +// In order to do that, I created two enums: ExceptionResource, ExceptionArgument to represent the +// argument name and resource name in a small integer. The source code will be changed to +// ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key, ExceptionResource.ArgumentNull_Key); +// +// The IL code will be 7 bytes. +// IL_0008: ldc.i4.4 +// IL_0009: ldc.i4.4 +// IL_000a: call void System.ThrowHelper::ThrowArgumentNullException(valuetype System.ExceptionArgument) +// IL_000f: ldarg.0 +// +// This will also reduce the Jitted code size a lot. +// +// It is very important we do this for generic classes because we can easily generate the same code +// multiple times for different instantiation. +// + +using System; +using System.Buffers; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.Serialization; + +namespace Collections.Pooled +{ + internal static class ThrowHelper + { + internal static void ThrowArrayTypeMismatchException() + { + throw new ArrayTypeMismatchException(); + } + + internal static void ThrowIndexOutOfRangeException() + { + throw new IndexOutOfRangeException(); + } + + internal static void ThrowArgumentOutOfRangeException() + { + throw new ArgumentOutOfRangeException(); + } + + internal static void ThrowArgumentException_DestinationTooShort() + { + throw new ArgumentException("Destination too short."); + } + + internal static void ThrowArgumentException_OverlapAlignmentMismatch() + { + throw new ArgumentException("Overlap alignment mismatch."); + } + + internal static void ThrowArgumentOutOfRange_IndexException() + { + throw GetArgumentOutOfRangeException(ExceptionArgument.index, + ExceptionResource.ArgumentOutOfRange_Index); + } + + internal static void ThrowIndexArgumentOutOfRange_NeedNonNegNumException() + { + throw GetArgumentOutOfRangeException(ExceptionArgument.index, + ExceptionResource.ArgumentOutOfRange_NeedNonNegNum); + } + + internal static void ThrowValueArgumentOutOfRange_NeedNonNegNumException() + { + throw GetArgumentOutOfRangeException(ExceptionArgument.value, + ExceptionResource.ArgumentOutOfRange_NeedNonNegNum); + } + + internal static void ThrowLengthArgumentOutOfRange_ArgumentOutOfRange_NeedNonNegNum() + { + throw GetArgumentOutOfRangeException(ExceptionArgument.length, + ExceptionResource.ArgumentOutOfRange_NeedNonNegNum); + } + + internal static void ThrowStartIndexArgumentOutOfRange_ArgumentOutOfRange_Index() + { + throw GetArgumentOutOfRangeException(ExceptionArgument.startIndex, + ExceptionResource.ArgumentOutOfRange_Index); + } + + internal static void ThrowCountArgumentOutOfRange_ArgumentOutOfRange_Count() + { + throw GetArgumentOutOfRangeException(ExceptionArgument.count, + ExceptionResource.ArgumentOutOfRange_Count); + } + + internal static void ThrowWrongKeyTypeArgumentException(T key, Type targetType) + { + // Generic key to move the boxing to the right hand side of throw + throw GetWrongKeyTypeArgumentException((object)key, targetType); + } + + internal static void ThrowWrongValueTypeArgumentException(T value, Type targetType) + { + // Generic key to move the boxing to the right hand side of throw + throw GetWrongValueTypeArgumentException((object)value, targetType); + } + + private static ArgumentException GetAddingDuplicateWithKeyArgumentException(object key) + { + return new ArgumentException($"Error adding duplicate with key: {key}."); + } + + internal static void ThrowAddingDuplicateWithKeyArgumentException(T key) + { + // Generic key to move the boxing to the right hand side of throw + throw GetAddingDuplicateWithKeyArgumentException((object)key); + } + + internal static void ThrowKeyNotFoundException(T key) + { + // Generic key to move the boxing to the right hand side of throw + throw GetKeyNotFoundException((object)key); + } + + internal static void ThrowArgumentException(ExceptionResource resource) + { + throw GetArgumentException(resource); + } + + internal static void ThrowArgumentException(ExceptionResource resource, ExceptionArgument argument) + { + throw GetArgumentException(resource, argument); + } + + private static ArgumentNullException GetArgumentNullException(ExceptionArgument argument) + { + return new ArgumentNullException(GetArgumentName(argument)); + } + + internal static void ThrowArgumentNullException(ExceptionArgument argument) + { + throw GetArgumentNullException(argument); + } + + internal static void ThrowArgumentNullException(ExceptionResource resource) + { + throw new ArgumentNullException(GetResourceString(resource)); + } + + internal static void ThrowArgumentNullException(ExceptionArgument argument, ExceptionResource resource) + { + throw new ArgumentNullException(GetArgumentName(argument), GetResourceString(resource)); + } + + internal static void ThrowArgumentOutOfRangeException(ExceptionArgument argument) + { + throw new ArgumentOutOfRangeException(GetArgumentName(argument)); + } + + internal static void ThrowArgumentOutOfRangeException(ExceptionArgument argument, ExceptionResource resource) + { + throw GetArgumentOutOfRangeException(argument, resource); + } + + internal static void ThrowArgumentOutOfRangeException(ExceptionArgument argument, int paramNumber, ExceptionResource resource) + { + throw GetArgumentOutOfRangeException(argument, paramNumber, resource); + } + + internal static void ThrowInvalidOperationException(ExceptionResource resource) + { + throw GetInvalidOperationException(resource); + } + + internal static void ThrowInvalidOperationException(ExceptionResource resource, Exception e) + { + throw new InvalidOperationException(GetResourceString(resource), e); + } + + internal static void ThrowSerializationException(ExceptionResource resource) + { + throw new SerializationException(GetResourceString(resource)); + } + + internal static void ThrowSecurityException(ExceptionResource resource) + { + throw new System.Security.SecurityException(GetResourceString(resource)); + } + + internal static void ThrowRankException(ExceptionResource resource) + { + throw new RankException(GetResourceString(resource)); + } + + internal static void ThrowNotSupportedException(ExceptionResource resource) + { + throw new NotSupportedException(GetResourceString(resource)); + } + + internal static void ThrowUnauthorizedAccessException(ExceptionResource resource) + { + throw new UnauthorizedAccessException(GetResourceString(resource)); + } + + internal static void ThrowObjectDisposedException(string objectName, ExceptionResource resource) + { + throw new ObjectDisposedException(objectName, GetResourceString(resource)); + } + + internal static void ThrowObjectDisposedException(ExceptionResource resource) + { + throw new ObjectDisposedException(null, GetResourceString(resource)); + } + + internal static void ThrowNotSupportedException() + { + throw new NotSupportedException(); + } + + internal static void ThrowAggregateException(List exceptions) + { + throw new AggregateException(exceptions); + } + + internal static void ThrowOutOfMemoryException() + { + throw new OutOfMemoryException(); + } + + internal static void ThrowArgumentException_Argument_InvalidArrayType() + { + throw new ArgumentException("Invalid array type."); + } + + internal static void ThrowInvalidOperationException_InvalidOperation_EnumNotStarted() + { + throw new InvalidOperationException("Enumeration has not started."); + } + + internal static void ThrowInvalidOperationException_InvalidOperation_EnumEnded() + { + throw new InvalidOperationException("Enumeration has ended."); + } + + internal static void ThrowInvalidOperationException_EnumCurrent(int index) + { + throw GetInvalidOperationException_EnumCurrent(index); + } + + internal static void ThrowInvalidOperationException_InvalidOperation_EnumFailedVersion() + { + throw new InvalidOperationException("Collection was modified during enumeration."); + } + + internal static void ThrowInvalidOperationException_InvalidOperation_EnumOpCantHappen() + { + throw new InvalidOperationException("Invalid enumerator state: enumeration cannot proceed."); + } + + internal static void ThrowInvalidOperationException_InvalidOperation_NoValue() + { + throw new InvalidOperationException("No value provided."); + } + + internal static void ThrowInvalidOperationException_ConcurrentOperationsNotSupported() + { + throw new InvalidOperationException("Concurrent operations are not supported."); + } + + internal static void ThrowInvalidOperationException_HandleIsNotInitialized() + { + throw new InvalidOperationException("Handle is not initialized."); + } + + internal static void ThrowFormatException_BadFormatSpecifier() + { + throw new FormatException("Bad format specifier."); + } + + private static ArgumentException GetArgumentException(ExceptionResource resource) + { + return new ArgumentException(GetResourceString(resource)); + } + + private static InvalidOperationException GetInvalidOperationException(ExceptionResource resource) + { + return new InvalidOperationException(GetResourceString(resource)); + } + + private static ArgumentException GetWrongKeyTypeArgumentException(object key, Type targetType) + { + return new ArgumentException($"Wrong key type. Expected {targetType}, got: '{key}'.", nameof(key)); + } + + private static ArgumentException GetWrongValueTypeArgumentException(object value, Type targetType) + { + return new ArgumentException($"Wrong value type. Expected {targetType}, got: '{value}'.", nameof(value)); + } + + private static KeyNotFoundException GetKeyNotFoundException(object key) + { + return new KeyNotFoundException($"Key not found: {key}"); + } + + private static ArgumentOutOfRangeException GetArgumentOutOfRangeException(ExceptionArgument argument, ExceptionResource resource) + { + return new ArgumentOutOfRangeException(GetArgumentName(argument), GetResourceString(resource)); + } + + private static ArgumentException GetArgumentException(ExceptionResource resource, ExceptionArgument argument) + { + return new ArgumentException(GetResourceString(resource), GetArgumentName(argument)); + } + + private static ArgumentOutOfRangeException GetArgumentOutOfRangeException(ExceptionArgument argument, int paramNumber, ExceptionResource resource) + { + return new ArgumentOutOfRangeException(GetArgumentName(argument) + "[" + paramNumber.ToString() + "]", GetResourceString(resource)); + } + + private static InvalidOperationException GetInvalidOperationException_EnumCurrent(int index) + { + return new InvalidOperationException( + index < 0 ? + "Enumeration has not started" : + "Enumeration has ended"); + } + + // Allow nulls for reference types and Nullable, but not for value types. + // Aggressively inline so the jit evaluates the if in place and either drops the call altogether + // Or just leaves null test and call to the Non-returning ThrowHelper.ThrowArgumentNullException + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void IfNullAndNullsAreIllegalThenThrow(object value, ExceptionArgument argName) + { + // Note that default(T) is not equal to null for value types except when T is Nullable. + if (!(default(T) == null) && value == null) + ThrowHelper.ThrowArgumentNullException(argName); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void ThrowForUnsupportedVectorBaseType() where T : struct + { + if (typeof(T) != typeof(byte) && typeof(T) != typeof(sbyte) && + typeof(T) != typeof(short) && typeof(T) != typeof(ushort) && + typeof(T) != typeof(int) && typeof(T) != typeof(uint) && + typeof(T) != typeof(long) && typeof(T) != typeof(ulong) && + typeof(T) != typeof(float) && typeof(T) != typeof(double)) + { + ThrowNotSupportedException(ExceptionResource.Arg_TypeNotSupported); + } + } + +#if false // Reflection-based implementation does not work for CoreRT/ProjectN + // This function will convert an ExceptionArgument enum value to the argument name string. + [MethodImpl(MethodImplOptions.NoInlining)] + private static string GetArgumentName(ExceptionArgument argument) + { + Debug.Assert(Enum.IsDefined(typeof(ExceptionArgument), argument), + "The enum value is not defined, please check the ExceptionArgument Enum."); + + return argument.ToString(); + } +#endif + + private static string GetArgumentName(ExceptionArgument argument) + { + switch (argument) + { + case ExceptionArgument.obj: + return "obj"; + case ExceptionArgument.dictionary: + return "dictionary"; + case ExceptionArgument.array: + return "array"; + case ExceptionArgument.info: + return "info"; + case ExceptionArgument.key: + return "key"; + case ExceptionArgument.text: + return "text"; + case ExceptionArgument.values: + return "values"; + case ExceptionArgument.value: + return "value"; + case ExceptionArgument.startIndex: + return "startIndex"; + case ExceptionArgument.task: + return "task"; + case ExceptionArgument.ch: + return "ch"; + case ExceptionArgument.s: + return "s"; + case ExceptionArgument.input: + return "input"; + case ExceptionArgument.list: + return "list"; + case ExceptionArgument.index: + return "index"; + case ExceptionArgument.capacity: + return "capacity"; + case ExceptionArgument.collection: + return "collection"; + case ExceptionArgument.item: + return "item"; + case ExceptionArgument.converter: + return "converter"; + case ExceptionArgument.match: + return "match"; + case ExceptionArgument.count: + return "count"; + case ExceptionArgument.action: + return "action"; + case ExceptionArgument.comparison: + return "comparison"; + case ExceptionArgument.exceptions: + return "exceptions"; + case ExceptionArgument.exception: + return "exception"; + case ExceptionArgument.enumerable: + return "enumerable"; + case ExceptionArgument.start: + return "start"; + case ExceptionArgument.format: + return "format"; + case ExceptionArgument.culture: + return "culture"; + case ExceptionArgument.comparer: + return "comparer"; + case ExceptionArgument.comparable: + return "comparable"; + case ExceptionArgument.source: + return "source"; + case ExceptionArgument.state: + return "state"; + case ExceptionArgument.length: + return "length"; + case ExceptionArgument.comparisonType: + return "comparisonType"; + case ExceptionArgument.manager: + return "manager"; + case ExceptionArgument.sourceBytesToCopy: + return "sourceBytesToCopy"; + case ExceptionArgument.callBack: + return "callBack"; + case ExceptionArgument.creationOptions: + return "creationOptions"; + case ExceptionArgument.function: + return "function"; + case ExceptionArgument.delay: + return "delay"; + case ExceptionArgument.millisecondsDelay: + return "millisecondsDelay"; + case ExceptionArgument.millisecondsTimeout: + return "millisecondsTimeout"; + case ExceptionArgument.timeout: + return "timeout"; + case ExceptionArgument.type: + return "type"; + case ExceptionArgument.sourceIndex: + return "sourceIndex"; + case ExceptionArgument.sourceArray: + return "sourceArray"; + case ExceptionArgument.destinationIndex: + return "destinationIndex"; + case ExceptionArgument.destinationArray: + return "destinationArray"; + case ExceptionArgument.other: + return "other"; + case ExceptionArgument.newSize: + return "newSize"; + case ExceptionArgument.lowerBounds: + return "lowerBounds"; + case ExceptionArgument.lengths: + return "lengths"; + case ExceptionArgument.len: + return "len"; + case ExceptionArgument.keys: + return "keys"; + case ExceptionArgument.indices: + return "indices"; + case ExceptionArgument.endIndex: + return "endIndex"; + case ExceptionArgument.elementType: + return "elementType"; + case ExceptionArgument.arrayIndex: + return "arrayIndex"; + default: + Debug.Fail("The enum value is not defined, please check the ExceptionArgument Enum."); + return argument.ToString(); + } + } + +#if false // Reflection-based implementation does not work for CoreRT/ProjectN + // This function will convert an ExceptionResource enum value to the resource string. + [MethodImpl(MethodImplOptions.NoInlining)] + private static string GetResourceString(ExceptionResource resource) + { + Debug.Assert(Enum.IsDefined(typeof(ExceptionResource), resource), + "The enum value is not defined, please check the ExceptionResource Enum."); + + return SR.GetResourceString(resource.ToString()); + } +#endif + + private static string GetResourceString(ExceptionResource resource) + { + switch (resource) + { + case ExceptionResource.ArgumentOutOfRange_Index: + return "Argument 'index' was out of the range of valid values."; + case ExceptionResource.ArgumentOutOfRange_Count: + return "Argument 'count' was out of the range of valid values."; + case ExceptionResource.Arg_ArrayPlusOffTooSmall: + return "Array plus offset too small."; + case ExceptionResource.NotSupported_ReadOnlyCollection: + return "This operation is not supported on a read-only collection."; + case ExceptionResource.Arg_RankMultiDimNotSupported: + return "Multi-dimensional arrays are not supported."; + case ExceptionResource.Arg_NonZeroLowerBound: + return "Arrays with a non-zero lower bound are not supported."; + case ExceptionResource.ArgumentOutOfRange_ListInsert: + return "Insertion index was out of the range of valid values."; + case ExceptionResource.ArgumentOutOfRange_NeedNonNegNum: + return "The number must be non-negative."; + case ExceptionResource.ArgumentOutOfRange_SmallCapacity: + return "The capacity cannot be set below the current Count."; + case ExceptionResource.Argument_InvalidOffLen: + return "Invalid offset length."; + case ExceptionResource.ArgumentOutOfRange_BiggerThanCollection: + return "The given value was larger than the size of the collection."; + case ExceptionResource.Serialization_MissingKeys: + return "Serialization error: missing keys."; + case ExceptionResource.Serialization_NullKey: + return "Serialization error: null key."; + case ExceptionResource.NotSupported_KeyCollectionSet: + return "The KeyCollection does not support modification."; + case ExceptionResource.NotSupported_ValueCollectionSet: + return "The ValueCollection does not support modification."; + case ExceptionResource.InvalidOperation_NullArray: + return "Null arrays are not supported."; + case ExceptionResource.InvalidOperation_HSCapacityOverflow: + return "Set hash capacity overflow. Cannot increase size."; + case ExceptionResource.NotSupported_StringComparison: + return "String comparison not supported."; + case ExceptionResource.ConcurrentCollection_SyncRoot_NotSupported: + return "SyncRoot not supported."; + case ExceptionResource.ArgumentException_OtherNotArrayOfCorrectLength: + return "The other array is not of the correct length."; + case ExceptionResource.ArgumentOutOfRange_EndIndexStartIndex: + return "The end index does not come after the start index."; + case ExceptionResource.ArgumentOutOfRange_HugeArrayNotSupported: + return "Huge arrays are not supported."; + case ExceptionResource.Argument_AddingDuplicate: + return "Duplicate item added."; + case ExceptionResource.Argument_InvalidArgumentForComparison: + return "Invalid argument for comparison."; + case ExceptionResource.Arg_LowerBoundsMustMatch: + return "Array lower bounds must match."; + case ExceptionResource.Arg_MustBeType: + return "Argument must be of type: "; + case ExceptionResource.InvalidOperation_IComparerFailed: + return "IComparer failed."; + case ExceptionResource.NotSupported_FixedSizeCollection: + return "This operation is not suppored on a fixed-size collection."; + case ExceptionResource.Rank_MultiDimNotSupported: + return "Multi-dimensional arrays are not supported."; + case ExceptionResource.Arg_TypeNotSupported: + return "Type not supported."; + default: + Debug.Assert(false, + "The enum value is not defined, please check the ExceptionResource Enum."); + return resource.ToString(); + } + } + } + + // + // The convention for this enum is using the argument name as the enum name + // + internal enum ExceptionArgument + { + obj, + dictionary, + array, + info, + key, + text, + values, + value, + startIndex, + task, + ch, + s, + input, + list, + index, + capacity, + collection, + item, + converter, + match, + count, + action, + comparison, + exceptions, + exception, + enumerable, + start, + format, + culture, + comparer, + comparable, + source, + state, + length, + comparisonType, + manager, + sourceBytesToCopy, + callBack, + creationOptions, + function, + delay, + millisecondsDelay, + millisecondsTimeout, + timeout, + type, + sourceIndex, + sourceArray, + destinationIndex, + destinationArray, + other, + newSize, + lowerBounds, + lengths, + len, + keys, + indices, + endIndex, + elementType, + arrayIndex + } + + // + // The convention for this enum is using the resource name as the enum name + // + internal enum ExceptionResource + { + ArgumentOutOfRange_Index, + ArgumentOutOfRange_Count, + Arg_ArrayPlusOffTooSmall, + NotSupported_ReadOnlyCollection, + Arg_RankMultiDimNotSupported, + Arg_NonZeroLowerBound, + ArgumentOutOfRange_ListInsert, + ArgumentOutOfRange_NeedNonNegNum, + ArgumentOutOfRange_SmallCapacity, + Argument_InvalidOffLen, + ArgumentOutOfRange_BiggerThanCollection, + Serialization_MissingKeys, + Serialization_NullKey, + NotSupported_KeyCollectionSet, + NotSupported_ValueCollectionSet, + InvalidOperation_NullArray, + InvalidOperation_HSCapacityOverflow, + NotSupported_StringComparison, + ConcurrentCollection_SyncRoot_NotSupported, + ArgumentException_OtherNotArrayOfCorrectLength, + ArgumentOutOfRange_EndIndexStartIndex, + ArgumentOutOfRange_HugeArrayNotSupported, + Argument_AddingDuplicate, + Argument_InvalidArgumentForComparison, + Arg_LowerBoundsMustMatch, + Arg_MustBeType, + InvalidOperation_IComparerFailed, + NotSupported_FixedSizeCollection, + Rank_MultiDimNotSupported, + Arg_TypeNotSupported, + } +} diff --git a/OctaneEngine/OctaneEngine.csproj b/OctaneEngine/OctaneEngine.csproj index 6380052..8ff72aa 100644 --- a/OctaneEngine/OctaneEngine.csproj +++ b/OctaneEngine/OctaneEngine.csproj @@ -46,7 +46,6 @@ - From f312a326f9ef2541acee78bb16c7e15322ff177a Mon Sep 17 00:00:00 2001 From: Greg Date: Mon, 21 Aug 2023 22:34:15 -0500 Subject: [PATCH 2/4] Added License --- OctaneEngine/Collections.Pooled/LICENSE.txt | 23 +++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 OctaneEngine/Collections.Pooled/LICENSE.txt diff --git a/OctaneEngine/Collections.Pooled/LICENSE.txt b/OctaneEngine/Collections.Pooled/LICENSE.txt new file mode 100644 index 0000000..74549cb --- /dev/null +++ b/OctaneEngine/Collections.Pooled/LICENSE.txt @@ -0,0 +1,23 @@ +The MIT License (MIT) + +Copyright (c) Joel Mueller + +All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file From 9969f989fbe3eb12870fdf5af1a59055f0544e8a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Aug 2023 22:36:14 -0500 Subject: [PATCH 3/4] Bump Serilog from 2.12.0 to 3.0.1 (#95) Bumps [Serilog](https://github.com/serilog/serilog) from 2.12.0 to 3.0.1. - [Release notes](https://github.com/serilog/serilog/releases) - [Changelog](https://github.com/serilog/serilog/blob/dev/CHANGES.md) - [Commits](https://github.com/serilog/serilog/compare/v2.12.0...v3.0.1) --- updated-dependencies: - dependency-name: Serilog dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- OctaneTester/OctaneTester.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/OctaneTester/OctaneTester.csproj b/OctaneTester/OctaneTester.csproj index bb1f975..725230c 100644 --- a/OctaneTester/OctaneTester.csproj +++ b/OctaneTester/OctaneTester.csproj @@ -1,4 +1,4 @@ - + Exe @@ -13,7 +13,7 @@ - + From 82b320aca7457a7018c608f30dcb598f9bdaca22 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Aug 2023 22:39:16 -0500 Subject: [PATCH 4/4] Bump System.Reactive from 5.0.0 to 6.0.0 (#89) Bumps [System.Reactive](https://github.com/dotnet/reactive) from 5.0.0 to 6.0.0. - [Release notes](https://github.com/dotnet/reactive/releases) - [Commits](https://github.com/dotnet/reactive/commits) --- updated-dependencies: - dependency-name: System.Reactive dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- OctaneEngine/OctaneEngine.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/OctaneEngine/OctaneEngine.csproj b/OctaneEngine/OctaneEngine.csproj index 8ff72aa..c369801 100644 --- a/OctaneEngine/OctaneEngine.csproj +++ b/OctaneEngine/OctaneEngine.csproj @@ -1,4 +1,4 @@ - + net461;net472;net6.0; @@ -52,7 +52,7 @@ - +