Added 1.2 and 1.5 support, mod icon, DefaultValue/Key for FishSet/Table

This commit is contained in:
bbradson
2024-04-14 20:06:00 +02:00
parent c584f58dfd
commit 64152cef07
60 changed files with 643 additions and 251 deletions
+45 -4
View File
@@ -7,6 +7,9 @@
// 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;
@@ -14,7 +17,10 @@ using System.Runtime.InteropServices;
using FisheryLib.FunctionPointers;
using FisheryLib.Pools;
using FisheryLib.Utility;
using JetBrains.Annotations;
#if V1_2
using CollectionAccessType = nuget::JetBrains.Annotations.CollectionAccessType;
using CollectionAccessAttribute = nuget::JetBrains.Annotations.CollectionAccessAttribute;
#endif
namespace FisheryLib.Collections;
@@ -42,6 +48,8 @@ public class FishSet<T> : ISet<T>, IReadOnlyCollection<T>, ICollection
private float _maxLoadFactor = 0.5f;
private T _defaultValue = Internal.KeyUtility<T>.Default;
public int Version
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -65,6 +73,22 @@ public class FishSet<T> : ISet<T>, IReadOnlyCollection<T>, ICollection
public bool IsReadOnly => false;
/// <summary>
/// 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.
/// </summary>
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<T>?
EntryAdded,
EntryRemoved;
@@ -112,6 +136,19 @@ public class FishSet<T> : ISet<T>, IReadOnlyCollection<T>, ICollection
public FishSet(int minimumCapacity) => Initialize(minimumCapacity);
/// <summary>
/// Initialize a new FishSet
/// </summary>
/// <param name="minimumCapacity">The minimum capacity of the backing array</param>
/// <param name="defaultValue">
/// The default value for empty entries. Contains() returns true for this value when empty
/// </param>
public FishSet(int minimumCapacity, T defaultValue)
{
_defaultValue = defaultValue;
Initialize(minimumCapacity);
}
[MemberNotNull(nameof(_buckets))]
[MemberNotNull(nameof(_tails))]
private void Initialize(int minimumCapacity = 0)
@@ -120,6 +157,7 @@ public class FishSet<T> : ISet<T>, IReadOnlyCollection<T>, ICollection
// 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;
@@ -167,7 +205,7 @@ public class FishSet<T> : ISet<T>, IReadOnlyCollection<T>, ICollection
private void SetBucketEmpty(int index)
{
_buckets[index] = default;
_buckets[index] = _defaultValue;
_tails.SetEmpty(index);
}
@@ -356,7 +394,7 @@ public class FishSet<T> : ISet<T>, IReadOnlyCollection<T>, ICollection
{
ref var tailingBucket = ref _buckets[tailIndex];
tailingEntriesList.Add(tailingBucket!);
tailingBucket = default;
tailingBucket = _defaultValue;
if (HasTail(tailIndex))
{
@@ -409,6 +447,7 @@ public class FishSet<T> : ISet<T>, IReadOnlyCollection<T>, ICollection
_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;
@@ -579,7 +618,7 @@ public class FishSet<T> : ISet<T>, IReadOnlyCollection<T>, ICollection
if (_buckets is null)
Initialize();
Array.Clear(_buckets, 0, _buckets.Length);
Array.Fill(_buckets, Internal.KeyUtility<T>.Default);
_tails.Reset();
_count = 0;
@@ -660,6 +699,8 @@ public class FishSet<T> : ISet<T>, IReadOnlyCollection<T>, ICollection
private NibbleArray _values = new(length);
public uint Length => _values.Length;
public uint this[uint index]
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
+85 -10
View File
@@ -7,15 +7,22 @@
// 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;
using JetBrains.Annotations;
#if V1_2
using CollectionAccessType = nuget::JetBrains.Annotations.CollectionAccessType;
using CollectionAccessAttribute = nuget::JetBrains.Annotations.CollectionAccessAttribute;
#endif
namespace FisheryLib.Collections;
@@ -45,6 +52,8 @@ public class FishTable<TKey, TValue> : IDictionary<TKey, TValue>, IDictionary, I
_version;
private float _maxLoadFactor = 0.5f;
private Entry _defaultEntry = Entry.Default;
public KeyCollection Keys => new(this);
@@ -65,6 +74,15 @@ public class FishTable<TKey, TValue> : IDictionary<TKey, TValue>, IDictionary, I
public bool IsReadOnly => false;
/// <summary>
/// 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.
/// </summary>
public TKey DefaultKey => _defaultEntry.Key;
public unsafe uint SizeEstimate
=> GetType().ComputeManagedObjectSizeEstimate()
+ ((uint)sizeof(Entry) * (uint)_buckets.Length)
@@ -227,6 +245,19 @@ public class FishTable<TKey, TValue> : IDictionary<TKey, TValue>, IDictionary, I
public FishTable(int minimumCapacity) => Initialize(minimumCapacity);
/// <summary>
/// Initialize a new FishTable
/// </summary>
/// <param name="minimumCapacity">The minimum capacity of the backing array</param>
/// <param name="defaultKey">
/// The default key for empty entries. ContainsKey() returns true for this when empty
/// </param>
public FishTable(int minimumCapacity, TKey defaultKey)
{
_defaultEntry.Key = defaultKey;
Initialize(minimumCapacity);
}
[MemberNotNull(nameof(_buckets))]
[MemberNotNull(nameof(_tails))]
private void Initialize(int minimumCapacity = 0)
@@ -235,6 +266,7 @@ public class FishTable<TKey, TValue> : IDictionary<TKey, TValue>, IDictionary, I
// 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;
@@ -280,7 +312,7 @@ public class FishTable<TKey, TValue> : IDictionary<TKey, TValue>, IDictionary, I
private void SetBucketEmpty(int index)
{
_buckets[index] = default;
_buckets[index] = _defaultEntry;
_tails.SetEmpty(index);
}
@@ -400,8 +432,11 @@ public class FishTable<TKey, TValue> : IDictionary<TKey, TValue>, IDictionary, I
{
ref var bucket = ref buckets[bucketIndex];
if (!_keyEqualityComparer(bucket.Key, default))
if (!CompareBytes(bucket.Key, _defaultEntry.Key)
&& !_keyEqualityComparer(bucket.Key, _defaultEntry.Key))
{
ThrowHelper.ThrowEmptyBucketKeyInvalidOperationException(bucketIndex, bucket);
}
if (bucket.Value.Equals<TValue>(default))
return true;
@@ -412,6 +447,43 @@ public class FishTable<TKey, TValue> : IDictionary<TKey, TValue>, IDictionary, I
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)
@@ -498,7 +570,7 @@ public class FishTable<TKey, TValue> : IDictionary<TKey, TValue>, IDictionary, I
{
ref var tailingBucket = ref _buckets[tailIndex];
tailingEntriesList.Add(tailingBucket);
tailingBucket = default;
tailingBucket = _defaultEntry;
if (HasTail(tailIndex))
{
@@ -585,6 +657,7 @@ public class FishTable<TKey, TValue> : IDictionary<TKey, TValue>, IDictionary, I
_tails = new((uint)newSize);
_buckets = new Entry[newSize];
Array.Fill(_buckets, _defaultEntry);
_bucketBitShift = 32 - FishMath.TrailingZeroCount((uint)newSize);
_wrapAroundMask = _buckets.Length - 1;
@@ -1046,7 +1119,7 @@ public class FishTable<TKey, TValue> : IDictionary<TKey, TValue>, IDictionary, I
private void EmplaceWithTailsBackward(int entryIndex)
{
if (IsBucketEmpty(entryIndex))
Utility.Diagnostics.ThrowHelper.ThrowInvalidOperationException("Tried to emplace entry bucket");
Utility.Diagnostics.ThrowHelper.ThrowInvalidOperationException("Tried to emplace empty bucket");
while (true)
{
@@ -1225,7 +1298,7 @@ public class FishTable<TKey, TValue> : IDictionary<TKey, TValue>, IDictionary, I
if (_buckets is null)
Initialize();
Array.Clear(_buckets, 0, _buckets.Length);
Array.Fill(_buckets, _defaultEntry);
_tails.Reset();
_count = 0;
@@ -1360,6 +1433,8 @@ public class FishTable<TKey, TValue> : IDictionary<TKey, TValue>, IDictionary, I
.Append<TValue>(Value)
.Append(" }").ToString();
}
public static readonly Entry Default = new(KeyUtility<TKey>.Default, default);
}
internal struct Tails(uint length)
@@ -1688,11 +1763,11 @@ public class FishTable<TKey, TValue> : IDictionary<TKey, TValue>, IDictionary, I
internal static void ThrowKeyNotFoundException<T>(T key)
{
Guard.IsNotNull(key);
ThrowKeyNotFoundException((object)key);
ThrowKeyNotFoundException((object?)key);
}
[DoesNotReturn]
private static void ThrowKeyNotFoundException(object key)
private static void ThrowKeyNotFoundException(object? key)
=> throw new KeyNotFoundException($"The given key '{key}' was not present in the dictionary.");
[DoesNotReturn]
@@ -1715,7 +1790,7 @@ public class FishTable<TKey, TValue> : IDictionary<TKey, TValue>, IDictionary, I
internal static bool ThrowAddingDuplicateWithKeyArgumentException<T>(T key)
{
Guard.IsNotNull(key);
ThrowAddingDuplicateWithKeyArgumentException((object)key);
ThrowAddingDuplicateWithKeyArgumentException((object?)key);
return true;
}
@@ -1782,7 +1857,7 @@ public class FishTable<TKey, TValue> : IDictionary<TKey, TValue>, IDictionary, I
}
[DoesNotReturn]
private static void ThrowAddingDuplicateWithKeyArgumentException(object key)
private static void ThrowAddingDuplicateWithKeyArgumentException(object? key)
=> throw new ArgumentException($"An item with the same key has already been added. Key: {key}");
}
}
@@ -6,7 +6,6 @@
using System.Collections;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using JetBrains.Annotations;
namespace FisheryLib.Collections;
@@ -5,7 +5,6 @@
using System.Collections;
using System.Diagnostics.CodeAnalysis;
using JetBrains.Annotations;
namespace FisheryLib.Collections;
@@ -0,0 +1,165 @@
// Copyright (c) 2022 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/.
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Reflection.Emit;
using System.Runtime.CompilerServices;
using System.Runtime.Serialization;
namespace FisheryLib.Collections.Internal;
public static class KeyUtility<T>
{
public static readonly T Default = GetDefaultValue();
private static T GetDefaultValue()
{
var type = typeof(T);
var invalidKey = KeyUtility.GetDefaultValue(type);
if (!type.IsValueType)
return Unsafe.As<object?, T>(ref invalidKey);
return (T)invalidKey!;
}
}
public static class KeyUtility
{
public static readonly object InvalidObject = new();
public const ulong InvalidValue = 0x7F7F_7F7F_7F7F_7F7FUL;
public static readonly string InvalidString
= new(['\u7F7F', '\u7F7F', '\u7F7F', '\u7F7F', '\u7F7F', '\u7F7F', '\u7F7F', '\u7F7F']);
private static readonly ConcurrentDictionary<FieldInfo, Action<object>> _fieldSetters = [];
private static readonly ConcurrentDictionary<Type, object> _defaultValues = [];
private static readonly MethodInfo _genericMaxValueMethodDefinition
= methodof(GetMaxValue<ulong>).GetGenericMethodDefinition();
private static readonly FieldInfo _invalidFieldInfo = typeof(KeyUtility).GetField(nameof(InvalidObject));
public static object? GetDefaultValue(Type type)
{
if (type == typeof(string))
return InvalidString;
if (_defaultValues.TryGetValue(type, out var result))
return result;
if (!type.IsValueType)
{
return !type.IsAssignableTo(typeof(IEquatable<>), type)
&& type.GetMethod(nameof(object.Equals),
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, [typeof(object)],
Array.Empty<ParameterModifier>())?.DeclaringType
== typeof(object)
? InvalidObject
: null;
}
result = FormatterServices.GetUninitializedObject(type);
var fields = type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
for (var i = 0; i < fields.Length; i++)
{
var field = fields[i];
var fieldType = field.FieldType;
if (fieldType.IsValueType)
{
if (AccessTools.IsValue(fieldType))
field.SetValue(result, GetMaxValue(fieldType));
else
GetFieldInvalidator(field)(result);
}
else
{
if (fieldType != typeof(string))
{
if (!fieldType.IsAssignableTo(typeof(IEquatable<>), fieldType))
GetFieldInvalidator(field)(result);
else
field.SetValue(result, null);
}
else
{
field.SetValue(result, InvalidString);
}
}
if (Unsafe.As<object, ulong>(ref result) > 0xFFFF_FFFF_FFFF)
Log.Error($"Detected invalid pointer with a value > 48 bit\n{new StackTrace(true)}");
}
_defaultValues.TryAdd(type, result);
return result;
}
private static object GetMaxValue(Type type)
{
if (!AccessTools.IsValue(type))
{
return ThrowHelper.ThrowArgumentException<object>($"Invalid type passed to KeyUtility.GetMaxValue: '{
type.FullDescription()}'. Only primitive structs are supported.");
}
var result = _genericMaxValueMethodDefinition.MakeGenericMethod(type).Invoke(null, null);
_defaultValues.TryAdd(type, result);
return result;
}
private static object GetMaxValue<T>() where T : struct
{
var maxValue = InvalidValue;
return Unsafe.As<ulong, T>(ref maxValue)!;
}
private static Action<object> GetFieldInvalidator(FieldInfo fieldInfo)
{
if (_fieldSetters.TryGetValue(fieldInfo, out var result))
return result;
var method = new DynamicMethod($"FisheryLib><::>UnsafeSetter<::><{fieldInfo.FullDescription()}", typeof(void),
[typeof(object)], typeof(KeyUtility), true);
var ilGenerator = method.GetILGenerator();
var fieldType = fieldInfo.FieldType;
var declaringType = fieldInfo.DeclaringType;
ilGenerator.Emit(FishTranspiler.Argument(0));
if (declaringType?.IsValueType ?? false)
ilGenerator.Emit(FishTranspiler.UnboxAddress(declaringType));
if (!fieldType.IsValueType)
{
ilGenerator.Emit(FishTranspiler.Field(_invalidFieldInfo));
ilGenerator.Emit(FishTranspiler.Call(methodof(Unsafe.As<object>).GetGenericMethodDefinition()
.MakeGenericMethod(fieldType)));
}
else
{
ilGenerator.Emit(FishTranspiler.Token(fieldType));
ilGenerator.Emit(FishTranspiler.Call(Type.GetTypeFromHandle));
ilGenerator.Emit(FishTranspiler.Call(AccessTools.IsValue(fieldType)
// ReSharper disable once RedundantCast
? (Func<Type, object?>)GetMaxValue
: GetDefaultValue));
ilGenerator.Emit(FishTranspiler.UnboxValue(fieldType));
}
ilGenerator.Emit(FishTranspiler.StoreField(fieldInfo));
ilGenerator.Emit(FishTranspiler.Return);
result = (Action<object>)method.CreateDelegate(typeof(Action<object>));
_fieldSetters.TryAdd(fieldInfo, result);
return result;
}
}
@@ -4,7 +4,6 @@
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
using System.Runtime.CompilerServices;
using JetBrains.Annotations;
namespace FisheryLib.Collections;