// 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 System.Security; using FisheryLib.Collections.Internal; 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 FishTable : IDictionary, IDictionary, IReadOnlyDictionary { private const int FIBONACCI_HASH = -1640531527; // unchecked((int)(uint)Math.Round(uint.MaxValue / FishMath.PHI)); internal Entry[] _buckets; private unsafe delegate* _keyHashCodeByRefGetter = GetHashCodeByRef.Default; private unsafe delegate* _keyEqualityByRefComparer = EqualsByRef.Default; private int _bucketBitShift, _count; private unsafe delegate* _keyHashCodeGetter = GetHashCode.Default; private unsafe delegate* _keyEqualityComparer = Equals.Default; internal Tails _tails; private int _wrapAroundMask, _version; private float _maxLoadFactor = 0.5f; private Entry _defaultEntry = Entry.Default; public KeyCollection Keys => new(this); public ValueCollection Values => new(this); public int Version { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _version; set => _version = value; } public int Count { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _count; } public bool IsReadOnly => false; /// /// Empty FishTables have their entries set to a default value, causing fishTable.ContainsKey(DefaultKey) 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 FishTable 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 TKey DefaultKey => _defaultEntry.Key; public unsafe uint SizeEstimate => GetType().ComputeManagedObjectSizeEstimate() + ((uint)sizeof(Entry) * (uint)_buckets.Length) + ((_tails.Length + 1u) >> 1); public uint CollisionCount => (uint)GetTailingEntriesCount(); public event Action>? EntryAdded, EntryRemoved; public Func ValueInitializer { get; set; } = static _ => Reflection.New(); public delegate void EntryEventHandler(TKey key, ref TValue value); public event Action? KeyAdded { add => AddEvent(ref EntryAdded, value); remove => RemoveEvent(ref EntryAdded, value); } public event Action? KeyRemoved { add => AddEvent(ref EntryRemoved, value); remove => RemoveEvent(ref EntryRemoved, value); } public event Action? ValueAdded { add => AddEvent(ref EntryAdded, value); remove => RemoveEvent(ref EntryAdded, value); } public event Action? ValueRemoved { add => AddEvent(ref EntryRemoved, value); remove => RemoveEvent(ref EntryRemoved, value); } private static void AddEvent(ref Action>? @event, Action? value) where THandler : IEventHandler { if (value != null) @event += Reflection.New>(value).Invoke; } private static void RemoveEvent(ref Action>? @event, Action? value) { if (@event is null) return; var delegates = @event.GetInvocationList(); for (var i = delegates.Length; i-- > 0;) { if (delegates[i].Target is not IEventHandler handler || handler.Action != value) continue; @event -= (Action>)delegates[i]; return; } } private interface IEventHandler { public Action Action { get; } public void Invoke(KeyValuePair pair); } private sealed record KeyEventHandler(Action Action) : IEventHandler { public void Invoke(KeyValuePair pair) => Action(pair.Key); } private sealed record ValueEventHandler(Action Action) : IEventHandler { public void Invoke(KeyValuePair pair) => Action(pair.Value); } public float MaxLoadFactor { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _maxLoadFactor; set { Guard.IsInRange(value, 0f, 1f); _maxLoadFactor = value; } } ICollection IDictionary.Keys => Keys; ICollection IDictionary.Values => Values; ICollection IDictionary.Keys => Keys; ICollection IDictionary.Values => Values; bool IDictionary.IsFixedSize => false; object ICollection.SyncRoot => this; // matching System.Collections.Generic.Dictionary bool ICollection.IsSynchronized => false; IEnumerable IReadOnlyDictionary.Keys => Keys; IEnumerable IReadOnlyDictionary.Values => Values; object IDictionary.this[object key] { get => this[AssertKeyType(key)]!; set => this[AssertKeyType(key)] = AssertValueType(value); } private static TKey AssertKeyType(object key) { if (key is TKey tKey) return tKey; else if (key != null!) ThrowHelper.ThrowWrongKeyTypeArgumentException(key); return default!; } private static TValue AssertValueType(object? value) { if (value is TValue tValue) return tValue; else if (value != null!) ThrowHelper.ThrowWrongValueTypeArgumentException(value); return default!; } public unsafe TValue this[TKey key] { [CollectionAccess(CollectionAccessType.Read)] [MethodImpl(MethodImplOptions.AggressiveInlining)] get { var bucketIndex = GetBucketIndex(key); while (true) { ref var bucket = ref _buckets[bucketIndex]; if (_keyEqualityComparer(key, bucket.Key)) return bucket.Value!; ContinueWithTailOrThrow(ref bucketIndex, key); } } [CollectionAccess(CollectionAccessType.UpdatedContent)] set => InsertEntry(key, value, ReplaceBehaviour.Replace); } public FishTable() : this(0) { } public FishTable(int minimumCapacity) => Initialize(minimumCapacity); /// /// Initialize a new FishTable /// /// The minimum capacity of the backing array /// /// The default key for empty entries. ContainsKey() returns true for this when empty /// public FishTable(int minimumCapacity, TKey defaultKey) { _defaultEntry.Key = defaultKey; 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 Entry[minimumCapacity]; Array.Fill(_buckets, _defaultEntry); _tails = new((uint)minimumCapacity); _bucketBitShift = 32 - FishMath.TrailingZeroCount((uint)_buckets.Length); _wrapAroundMask = _buckets.Length - 1; } public FishTable(IEnumerable> entries) : this(0) => AddRange(entries); 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($"Tried computing jump distance from a tails value of {index}"); // => (int)(uint)Math.Round(index * FishMath.PHI); [MethodImpl(MethodImplOptions.AggressiveInlining)] private bool IsTail(int bucketIndex) => bucketIndex != GetBucketIndexForKeyAt(bucketIndex); [MethodImpl(MethodImplOptions.AggressiveInlining)] private bool IsTail(ref Entry entry, int bucketIndex) => bucketIndex != GetBucketIndex(entry.Key); [MethodImpl(MethodImplOptions.AggressiveInlining)] private bool HasTail(int entryIndex) => !_tails.IsSoloOrEmpty(entryIndex); private void SetEntryWithoutTail(int bucketIndex, in Entry entry) { _buckets[bucketIndex] = entry; _tails.SetSolo(bucketIndex); } private void SetBucketEmpty(int index) { _buckets[index] = _defaultEntry; _tails.SetEmpty(index); } private void SetEntryAsTail(int bucketIndex, in Entry entry, int parentIndex, uint offset) { SetEntryWithoutTail(bucketIndex, entry); _tails[(uint)parentIndex] = offset; } private void SetParentTail(int entryIndex, uint newTail) => _tails[(uint)GetParentBucketIndex(entryIndex)] = newTail; /// /// Returns true when adding a new entry, false for replacing an existing entry or failing to do so. Does not /// adjust Count, Version or invoke EntryAdded. /// private bool InsertEntry(TKey key, TValue? value, ReplaceBehaviour replaceBehaviour, bool shifting = false) => InsertEntry(new(key, value), replaceBehaviour, shifting); /// /// Returns true when adding a new entry, false for replacing an existing entry or failing to do so. Does not /// adjust Count, Version or invoke EntryAdded. /// private bool InsertEntry(in Entry entry, ReplaceBehaviour replaceBehaviour, bool shifting = false) { var addedNewEntry = InsertEntryInternal(entry, replaceBehaviour); if (shifting) return addedNewEntry; _version++; if (!addedNewEntry) return false; OnEntryAdded(entry.AsKeyValuePair()); return true; } private void OnEntryAdded(in KeyValuePair entry) { // _version++; handled separately _count++; EntryAdded?.Invoke(entry); } /// /// Returns true when adding a new entry, false for replacing an existing entry or failing to do so. Does not /// adjust Count, Version or invoke EntryAdded. /// private bool InsertEntryInternal(in Entry entry, ReplaceBehaviour replaceBehaviour) { StartOfMethod: var bucketIndex = GetBucketIndex(entry.Key); 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(ref 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; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private bool IsBucketEmpty(int bucketIndex) => IsBucketEmpty(bucketIndex, _tails, _buckets); [MethodImpl(MethodImplOptions.AggressiveInlining)] private bool IsBucketEmpty(int bucketIndex, Tails tails, Entry[] buckets) => tails.IsEmpty(bucketIndex) && VerifyAgainstNullBucket(bucketIndex, tails, buckets); private unsafe bool VerifyAgainstNullBucket(int bucketIndex, Tails tails, Entry[] buckets) { ref var bucket = ref buckets[bucketIndex]; if (!CompareBytes(bucket.Key, _defaultEntry.Key) && !_keyEqualityComparer(bucket.Key, _defaultEntry.Key)) { ThrowHelper.ThrowEmptyBucketKeyInvalidOperationException(bucketIndex, bucket); } if (bucket.Value.Equals(default)) return true; // GetReference methods can get here when using a default key and accessing the returned ref tails.SetSolo(bucketIndex); OnEntryAdded(bucket.AsKeyValuePair()); // technically late, but it's an edge case and there's return false; // no good workaround without performance hit } private static unsafe bool CompareBytes(TKey a, TKey b) { var size = sizeof(TKey); var iOffset = 0; var i = 0; do { var currentA = *((nint*)&a + iOffset); var currentB = *((nint*)&b + iOffset); if (size - i >= sizeof(nint)) { if (currentA != currentB) return false; iOffset++; i += sizeof(nint); } else { var remainingSize = size & sizeof(nint); i = 0; do { if ((byte)(currentA >> i) != (byte)(currentB >> i)) return false; } while (++i < remainingSize); break; } } while (i < size); return size != 0; } private bool TryFindTailIndexAndReplace(in Entry 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(in Entry entry, ReplaceBehaviour replaceBehaviour, ref Entry bucket) { if (!_keyEqualityComparer(bucket.Key, entry.Key)) return false; switch (replaceBehaviour) { case ReplaceBehaviour.Throw: return ThrowHelper.ThrowAddingDuplicateWithKeyArgumentException(entry.Key); case ReplaceBehaviour.Replace: bucket.Value = entry.Value; goto default; case ReplaceBehaviour.Return: default: return true; } } private bool InsertAsTail(in Entry 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 = _defaultEntry; if (HasTail(tailIndex)) { var nextTailIndex = GetTailIndex(tailIndex); _tails.SetEmpty(tailIndex); tailIndex = nextTailIndex; } else { _tails.SetEmpty(tailIndex); break; } } return tailingEntries; } // probably fails due to varying jump distances /*private void EmplaceWithTailsForward(in Entry insertedEntry, int entryIndex) { var replacementEntry = insertedEntry; while (true) { if (entryIndex < 0) { InsertEntry(replacementEntry, false, true); return; } var tailIndex = HasTail(entryIndex) ? GetTailIndex(entryIndex) : -1; var previousEntry = _buckets[entryIndex]; SetBucketEmpty(entryIndex); InsertEntry(replacementEntry, false, true); replacementEntry = previousEntry; entryIndex = tailIndex; } }*/ [MethodImpl(MethodImplOptions.NoInlining)] private void WarnForPossiblyExcessiveResizing(Entry entry) => Log.Warning($"FishTable is resizing from a large number of clashing hashCodes. Last inserted key: '{ entry.Key}', value: '{entry.Value}', 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; } public void EnsureCapacity(int minimumSize) { if (_buckets.Length >= minimumSize) return; Resize(FishMath.NextPowerOfTwo(minimumSize)); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private bool CheckResize() { if (_count <= (int)(_buckets.Length * _maxLoadFactor)) return false; Resize(); return true; } private void Resize() => Resize(_buckets.Length << 1); private void Resize(int newSize) { var oldBuckets = _buckets; var oldTails = _tails; _tails = new((uint)newSize); _buckets = new Entry[newSize]; Array.Fill(_buckets, _defaultEntry); _bucketBitShift = 32 - FishMath.TrailingZeroCount((uint)newSize); _wrapAroundMask = _buckets.Length - 1; for (var i = 0; i < oldBuckets.Length; i++) { if (!IsBucketEmpty(i, oldTails, oldBuckets)) InsertEntry(oldBuckets[i], ReplaceBehaviour.Throw, true); } } #region GetterMethods [CollectionAccess(CollectionAccessType.Read)] public unsafe bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value) { var bucketIndex = GetBucketIndex(key); while (true) { ref var bucket = ref _buckets[bucketIndex]; if (_keyEqualityComparer(key, bucket.Key)) { value = bucket.Value!; return true; } if (TryContinueWithTail(ref bucketIndex)) continue; value = default; return false; } } [CollectionAccess(CollectionAccessType.Read)] public unsafe TValue? TryGetValue(TKey key) { var bucketIndex = GetBucketIndex(key); while (true) { ref var bucket = ref _buckets[bucketIndex]; if (_keyEqualityComparer(key, bucket.Key)) return bucket.Value; if (!TryContinueWithTail(ref bucketIndex)) return default; } } [CollectionAccess(CollectionAccessType.Read | CollectionAccessType.UpdatedContent)] public unsafe bool TryGetOrAddValue(TKey key, out TValue value) { var result = true; StartOfLookup: var bucketIndex = GetBucketIndex(key); while (true) { ref var bucket = ref _buckets[bucketIndex]; if (_keyEqualityComparer(key, bucket.Key)) { value = bucket.Value!; return result; } if (ContinueWithTailOrAddNew(ref bucketIndex, key)) continue; result = false; goto StartOfLookup; } } [CollectionAccess(CollectionAccessType.Read | CollectionAccessType.UpdatedContent)] public unsafe TValue GetOrAdd(TKey key) { StartOfMethod: var bucketIndex = GetBucketIndex(key); while (true) { ref var bucket = ref _buckets[bucketIndex]; if (_keyEqualityComparer(key, bucket.Key)) return bucket.Value!; if (!ContinueWithTailOrAddNew(ref bucketIndex, key)) goto StartOfMethod; } } [CollectionAccess(CollectionAccessType.Read | CollectionAccessType.UpdatedContent)] public unsafe TValue GetOrAdd(ref TKey key) { StartOfMethod: var bucketIndex = GetBucketIndex(ref key); while (true) { ref var bucket = ref _buckets[bucketIndex]; if (_keyEqualityByRefComparer(ref key, ref bucket.Key)) return bucket.Value!; if (!ContinueWithTailOrAddNew(ref bucketIndex, ref key)) goto StartOfMethod; } } /// /// Returns a reference to a value field of an existing entry if one exists, otherwise to a new entry. This /// reference is only valid until the next time the collection gets modified. An invalid reference gives undefined /// behaviour and does not throw. /// [CollectionAccess(CollectionAccessType.Read | CollectionAccessType.UpdatedContent)] public unsafe ref TValue GetOrAddReference(TKey key) { StartOfMethod: var bucketIndex = GetBucketIndex(key); while (true) { ref var bucket = ref _buckets[bucketIndex]; if (_keyEqualityComparer(key, bucket.Key)) return ref bucket.Value!; if (!ContinueWithTailOrAddNew(ref bucketIndex, key)) goto StartOfMethod; } } /// /// Returns a reference to a value field of an existing entry if one exists, otherwise to a new entry. This /// reference is only valid until the next time the collection gets modified. An invalid reference gives undefined /// behaviour and does not throw. /// [CollectionAccess(CollectionAccessType.Read | CollectionAccessType.UpdatedContent)] public unsafe ref TValue GetOrAddReference(ref TKey key) { StartOfMethod: var bucketIndex = GetBucketIndex(ref key); while (true) { ref var bucket = ref _buckets[bucketIndex]; if (_keyEqualityByRefComparer(ref key, ref bucket.Key)) return ref bucket.Value!; if (!ContinueWithTailOrAddNew(ref bucketIndex, ref key)) goto StartOfMethod; } } /// /// Returns a reference to a value field of an existing entry if one exists, otherwise throws a /// KeyNotFoundException. This reference is only valid until the next time the collection gets modified. An invalid /// reference gives undefined behaviour and does not throw. /// [CollectionAccess(CollectionAccessType.Read | CollectionAccessType.ModifyExistingContent)] public unsafe ref TValue GetReference(TKey key) { var bucketIndex = GetBucketIndex(key); while (true) { ref var bucket = ref _buckets[bucketIndex]; if (_keyEqualityComparer(key, bucket.Key)) return ref bucket.Value!; ContinueWithTailOrThrow(ref bucketIndex, key); } } /// /// Returns a reference to a value field of an existing entry if one exists, otherwise throws a /// KeyNotFoundException. This reference is only valid until the next time the collection gets modified. An invalid /// reference gives undefined behaviour and does not throw. /// [CollectionAccess(CollectionAccessType.Read | CollectionAccessType.ModifyExistingContent)] public unsafe ref TValue GetReference(ref TKey key) { var bucketIndex = GetBucketIndex(ref key); while (true) { ref var bucket = ref _buckets[bucketIndex]; if (_keyEqualityByRefComparer(ref key, ref bucket.Key)) return ref bucket.Value!; ContinueWithTailOrThrow(ref bucketIndex, ref key); } } /// /// Returns a reference to a value field of an existing entry if one exists, otherwise an Unsafe.NullRef{TValue}. /// Unsafe.IsNullRef should be used to check for Unsafe.NullRef before assigning to a regular var or field without /// ref. Regular null checks throw. The returned reference is only valid until the next time the collection gets /// modified. An invalid reference gives undefined behaviour and does not throw. /// /// ref TValue or Unsafe.NullRef{TValue} [CollectionAccess(CollectionAccessType.Read | CollectionAccessType.ModifyExistingContent)] public unsafe ref TValue? TryGetReferenceUnsafe(TKey key) { var bucketIndex = GetBucketIndex(key); while (true) { ref var bucket = ref _buckets[bucketIndex]; if (_keyEqualityComparer(key, bucket.Key)) return ref bucket.Value; if (!TryContinueWithTail(ref bucketIndex)) return ref Unsafe.NullRef(); } } /// /// Returns a reference to a value field of an existing entry if one exists, otherwise an Unsafe.NullRef{TValue}. /// Unsafe.IsNullRef should be used to check for Unsafe.NullRef before assigning to a regular var or field without /// ref. Regular null checks throw. The returned reference is only valid until the next time the collection gets /// modified. An invalid reference gives undefined behaviour and does not throw. /// /// ref TValue or Unsafe.NullRef{TValue} [CollectionAccess(CollectionAccessType.Read | CollectionAccessType.ModifyExistingContent)] public unsafe ref TValue? TryGetReferenceUnsafe(ref TKey key) { var bucketIndex = GetBucketIndex(ref key); while (true) { ref var bucket = ref _buckets[bucketIndex]; if (_keyEqualityByRefComparer(ref key, ref bucket.Key)) return ref bucket.Value; if (!TryContinueWithTail(ref bucketIndex)) return ref Unsafe.NullRef(); } } #endregion [CollectionAccess(CollectionAccessType.Read)] public unsafe bool ContainsKey(TKey key) { var bucketIndex = GetBucketIndex(key); while (true) { if (_keyEqualityComparer(key, _buckets[bucketIndex].Key)) return true; if (!TryContinueWithTail(ref bucketIndex)) return false; } } [CollectionAccess(CollectionAccessType.Read)] public unsafe bool ContainsValue(TValue value) { var equalityComparer = Equals.Default; var length = _buckets.Length; for (var i = 0; i < length; i++) { if (!IsBucketEmpty(i) && equalityComparer(_buckets[i].Value!, value)) return true; } return false; } #region GetBucketIndexMethods [MethodImpl(MethodImplOptions.AggressiveInlining)] private unsafe int GetBucketIndex(ref TKey key) => GetBucketIndex(_keyHashCodeByRefGetter(ref key)); [MethodImpl(MethodImplOptions.AggressiveInlining)] private unsafe int GetBucketIndex(TKey key) => GetBucketIndex(_keyHashCodeGetter(key)); [MethodImpl(MethodImplOptions.AggressiveInlining)] private int GetBucketIndex(int hashCode) => (hashCode * FIBONACCI_HASH) >>> _bucketBitShift; [MethodImpl(MethodImplOptions.AggressiveInlining)] private int GetBucketIndexForKeyAt(int index) => GetBucketIndex(_buckets[index].Key); private int GetParentBucketIndex(int childBucketIndex, bool throwOnFailure = true) { var ancestorIndex = GetBucketIndexForKeyAt(childBucketIndex); var i = 0; while (true) { if (i++ > 32 || !HasTail(ancestorIndex)) { if (throwOnFailure) ThrowHelper.ThrowFailedToFindParentInvalidOperationException(this, childBucketIndex); else return -1; } var nextIndex = GetTailIndex(ancestorIndex); if (nextIndex == childBucketIndex) break; ancestorIndex = nextIndex; } return ancestorIndex; } private bool HasParentBucket(int childBucketIndex) => GetParentBucketIndex(childBucketIndex, false) >= 0; #endregion #region ContinueWithTailMethods /// /// Tries setting the index to the tail's index and returns true on success. If none exists, inserts a new entry /// and returns false /// [MethodImpl(MethodImplOptions.NoInlining)] private bool ContinueWithTailOrAddNew(ref int entryIndex, TKey key) { if (TryContinueWithTail(ref entryIndex)) return true; this[key] = ValueInitializer(key); return false; } /// /// Tries setting the index to the tail's index and returns true on success. If none exists, inserts a new entry /// and returns false /// [MethodImpl(MethodImplOptions.NoInlining)] private bool ContinueWithTailOrAddNew(ref int entryIndex, ref TKey key) { if (TryContinueWithTail(ref entryIndex)) return true; this[key] = ValueInitializer(key); return false; } /// /// Tries setting the index to the tail's index. If none exists, throws instead /// [MethodImpl(MethodImplOptions.NoInlining)] private void ContinueWithTailOrThrow(ref int entryIndex, TKey key) { if (!TryContinueWithTail(ref entryIndex)) ThrowHelper.ThrowKeyNotFoundException(key); } /// /// Tries setting the index to the tail's index. If none exists, throws instead /// [MethodImpl(MethodImplOptions.NoInlining)] private void ContinueWithTailOrThrow(ref int entryIndex, ref TKey key) { if (!TryContinueWithTail(ref entryIndex)) ThrowHelper.ThrowKeyNotFoundException(key); } /// /// 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; } #endregion [CollectionAccess(CollectionAccessType.UpdatedContent)] public void Add(TKey key, TValue value) { Guard.IsNotNull(key); InsertEntry(key, value, ReplaceBehaviour.Throw); } [CollectionAccess(CollectionAccessType.Read | CollectionAccessType.UpdatedContent)] public bool TryAdd(TKey key, TValue value) { Guard.IsNotNull(key); return InsertEntry(key, value, ReplaceBehaviour.Return); } [CollectionAccess(CollectionAccessType.ModifyExistingContent)] public bool Remove(TKey key) { if (!RemoveInternal(key, out var removedEntry)) return false; OnEntryRemoved(removedEntry.AsKeyValuePair()); return true; } [CollectionAccess(CollectionAccessType.ModifyExistingContent)] public bool Remove(TKey key, out TValue? value) { if (!RemoveInternal(key, out var removedEntry)) { value = default; return false; } value = removedEntry.Value!; OnEntryRemoved(removedEntry.AsKeyValuePair()); return true; } [CollectionAccess(CollectionAccessType.ModifyExistingContent)] public int RemoveWhere(Predicate> predicate) { Guard.IsNotNull(predicate); var removedCount = 0; var length = _buckets.Length; for (var i = 0; i < length; i++) { if (IsBucketEmpty(i)) continue; var entry = _buckets[i]; if (!predicate(entry.AsKeyValuePair())) continue; if (!Remove(entry.Key)) continue; removedCount++; i--; // extra check in case of emplacement after removal } return removedCount; } private void EmplaceWithTailsBackward(int entryIndex) { if (IsBucketEmpty(entryIndex)) Utility.Diagnostics.ThrowHelper.ThrowInvalidOperationException("Tried to emplace empty 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(TKey key, out Entry removedEntry, bool checkValue = false, TValue? value = default) { var bucketIndex = GetBucketIndex(key); while (true) { ref var bucket = ref _buckets[bucketIndex]; if (_keyEqualityComparer(key, bucket.Key)) { if (checkValue && !value.Equals(bucket.Value)) goto OnFailure; removedEntry = bucket; var tailIndex = HasTail(bucketIndex) ? GetTailIndex(bucketIndex) : -1; if (IsTail(ref bucket, bucketIndex)) SetParentTail(bucketIndex, Tails.SOLO); SetBucketEmpty(bucketIndex); if (tailIndex != -1) EmplaceWithTailsBackward(tailIndex); return true; } if (TryContinueWithTail(ref bucketIndex)) continue; OnFailure: Unsafe.SkipInit(out removedEntry); return false; } } [CollectionAccess(CollectionAccessType.UpdatedContent)] public void Add(KeyValuePair item) => Add(item.Key, item.Value); [CollectionAccess(CollectionAccessType.UpdatedContent)] public void AddRange(IList> range) { Guard.IsNotNull(range); EnsureCapacity((int)(range.Count * 1.5f)); for (var i = range.Count; i-- > 0;) Add(range[i]); } [CollectionAccess(CollectionAccessType.UpdatedContent)] public void AddRange(IEnumerable> range) { Guard.IsNotNull(range); if (range is IList> iList) AddRange(iList); else AddRangeFromEnumerable(range); } private void AddRangeFromEnumerable(IEnumerable> range) { Guard.IsNotNull(range); foreach (var item in range) Add(item); } [CollectionAccess(CollectionAccessType.UpdatedContent)] public void AddRange(IList range, Func keySelector, Func valueSelector) { Guard.IsNotNull(range); Guard.IsNotNull(keySelector); Guard.IsNotNull(valueSelector); EnsureCapacity((int)(range.Count * 1.5f)); for (var i = range.Count; i-- > 0;) { var item = range[i]; Add(keySelector(item), valueSelector(item)); } } [CollectionAccess(CollectionAccessType.UpdatedContent)] public void AddRange(IEnumerable range, Func keySelector, Func valueSelector) { Guard.IsNotNull(range); Guard.IsNotNull(keySelector); Guard.IsNotNull(valueSelector); if (range is IList iList) AddRange(iList, keySelector, valueSelector); else AddRangeFromEnumerable(range, keySelector, valueSelector); } private void AddRangeFromEnumerable(IEnumerable range, Func keySelector, Func valueSelector) { Guard.IsNotNull(range); Guard.IsNotNull(keySelector); Guard.IsNotNull(valueSelector); foreach (var item in range) Add(keySelector(item), valueSelector(item)); } [CollectionAccess(CollectionAccessType.UpdatedContent)] public void AddRange(IList keys, IList values) { Guard.IsNotNull(keys); Guard.IsNotNull(values); var count = keys.Count; if (count != values.Count) ThrowHelper.ThrowInvalidKeysValuesSizeArgumentException(); EnsureCapacity((int)(count * 1.5f)); for (var i = count; i-- > 0;) Add(keys[i], values[i]); } [CollectionAccess(CollectionAccessType.UpdatedContent)] public void AddRange(IEnumerable keys, IEnumerable values) { Guard.IsNotNull(keys); Guard.IsNotNull(values); if (keys is IList keysList && values is IList valuesList) AddRange(keysList, valuesList); else AddRangeFromEnumerable(keys, values); } private void AddRangeFromEnumerable(IEnumerable keys, IEnumerable values) { Guard.IsNotNull(keys); Guard.IsNotNull(values); using var keyEnumerator = keys.GetEnumerator(); using var valueEnumerator = values.GetEnumerator(); while (keyEnumerator.MoveNext()) { if (!valueEnumerator.MoveNext()) ThrowHelper.ThrowInvalidKeysValuesSizeArgumentException(); Add(keyEnumerator.Current, valueEnumerator.Current); } } [CollectionAccess(CollectionAccessType.ModifyExistingContent)] public void Clear() { // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract if (_buckets is null) Initialize(); Array.Fill(_buckets, _defaultEntry); _tails.Reset(); _count = 0; _version++; } [CollectionAccess(CollectionAccessType.Read)] public bool Contains(KeyValuePair item) => TryGetValue(item.Key, out var value) && value.Equals(item.Value); [CollectionAccess(CollectionAccessType.Read)] public void CopyTo(KeyValuePair[] array, int arrayIndex) { var length = _buckets.Length; for (var i = 0; i < length; i++) { if (IsBucketEmpty(i)) continue; array[arrayIndex] = _buckets[i].AsKeyValuePair()!; arrayIndex++; } } [CollectionAccess(CollectionAccessType.ModifyExistingContent)] public bool Remove(KeyValuePair item) { if (!RemoveInternal(item.Key, out _, true, item.Value)) return false; OnEntryRemoved(item); return true; } private void OnEntryRemoved(in KeyValuePair 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); bool IDictionary.Contains(object key) { Guard.IsNotNull(key); return key is TKey tKey && ContainsKey(tKey); } void IDictionary.Add(object key, object value) => Add(AssertKeyType(key), AssertValueType(value)); IDictionaryEnumerator IDictionary.GetEnumerator() => new Enumerator(this, Enumerator.DICT_ENTRY); void IDictionary.Remove(object key) { Guard.IsNotNull(key); if (key is TKey tKey) Remove(tKey); } void ICollection.CopyTo(Array array, int index) { if (array is KeyValuePair[] 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].AsKeyValuePair(); index++; } } #endregion #region enums private enum ReplaceBehaviour { Replace, Throw, Return } #endregion #region Structs // [StructLayout(LayoutKind.Sequential, Pack = 1)] // matches KeyValuePair and gets reinterpret cast into that in the Enumerator internal struct Entry(TKey key, TValue? value) { public TKey Key = key; public TValue? Value = value; [Pure] [UnscopedRef] [MethodImpl(MethodImplOptions.AggressiveInlining)] [SecuritySafeCritical] public ref KeyValuePair AsKeyValuePair() => ref Unsafe.As>(ref this); public override string ToString() { using var stringBuilder = new PooledStringBuilder(42); return stringBuilder .Append("FishTable<") .Append(typeof(TKey)) .Append(", ") .Append(typeof(TValue)) .Append(">.Entry { Key = ") .Append(Key) .Append(", Value = ") .Append(Value) .Append(" }").ToString(); } public static readonly Entry Default = new(KeyUtility.Default, default); } 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>, IDictionaryEnumerator { private readonly FishTable _dictionary; private readonly int _version; private uint _index; private KeyValuePair _current; private readonly int _getEnumeratorRetType; // What should Enumerator.Current return? internal const int DICT_ENTRY = 1; internal const int KEY_VALUE_PAIR = 2; internal Enumerator(FishTable dictionary, int getEnumeratorRetType) { _dictionary = dictionary; _version = dictionary._version; _index = 0u; _getEnumeratorRetType = getEnumeratorRetType; _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].AsKeyValuePair()!; return true; } _index = (uint)_dictionary._buckets.Length + 1u; _current = default; return false; } [CollectionAccess(CollectionAccessType.Read)] public KeyValuePair Current => _current; public void Dispose() { } object IEnumerator.Current { get { if (_index == 0u || _index == (uint)_dictionary._buckets.Length + 1u) ThrowHelper.ThrowInvalidOperationException_InvalidOperation_EnumOpCantHappen(); return _getEnumeratorRetType == DICT_ENTRY ? new DictionaryEntry(_current.Key!, _current.Value) : _current; } } void IEnumerator.Reset() { if (_version != _dictionary._version) ThrowHelper.ThrowInvalidOperationException_InvalidOperation_EnumFailedVersion(); _index = 0u; _current = default; } DictionaryEntry IDictionaryEnumerator.Entry { get { if (_index == 0u || _index == (uint)_dictionary._buckets.Length + 1u) ThrowHelper.ThrowInvalidOperationException_InvalidOperation_EnumOpCantHappen(); return new(_current.Key!, _current.Value); } } object? IDictionaryEnumerator.Key { get { if (_index == 0u || _index == (uint)_dictionary._buckets.Length + 1u) ThrowHelper.ThrowInvalidOperationException_InvalidOperation_EnumOpCantHappen(); return _current.Key; } } object? IDictionaryEnumerator.Value { get { if (_index == 0u || _index == (uint)_dictionary._buckets.Length + 1u) ThrowHelper.ThrowInvalidOperationException_InvalidOperation_EnumOpCantHappen(); return _current.Value; } } } [PublicAPI] public readonly record struct KeyCollection(FishTable Parent) : ICollection, ICollection { public IEnumerator GetEnumerator() { foreach (var entry in Parent) yield return entry.Key; } IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); public bool Contains(TKey item) => Parent.ContainsKey(item); public void CopyTo(TKey[] array, int arrayIndex) { foreach (var entry in Parent) array[arrayIndex++] = entry.Key; } public void CopyTo(Array array, int index) { if (array is TKey[] typedArray) { CopyTo(typedArray, index); } else { foreach (var entry in Parent) array.SetValue(entry.Key, index++); } } public bool Remove(TKey item) => Parent.Remove(item); public int RemoveWhere(Predicate predicate) { Guard.IsNotNull(predicate); var removedCount = 0; var buckets = Parent._buckets; var tails = Parent._tails; var length = buckets.Length; for (var i = 0; i < length; i++) { if (Parent.IsBucketEmpty(i, tails, buckets)) continue; var entry = buckets[i]; if (!predicate(entry.Key)) continue; if (!Parent.Remove(entry.Key)) continue; removedCount++; i--; // extra check in case of emplacement after removal } return removedCount; } public int Count => Parent.Count; public bool IsReadOnly => true; void ICollection.Add(TKey item) => throw new NotSupportedException(); void ICollection.Clear() => throw new NotSupportedException(); bool ICollection.IsSynchronized => false; object ICollection.SyncRoot => throw new NotSupportedException(); } [PublicAPI] public readonly record struct ValueCollection(FishTable Parent) : ICollection, ICollection { public IEnumerator GetEnumerator() { foreach (var entry in Parent) yield return entry.Value; } IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); public bool Contains(TValue item) => Parent.ContainsValue(item); public void CopyTo(TValue[] array, int arrayIndex) { foreach (var entry in Parent) array[arrayIndex++] = entry.Value; } public void CopyTo(Array array, int index) { if (array is TValue[] typedArray) { CopyTo(typedArray, index); } else { foreach (var entry in Parent) array.SetValue(entry.Value, index++); } } public unsafe bool Remove(TValue item) { var buckets = Parent._buckets; var tails = Parent._tails; var equalityComparer = Equals.Default; var length = buckets.Length; for (var i = 0; i < length; i++) { if (Parent.IsBucketEmpty(i, tails, buckets)) continue; var entry = buckets[i]; if (!equalityComparer(entry.Value!, item)) continue; if (Parent.Remove(entry.Key)) return true; } return false; } public int RemoveWhere(Predicate predicate) { Guard.IsNotNull(predicate); var removedCount = 0; var buckets = Parent._buckets; var tails = Parent._tails; var length = buckets.Length; for (var i = 0; i < length; i++) { if (Parent.IsBucketEmpty(i, tails, buckets)) continue; var entry = buckets[i]; if (!predicate(entry.Value!)) continue; if (!Parent.Remove(entry.Key)) continue; removedCount++; i--; // extra check in case of emplacement after removal } return removedCount; } public int Count => Parent.Count; public bool IsReadOnly => true; void ICollection.Add(TValue item) => throw new NotSupportedException(); void ICollection.Clear() => throw new NotSupportedException(); bool ICollection.IsSynchronized => false; object ICollection.SyncRoot => throw new NotSupportedException(); } #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 ThrowKeyNotFoundException(T key) { Guard.IsNotNull(key); ThrowKeyNotFoundException((object?)key); } [DoesNotReturn] private static void ThrowKeyNotFoundException(object? key) => throw new KeyNotFoundException($"The given key '{key}' was not present in the dictionary."); [DoesNotReturn] internal static void ThrowWrongValueTypeArgumentException(object value) => throw new ArgumentException( $"The value \"{value}\" is not of type \"{ typeof(TValue)}\" and cannot be used in this generic collection.", nameof(value)); [DoesNotReturn] internal static void ThrowWrongKeyTypeArgumentException(object? key) { Guard.IsNotNull(key); throw new ArgumentException($"The value \"{key}\" is not of type \"{ typeof(TKey)}\" 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 void ThrowEmptyBucketKeyInvalidOperationException(int bucketIndex, Entry bucket) => throw new InvalidOperationException( $"Bucket marked as empty had non-default key. This should never happen. Index: '{ bucketIndex}', key: '{bucket.Key}', value: '{bucket.Value}'"); [DoesNotReturn] internal static void ThrowInvalidKeysValuesSizeArgumentException() => Utility.Diagnostics.ThrowHelper.ThrowArgumentException( "Input collections have different sizes. AddRange expects a matching Count for keys and values"); [DoesNotReturn] internal static unsafe void ThrowFailedToFindParentInvalidOperationException(FishTable fishTable, int childBucketIndex) { var entry = fishTable._buckets[childBucketIndex]; using var stringBuilder = new PooledStringBuilder(227); stringBuilder .Append("Failed to find parent index in FishTable<") .Append(typeof(TKey)) .Append(", ") .Append(typeof(TValue)) .Append("> for key: '") .Append(entry.Key) .Append("', hashCode: '") .Append(fishTable._keyHashCodeGetter(entry.Key)) .Append("', value: '") .Append(entry.Value) .Append("', count: '") .Append(fishTable._count) .Append("', bucket array length: '") .Append(fishTable._buckets.Length) .Append("', total tailing entries count: '") .Append(fishTable.GetTailingEntriesCount()) .Append("', known chain of tails:"); var ancestorIndex = fishTable.GetBucketIndexForKeyAt(childBucketIndex); var i = 0; while (true) { var tailEntry = fishTable._buckets[ancestorIndex]; stringBuilder.Append("\n{ index: '") .Append(ancestorIndex) .Append("' key: '") .Append(tailEntry.Key) .Append("', hashCode: '") .Append(fishTable._keyHashCodeGetter(tailEntry.Key)) .Append("', value: '") .Append(tailEntry.Value) .Append(fishTable._tails.IsEmpty(ancestorIndex) ? " (empty) }" : " }"); if (i++ > 32 || !fishTable.HasTail(ancestorIndex)) break; ancestorIndex = fishTable.GetTailIndex(ancestorIndex); } throw new InvalidOperationException(stringBuilder.ToString()); } [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