// Copyright (c) 2023 bradson // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. // // Loosely based on https://github.com/matthewcrews/FastDictionaryTest/tree/main // which itself is based on // https://probablydance.com/2018/05/28/a-new-fast-hash-table-in-response-to-googles-new-fast-hash-table/ #if V1_2 extern alias nuget; #endif using System.Collections; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using FisheryLib.FunctionPointers; using FisheryLib.Pools; using FisheryLib.Utility; #if V1_2 using CollectionAccessType = nuget::JetBrains.Annotations.CollectionAccessType; using CollectionAccessAttribute = nuget::JetBrains.Annotations.CollectionAccessAttribute; #endif namespace FisheryLib.Collections; #pragma warning disable CS8766, CS8767 [PublicAPI] [StructLayout(LayoutKind.Sequential)] public class FishSet : ISet, IReadOnlyCollection, ICollection { private const int FIBONACCI_HASH = -1640531527; // unchecked((int)(uint)Math.Round(uint.MaxValue / FishMath.PHI)); internal T?[] _buckets; private unsafe delegate* _hashCodeGetter = GetHashCode.Default; private unsafe delegate* _equalityComparer = Equals.Default; private int _bucketBitShift, _count; internal Tails _tails; private int _wrapAroundMask, _version; private float _maxLoadFactor = 0.5f; private T _defaultValue = Internal.KeyUtility.Default; public int Version { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _version; set { Guard.IsGreaterThan(value, _version); _version = value; } } public int Count { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _count; } object ICollection.SyncRoot => this; bool ICollection.IsSynchronized => false; public bool IsReadOnly => false; /// /// Empty FishSets have their entries set to a default value, causing fishSet.Contains(DefaultValue) to return /// true. This property can be used for a check against this default, similar to a null. Unless set to a different /// default through the FishSet constructor, for most reference types, the returned value is a static invalid /// reference. Reference types implementing IEquatable or overriding Equals have null as default instead. Primitives /// have all their bytes set to sbyte.MaxValue. Structs have their contents set according to previous rules. /// public T DefaultValue => _defaultValue; public unsafe uint SizeEstimate => GetType().ComputeManagedObjectSizeEstimate() + ((uint)sizeof(T) * (uint)_buckets.Length) + ((_tails.Length + 1u) >> 1); public uint CollisionCount => (uint)GetTailingEntriesCount(); public event Action? EntryAdded, EntryRemoved; public float MaxLoadFactor { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _maxLoadFactor; set { Guard.IsInRange(value, 0f, 1f); _maxLoadFactor = value; } } private static T AssertKeyType(object key) { if (key is T tKey) return tKey; else if (key != null!) ThrowHelper.ThrowWrongKeyTypeArgumentException(key); return default!; } public bool this[T key] { [CollectionAccess(CollectionAccessType.Read)] [MethodImpl(MethodImplOptions.AggressiveInlining)] get => Contains(key); [CollectionAccess(CollectionAccessType.UpdatedContent)] set { if (value) InsertEntry(key, ReplaceBehaviour.Fail); else Remove(key); } } public FishSet() : this(0) { } public FishSet(int minimumCapacity) => Initialize(minimumCapacity); /// /// Initialize a new FishSet /// /// The minimum capacity of the backing array /// /// The default value for empty entries. Contains() returns true for this value when empty /// public FishSet(int minimumCapacity, T defaultValue) { _defaultValue = defaultValue; Initialize(minimumCapacity); } [MemberNotNull(nameof(_buckets))] [MemberNotNull(nameof(_tails))] private void Initialize(int minimumCapacity = 0) { minimumCapacity = minimumCapacity <= 4 ? 4 : FishMath.NextPowerOfTwo(minimumCapacity); // Mathf.NextPowerOfTwo(minimumCapacity); _buckets = new T?[minimumCapacity]; Array.Fill(_buckets, _defaultValue); _tails = new((uint)minimumCapacity); _bucketBitShift = 32 - FishMath.TrailingZeroCount((uint)_buckets.Length); _wrapAroundMask = _buckets.Length - 1; } public FishSet(IEnumerable entries) : this(0) { foreach (var entry in entries) InsertEntry(entry, ReplaceBehaviour.Throw); } private int GetTailIndex(int entryIndex) => GetIndexForTail(entryIndex, _tails[(uint)entryIndex]); [MethodImpl(MethodImplOptions.AggressiveInlining)] private int GetIndexForTail(int entryIndex, uint tail) => (entryIndex + GetJumpDistance(tail)) & _wrapAroundMask; private static int GetJumpDistance(uint index) => index switch { 2 => 1, 3 => 2, 4 => 3, 5 => 4, 6 => 5, 7 => 8, 8 => 13, 9 => 21, 10 => 34, 11 => 55, 12 => 89, 13 => 144, 14 => 233, 15 => 377, _ => MultiplyWithPhi(index) }; [MethodImpl(MethodImplOptions.NoInlining)] [DoesNotReturn] private static int MultiplyWithPhi(uint index) => throw new NotImplementedException(); // (int)(uint)Math.Round(index * FishMath.PHI); [MethodImpl(MethodImplOptions.AggressiveInlining)] private bool IsTail(int bucketIndex) => bucketIndex != GetBucketIndexForKeyAt(bucketIndex); [MethodImpl(MethodImplOptions.AggressiveInlining)] private bool IsTail(T entry, int bucketIndex) => bucketIndex != GetBucketIndex(entry); [MethodImpl(MethodImplOptions.AggressiveInlining)] private bool HasTail(int entryIndex) => !_tails.IsSoloOrEmpty(entryIndex); private void SetEntryWithoutTail(int bucketIndex, T entry) { _buckets[bucketIndex] = entry; _tails.SetSolo(bucketIndex); } private void SetBucketEmpty(int index) { _buckets[index] = _defaultValue; _tails.SetEmpty(index); } private void SetEntryAsTail(int bucketIndex, T entry, int parentIndex, uint offset) { SetEntryWithoutTail(bucketIndex, entry); _tails[(uint)parentIndex] = offset; } private void SetParentTail(int entryIndex, uint newTail) => _tails[(uint)GetParentBucketIndex(entryIndex)] = newTail; private bool InsertEntry(T entry, ReplaceBehaviour replaceBehaviour, bool shifting = false) { var addedNewEntry = InsertEntryInternal(entry, replaceBehaviour); if (shifting) return addedNewEntry; _version++; if (!addedNewEntry) return false; OnEntryAdded(entry); return true; } private void OnEntryAdded(T entry) { // _version++; handled separately _count++; EntryAdded?.Invoke(entry); } /// /// Returns true when adding a new entry, false for replacing an existing entry. AllowReplace: false causes throwing /// instead. Does not adjust Count, Version or invoke EntryAdded. /// private bool InsertEntryInternal(T entry, ReplaceBehaviour replaceBehaviour) { StartOfMethod: var bucketIndex = GetBucketIndex(entry); ref var bucket = ref _buckets[bucketIndex]; if (IsBucketEmpty(bucketIndex)) { SetEntryWithoutTail(bucketIndex, entry); return true; } if (TryReplaceValueOfMatchingKey(entry, replaceBehaviour, ref bucket!)) return false; if (CheckResize()) goto StartOfMethod; if (IsTail(bucket, bucketIndex)) { var previousEntry = bucket; using var tailingEntries = TryGetAndClearTailingEntries(bucketIndex); SetParentTail(bucketIndex, Tails.SOLO); SetEntryWithoutTail(bucketIndex, entry); InsertEntry(previousEntry, ReplaceBehaviour.Throw, true); if (tailingEntries != default) InsertEntries(tailingEntries.List, ReplaceBehaviour.Throw, true); return true; } else { return !TryFindTailIndexAndReplace(entry, ref bucketIndex, replaceBehaviour) && InsertAsTail(entry, bucketIndex); } } private void InsertEntries(List tailingEntries, ReplaceBehaviour replaceBehaviour, bool shifting = false) { for (var i = 0; i < tailingEntries.Count; i++) InsertEntry(tailingEntries[i], replaceBehaviour, shifting); } private int GetTailCount(int index) { var tailCount = 0; while (HasTail(index)) { tailCount++; index = GetTailIndex(index); } return tailCount; } private bool IsBucketEmpty(int bucketIndex) => IsBucketEmpty(bucketIndex, _tails, _buckets); [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool IsBucketEmpty(int bucketIndex, Tails tails, T?[] buckets) => tails.IsEmpty(bucketIndex); private bool TryFindTailIndexAndReplace(T entry, ref int bucketIndex, ReplaceBehaviour replaceBehaviour) { while (true) { if (!TryContinueWithTail(ref bucketIndex)) return false; if (TryReplaceValueOfMatchingKey(entry, replaceBehaviour, ref _buckets[bucketIndex]!)) return true; } } private unsafe bool TryReplaceValueOfMatchingKey(T entry, ReplaceBehaviour replaceBehaviour, ref T bucket) { if (!_equalityComparer(bucket, entry)) return false; switch (replaceBehaviour) { case ReplaceBehaviour.Throw: return ThrowHelper.ThrowAddingDuplicateWithKeyArgumentException(entry); case ReplaceBehaviour.Replace: bucket = entry; goto default; case ReplaceBehaviour.Fail: default: return true; } } private bool InsertAsTail(T entry, int bucketIndex) { var parentIndex = bucketIndex; var offset = 2u; while (true) { bucketIndex = GetIndexForTail(parentIndex, offset); if (IsBucketEmpty(bucketIndex)) { SetEntryAsTail(bucketIndex, entry, parentIndex, offset); return true; } if (IsTail(bucketIndex) && offset < _tails[(uint)GetParentBucketIndex(bucketIndex)]) break; if (++offset <= 15u) continue; if (_buckets.Length > Count * 5) WarnForPossiblyExcessiveResizing(entry); Resize(); InsertEntry(entry, ReplaceBehaviour.Throw, true); return true; } var previousEntry = _buckets[bucketIndex]; using var tailingEntries = TryGetAndClearTailingEntries(bucketIndex); SetParentTail(bucketIndex, Tails.SOLO); SetEntryAsTail(bucketIndex, entry, parentIndex, offset); InsertEntry(previousEntry!, ReplaceBehaviour.Throw, true); if (tailingEntries != default) InsertEntries(tailingEntries.List, ReplaceBehaviour.Throw, true); return true; } private PooledIList> TryGetAndClearTailingEntries(int bucketIndex) { if (!HasTail(bucketIndex)) return default; var tailingEntries = new PooledIList>(); var tailingEntriesList = tailingEntries.List; var tailIndex = GetTailIndex(bucketIndex); while (true) { ref var tailingBucket = ref _buckets[tailIndex]; tailingEntriesList.Add(tailingBucket!); tailingBucket = _defaultValue; if (HasTail(tailIndex)) { var nextTailIndex = GetTailIndex(tailIndex); _tails.SetEmpty(tailIndex); tailIndex = nextTailIndex; } else { _tails.SetEmpty(tailIndex); break; } } return tailingEntries; } [MethodImpl(MethodImplOptions.NoInlining)] private void WarnForPossiblyExcessiveResizing(T entry) => Log.Warning($"FishTable is resizing from a large number of clashing hashCodes. Last inserted key: '{ entry}', count: '{_count}', bucket array length: '{ _buckets.Length}', tailing entries: '{GetTailingEntriesCount()}'"); private int GetTailingEntriesCount() { var count = 0; for (var i = _buckets.Length; i-- > 0;) { if (!IsBucketEmpty(i) && IsTail(i)) count++; } return count; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private bool CheckResize() { if (_count <= (int)(_buckets.Length * _maxLoadFactor)) return false; Resize(); return true; } private void Resize() { var oldBuckets = _buckets; var oldTails = _tails; _tails = new((uint)_buckets.Length << 1); _buckets = new T?[_buckets.Length << 1]; Array.Fill(_buckets, _defaultValue); _bucketBitShift = 32 - FishMath.TrailingZeroCount((uint)_buckets.Length); _wrapAroundMask = _buckets.Length - 1; for (var i = 0; i < oldBuckets.Length; i++) { if (!IsBucketEmpty(i, oldTails, oldBuckets)) InsertEntry(oldBuckets[i]!, ReplaceBehaviour.Throw, true); } } [CollectionAccess(CollectionAccessType.Read)] public unsafe bool Contains(T key) { var bucketIndex = GetBucketIndex(key); while (true) { if (_equalityComparer(key, _buckets[bucketIndex])) return true; if (!TryContinueWithTail(ref bucketIndex)) return false; } } #region GetBucketIndexMethods [MethodImpl(MethodImplOptions.AggressiveInlining)] private unsafe int GetBucketIndex(T key) => GetBucketIndex(_hashCodeGetter(key)); [MethodImpl(MethodImplOptions.AggressiveInlining)] private int GetBucketIndex(int hashCode) => (hashCode * FIBONACCI_HASH) >>> _bucketBitShift; [MethodImpl(MethodImplOptions.AggressiveInlining)] private int GetBucketIndexForKeyAt(int index) => GetBucketIndex(_buckets[index]!); [MethodImpl(MethodImplOptions.AggressiveInlining)] private int GetParentBucketIndex(int childBucketIndex) { var ancestorIndex = GetBucketIndexForKeyAt(childBucketIndex); var i = 0; while (true) { if (i++ > 32) ThrowHelper.ThrowFailedToFindParentInvalidOperationException(this, childBucketIndex); var nextIndex = GetTailIndex(ancestorIndex); if (nextIndex == childBucketIndex) break; ancestorIndex = nextIndex; } return ancestorIndex; } #endregion /// /// Returns false for solo or empty, otherwise sets the index to the tail index and returns true /// [MethodImpl(MethodImplOptions.NoInlining)] private bool TryContinueWithTail(ref int entryIndex) { if (!HasTail(entryIndex)) return false; entryIndex = GetTailIndex(entryIndex); return true; } [CollectionAccess(CollectionAccessType.UpdatedContent)] public bool Add(T key) { Guard.IsNotNull(key); return InsertEntry(key, ReplaceBehaviour.Fail); } public void UnionWith(IEnumerable other) => throw new NotImplementedException(); public void IntersectWith(IEnumerable other) => throw new NotImplementedException(); public void ExceptWith(IEnumerable other) => throw new NotImplementedException(); public void SymmetricExceptWith(IEnumerable other) => throw new NotImplementedException(); public bool IsSubsetOf(IEnumerable other) => throw new NotImplementedException(); public bool IsSupersetOf(IEnumerable other) => throw new NotImplementedException(); public bool IsProperSupersetOf(IEnumerable other) => throw new NotImplementedException(); public bool IsProperSubsetOf(IEnumerable other) => throw new NotImplementedException(); public bool Overlaps(IEnumerable other) => throw new NotImplementedException(); public bool SetEquals(IEnumerable other) => throw new NotImplementedException(); [CollectionAccess(CollectionAccessType.ModifyExistingContent)] public bool Remove(T key) { if (!RemoveInternal(key)) return false; OnEntryRemoved(key); return true; } private void EmplaceWithTailsBackward(int entryIndex) { if (IsBucketEmpty(entryIndex)) Utility.Diagnostics.ThrowHelper.ThrowInvalidOperationException("Tried to emplace entry bucket"); while (true) { var tailIndex = HasTail(entryIndex) ? GetTailIndex(entryIndex) : -1; var entry = _buckets[entryIndex]; SetBucketEmpty(entryIndex); InsertEntry(entry!, ReplaceBehaviour.Throw, true); if (tailIndex < 0) return; entryIndex = tailIndex; } } private unsafe bool RemoveInternal(T key) { var bucketIndex = GetBucketIndex(key); while (true) { ref var entry = ref _buckets[bucketIndex]; if (_equalityComparer(key, entry)) { var tailIndex = HasTail(bucketIndex) ? GetTailIndex(bucketIndex) : -1; if (IsTail(entry!, bucketIndex)) SetParentTail(bucketIndex, Tails.SOLO); SetBucketEmpty(bucketIndex); if (tailIndex != -1) EmplaceWithTailsBackward(tailIndex); return true; } if (TryContinueWithTail(ref bucketIndex)) continue; return false; } } void ICollection.Add(T item) => Add(item); [CollectionAccess(CollectionAccessType.ModifyExistingContent)] public void Clear() { // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract if (_buckets is null) Initialize(); Array.Fill(_buckets, _defaultValue); _tails.Reset(); _count = 0; _version++; } [CollectionAccess(CollectionAccessType.Read)] public void CopyTo(T[] array, int arrayIndex) { var length = _buckets.Length; for (var i = 0; i < length; i++) { if (IsBucketEmpty(i)) continue; array[arrayIndex] = _buckets[i]!; arrayIndex++; } } private void OnEntryRemoved(T entry) { _version++; _count--; EntryRemoved?.Invoke(entry); } #region InterfaceMethods [CollectionAccess(CollectionAccessType.Read)] public Enumerator GetEnumerator() => new(this, Enumerator.KEY_VALUE_PAIR); IEnumerator IEnumerable.GetEnumerator() => new Enumerator(this, Enumerator.KEY_VALUE_PAIR); IEnumerator IEnumerable.GetEnumerator() => new Enumerator(this, Enumerator.KEY_VALUE_PAIR); void ICollection.CopyTo(Array array, int index) { if (array is T[] typedArray) { CopyTo(typedArray, index); return; } var length = _buckets.Length; for (var i = 0; i < length; i++) { if (IsBucketEmpty(i)) continue; ((IList)array)[index] = _buckets[i]; index++; } } #endregion #region enums private enum ReplaceBehaviour { Replace, Throw, Fail } #endregion #region Structs internal struct Tails(uint length) { public const uint EMPTY = 0u, SOLO = 1u; private NibbleArray _values = new(length); public uint Length => _values.Length; public uint this[uint index] { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _values[index]; set => _values[index] = value; } public void Reset() => _values.Clear(); //_values.Initialize(15u); [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool IsEmpty(int index) => _values[(uint)index] == EMPTY; [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool IsSolo(int index) => _values[(uint)index] == SOLO; [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool IsSoloOrEmpty(int index) => _values[(uint)index] <= 1u; public void SetSolo(int index) => _values[(uint)index] = SOLO; public void SetEmpty(int index) => _values[(uint)index] = EMPTY; } public record struct Enumerator : IEnumerator { private readonly FishSet _dictionary; private readonly int _version; private uint _index; private T? _current; internal const int DICT_ENTRY = 1; internal const int KEY_VALUE_PAIR = 2; internal Enumerator(FishSet dictionary, int getEnumeratorRetType) { _dictionary = dictionary; _version = dictionary._version; _index = 0u; _current = default; } public bool MoveNext() { if (_version != _dictionary._version) ThrowHelper.ThrowInvalidOperationException_InvalidOperation_EnumFailedVersion(); while (_index < (uint)_dictionary._buckets.Length) { if (_dictionary.IsBucketEmpty((int)_index++)) continue; _current = _dictionary._buckets[_index - 1]!; return true; } _index = (uint)_dictionary._buckets.Length + 1u; _current = default; return false; } [CollectionAccess(CollectionAccessType.Read)] public T Current => _current!; public void Dispose() { } object IEnumerator.Current { get { if (_index == 0u || _index == (uint)_dictionary._buckets.Length + 1u) ThrowHelper.ThrowInvalidOperationException_InvalidOperation_EnumOpCantHappen(); return _current!; } } void IEnumerator.Reset() { if (_version != _dictionary._version) ThrowHelper.ThrowInvalidOperationException_InvalidOperation_EnumFailedVersion(); _index = 0u; _current = default; } } #endregion private static class ThrowHelper { [DoesNotReturn] internal static void ThrowInvalidOperationException_InvalidOperation_EnumFailedVersion() => throw new InvalidOperationException("Collection was modified; enumeration operation may not execute."); [DoesNotReturn] internal static void ThrowInvalidOperationException_InvalidOperation_EnumOpCantHappen() => throw new InvalidOperationException("Enumeration has either not started or has already finished."); [DoesNotReturn] internal static void ThrowWrongKeyTypeArgumentException(object? key) { Guard.IsNotNull(key); throw new ArgumentException($"The value \"{key}\" is not of type \"{ typeof(T)}\" and cannot be used in this generic collection.", nameof(key)); } [DoesNotReturn] internal static bool ThrowAddingDuplicateWithKeyArgumentException(T key) { Guard.IsNotNull(key); ThrowAddingDuplicateWithKeyArgumentException((object)key); return true; } [DoesNotReturn] internal static unsafe void ThrowFailedToFindParentInvalidOperationException(FishSet fishTable, int childBucketIndex) { var entry = fishTable._buckets[childBucketIndex]; throw new InvalidOperationException($"Failed to find parent index in FishTable for key: '{ entry}', hashCode: '{fishTable._hashCodeGetter(entry)}', count: '{ fishTable._count}', bucket array length: '{fishTable._buckets.Length}', tailing entries: '{ fishTable.GetTailingEntriesCount()}'"); } [DoesNotReturn] private static void ThrowAddingDuplicateWithKeyArgumentException(object key) => throw new ArgumentException($"An item with the same key has already been added. Key: {key}"); } } #pragma warning restore CS8766, CS8767