Refactor IdentityMap to be generic over key and value types

IdentityMap is now fully generic as IdentityMap<TKey, TValue>, enabling type-safe value storage and improved flexibility. All internal logic and method signatures are updated to use TKey and TValue. The small int optimization for value storage is removed, and the _useSmallInt flag is disabled by default. Legacy IIdentityMap code is deleted. TypeMetadataWrapper is updated to use the new generic IdentityMap signatures. This refactor improves type safety, eliminates boxing, and prepares the code for value-type scenarios.
This commit is contained in:
Loretta 2026-02-01 10:02:48 +01:00
parent 056a66d713
commit 1c41eba96e
2 changed files with 94 additions and 326 deletions

View File

@ -43,35 +43,35 @@ public interface IIdentityMap
/// - Large keys: custom hash table with chaining /// - Large keys: custom hash table with chaining
/// No Dictionary overhead, no per-entry allocation. /// No Dictionary overhead, no per-entry allocation.
/// </summary> /// </summary>
/// <typeparam name="TId">The ID type (int, long, Guid, string)</typeparam> /// <typeparam name="TKey">The key type (int, long, Guid, string)</typeparam>
public sealed class IdentityMap<TId> : IIdentityMap where TId : notnull /// <typeparam name="TValue">The value type</typeparam>
public sealed class IdentityMap<TKey, TValue> : IIdentityMap where TKey : notnull
{ {
private const bool _useSmallInt = true; private const bool _useSmallInt = false;
// Small int optimization (TId = int only, 0-4095), 512 bytes (fits in L1 cache!) // Small int optimization (TKey = int only, 0-4095), 512 bytes (fits in L1 cache!)
private const int SmallBitmapSize = 64; private const int SmallBitmapSize = 64;
private const int SmallSize = 4096; private const int SmallSize = 4096;
// Small int optimization (TId = int only, 0-65535) // Small int optimization (TKey = int only, 0-65535)
//private const int SmallBitmapSize = 1024; //private const int SmallBitmapSize = 1024;
//private const int SmallSize = SmallBitmapSize * 64; //private const int SmallSize = SmallBitmapSize * 64;
// Slot for hash table entries (generation needed for hash table validity) // Slot for hash table entries (generation needed for hash table validity)
private struct HashSlot private struct HashSlot
{ {
public object? Value; public TValue Value;
public int Next; // next slot index in chain (-1 = end) public int Next; // next slot index in chain (-1 = end)
} }
// Small int storage - SIMPLE: bitmap for "seen?" + direct object[] for values // Small int storage - SIMPLE: bitmap for "seen?" + direct TValue[] for values
// Bitmap protects values - no generation needed, no value array clearing on Reset! // Bitmap protects values - no generation needed, no value array clearing on Reset!
private ulong[]? _smallBitmap; // Cache-friendly "seen?" check (512 bytes, cleared on Reset) private ulong[]? _smallBitmap; // Cache-friendly "seen?" check (512 bytes, cleared on Reset)
private object?[]? _smallValues; // Direct value storage (NOT cleared on Reset - bitmap protects)
// Hash table storage (for large ints and other types) // Hash table storage (for large ints and other types)
private int[]? _buckets; // bucket index → first entry index private int[]? _buckets; // bucket index → first entry index
private HashSlot[]? _entries; // hash table entries private HashSlot[]? _entries; // hash table entries
private TId[]? _keys; // keys for equality check private TKey[]? _keys; // keys for equality check
private int _count; // number of entries in hash table private int _count; // number of entries in hash table
private int _bucketsLength; // actual rented length (for modulo) private int _bucketsLength; // actual rented length (for modulo)
private int _entriesLength; // actual capacity private int _entriesLength; // actual capacity
@ -79,10 +79,11 @@ public sealed class IdentityMap<TId> : IIdentityMap where TId : notnull
private const int InitialHashCapacity = 16; private const int InitialHashCapacity = 16;
// Type checks (JIT eliminates these at compile time) // Type checks (JIT eliminates these at compile time)
private static readonly bool IsInt32 = typeof(TId) == typeof(int); private static readonly bool IsInt32 = typeof(TKey) == typeof(int);
private static readonly bool IsInt64 = typeof(TId) == typeof(long); private static readonly bool IsInt64 = typeof(TKey) == typeof(long);
private static readonly bool IsGuid = typeof(TId) == typeof(Guid); private static readonly bool IsGuid = typeof(TKey) == typeof(Guid);
private static readonly bool IsString = typeof(TId) == typeof(string); private static readonly bool IsString = typeof(TKey) == typeof(string);
private static readonly bool IsValueTypeValue = typeof(TValue).IsValueType;
public IdentityMap() public IdentityMap()
{ {
@ -94,12 +95,12 @@ public sealed class IdentityMap<TId> : IIdentityMap where TId : notnull
/// Returns false if already seen. /// Returns false if already seen.
/// </summary> /// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryAddKey(TId key) public bool TryAddKey(TKey key)
{ {
// Small int fast path // Small int fast path
if (_useSmallInt && IsInt32) if (_useSmallInt && IsInt32)
{ {
var intKey = Unsafe.As<TId, int>(ref key); var intKey = Unsafe.As<TKey, int>(ref key);
if ((uint)intKey < SmallSize) if ((uint)intKey < SmallSize)
{ {
return TryAddSmallInt(intKey); return TryAddSmallInt(intKey);
@ -113,12 +114,11 @@ public sealed class IdentityMap<TId> : IIdentityMap where TId : notnull
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool TryAddSmallInt(int key) private bool TryAddSmallInt(int key)
{ {
// Lazy init - ONLY bitmap for tracking (serializer doesn't need _smallValues) // Lazy init - ONLY bitmap for tracking
if (_smallBitmap == null) if (_smallBitmap == null)
{ {
_smallBitmap = ArrayPool<ulong>.Shared.Rent(SmallBitmapSize); _smallBitmap = ArrayPool<ulong>.Shared.Rent(SmallBitmapSize);
Array.Clear(_smallBitmap, 0, SmallBitmapSize); Array.Clear(_smallBitmap, 0, SmallBitmapSize);
// _smallValues allocated lazily in TryGetOrAddSmallIntValue when needed
} }
var wordIdx = key >> 6; var wordIdx = key >> 6;
@ -133,17 +133,17 @@ public sealed class IdentityMap<TId> : IIdentityMap where TId : notnull
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool KeyEquals(TId a, TId b) private static bool KeyEquals(TKey a, TKey b)
{ {
// JIT eliminates these branches at compile time for each TId instantiation // JIT eliminates these branches at compile time for each TKey instantiation
if (IsInt32) if (IsInt32)
{ {
return Unsafe.As<TId, int>(ref a) == Unsafe.As<TId, int>(ref b); return Unsafe.As<TKey, int>(ref a) == Unsafe.As<TKey, int>(ref b);
} }
if (IsString) if (IsString)
{ {
var strA = Unsafe.As<TId, string>(ref a); var strA = Unsafe.As<TKey, string>(ref a);
var strB = Unsafe.As<TId, string>(ref b); var strB = Unsafe.As<TKey, string>(ref b);
// Fast path: reference equality (interned strings) // Fast path: reference equality (interned strings)
//if (ReferenceEquals(strA, strB)) return true; //if (ReferenceEquals(strA, strB)) return true;
@ -153,18 +153,18 @@ public sealed class IdentityMap<TId> : IIdentityMap where TId : notnull
} }
if (IsInt64) if (IsInt64)
{ {
return Unsafe.As<TId, long>(ref a) == Unsafe.As<TId, long>(ref b); return Unsafe.As<TKey, long>(ref a) == Unsafe.As<TKey, long>(ref b);
} }
if (IsGuid) if (IsGuid)
{ {
return Unsafe.As<TId, Guid>(ref a) == Unsafe.As<TId, Guid>(ref b); return Unsafe.As<TKey, Guid>(ref a) == Unsafe.As<TKey, Guid>(ref b);
} }
// Fallback for other types // Fallback for other types
return EqualityComparer<TId>.Default.Equals(a, b); return EqualityComparer<TKey>.Default.Equals(a, b);
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool TryAddHash(TId key, out int slotIndex) private bool TryAddHash(TKey key, out int slotIndex)
{ {
var hash = GetHashCode(key); var hash = GetHashCode(key);
@ -179,7 +179,7 @@ public sealed class IdentityMap<TId> : IIdentityMap where TId : notnull
// Search chain // Search chain
for (var i = _buckets[bucketIdx]; i >= 0; i = _entries![i].Next) for (var i = _buckets[bucketIdx]; i >= 0; i = _entries![i].Next)
{ {
//if (EqualityComparer<TId>.Default.Equals(_keys![i], key)) //if (EqualityComparer<TKey>.Default.Equals(_keys![i], key))
if (KeyEquals(_keys![i], key)) // Direct comparison, no virtual call if (KeyEquals(_keys![i], key)) // Direct comparison, no virtual call
{ {
slotIndex = i; slotIndex = i;
@ -203,13 +203,13 @@ public sealed class IdentityMap<TId> : IIdentityMap where TId : notnull
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int GetHashCode(TId key) private static int GetHashCode(TKey key)
{ {
// Specialized hash for known types - JIT eliminates branches // Specialized hash for known types - JIT eliminates branches
if (IsInt32) return Unsafe.As<TId, int>(ref key); if (IsInt32) return Unsafe.As<TKey, int>(ref key);
if (IsInt64) return Unsafe.As<TId, long>(ref key).GetHashCode(); if (IsInt64) return Unsafe.As<TKey, long>(ref key).GetHashCode();
if (IsGuid) return Unsafe.As<TId, Guid>(ref key).GetHashCode(); if (IsGuid) return Unsafe.As<TKey, Guid>(ref key).GetHashCode();
if (IsString) return string.GetHashCode(Unsafe.As<TId, string>(ref key), StringComparison.Ordinal); if (IsString) return string.GetHashCode(Unsafe.As<TKey, string>(ref key), StringComparison.Ordinal);
return key.GetHashCode(); return key.GetHashCode();
} }
@ -220,14 +220,14 @@ public sealed class IdentityMap<TId> : IIdentityMap where TId : notnull
var actualCapacity = Math.Max(capacity, _bucketsLength); var actualCapacity = Math.Max(capacity, _bucketsLength);
_buckets = ArrayPool<int>.Shared.Rent(actualCapacity); _buckets = ArrayPool<int>.Shared.Rent(actualCapacity);
_bucketsLength = actualCapacity; _bucketsLength = _buckets.Length;
Array.Fill(_buckets, -1, 0, actualCapacity); Array.Fill(_buckets, -1, 0, _bucketsLength);
_entries = ArrayPool<HashSlot>.Shared.Rent(actualCapacity); _entries = ArrayPool<HashSlot>.Shared.Rent(actualCapacity);
_entriesLength = actualCapacity; _entriesLength = _entries.Length;
//Array.Clear(_entries, 0, _entriesLength); //Array.Clear(_entries, 0, _entriesLength);
_keys = ArrayPool<TId>.Shared.Rent(actualCapacity); _keys = ArrayPool<TKey>.Shared.Rent(actualCapacity);
//Array.Clear(_keys, 0, actualCapacity); //Array.Clear(_keys, 0, actualCapacity);
_count = 0; _count = 0;
@ -239,9 +239,10 @@ public sealed class IdentityMap<TId> : IIdentityMap where TId : notnull
// Rent new arrays // Rent new arrays
var newBuckets = ArrayPool<int>.Shared.Rent(newCapacity); var newBuckets = ArrayPool<int>.Shared.Rent(newCapacity);
Array.Fill(newBuckets, -1, 0, newCapacity); // MUST fill with -1 var newBucketsLength = newBuckets.Length;
Array.Fill(newBuckets, -1, 0, newBucketsLength);
var newEntries = ArrayPool<HashSlot>.Shared.Rent(newCapacity); var newEntries = ArrayPool<HashSlot>.Shared.Rent(newCapacity);
var newKeys = ArrayPool<TId>.Shared.Rent(newCapacity); var newKeys = ArrayPool<TKey>.Shared.Rent(newCapacity);
// Copy entries (no clear needed) // Copy entries (no clear needed)
Array.Copy(_entries!, newEntries, _count); Array.Copy(_entries!, newEntries, _count);
@ -250,7 +251,7 @@ public sealed class IdentityMap<TId> : IIdentityMap where TId : notnull
// Rebuild bucket chains // Rebuild bucket chains
for (var i = 0; i < _count; i++) for (var i = 0; i < _count; i++)
{ {
var bucketIdx = (GetHashCode(newKeys[i]) & 0x7FFFFFFF) % newCapacity; var bucketIdx = (GetHashCode(newKeys[i]) & 0x7FFFFFFF) % newBucketsLength;
newEntries[i].Next = newBuckets[bucketIdx]; newEntries[i].Next = newBuckets[bucketIdx];
newBuckets[bucketIdx] = i; newBuckets[bucketIdx] = i;
} }
@ -258,22 +259,22 @@ public sealed class IdentityMap<TId> : IIdentityMap where TId : notnull
// Return old arrays to pool // Return old arrays to pool
ArrayPool<int>.Shared.Return(_buckets!); ArrayPool<int>.Shared.Return(_buckets!);
ArrayPool<HashSlot>.Shared.Return(_entries!); ArrayPool<HashSlot>.Shared.Return(_entries!);
ArrayPool<TId>.Shared.Return(_keys!); ArrayPool<TKey>.Shared.Return(_keys!);
_buckets = newBuckets; _buckets = newBuckets;
_bucketsLength = newCapacity; _bucketsLength = newBucketsLength;
_entries = newEntries; _entries = newEntries;
_entriesLength = newCapacity; _entriesLength = newEntries.Length;
_keys = newKeys; _keys = newKeys;
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool HasKey(TId key) public bool HasKey(TKey key)
{ {
// Small int fast path - bitmap check is cache-friendly (512 bytes) // Small int fast path - bitmap check is cache-friendly (512 bytes)
if (_useSmallInt && IsInt32) if (_useSmallInt && IsInt32)
{ {
var intKey = Unsafe.As<TId, int>(ref key); var intKey = Unsafe.As<TKey, int>(ref key);
if ((uint)intKey < SmallSize && _smallBitmap != null) if ((uint)intKey < SmallSize && _smallBitmap != null)
{ {
var wordIdx = intKey >> 6; var wordIdx = intKey >> 6;
@ -298,87 +299,28 @@ public sealed class IdentityMap<TId> : IIdentityMap where TId : notnull
/// <summary> /// <summary>
/// Checks if key exists and returns existing value, or adds new key (deserialization). /// Checks if key exists and returns existing value, or adds new key (deserialization).
/// Returns true if first occurrence (key was added, out = null). /// Returns true if first occurrence (key was added, out = default).
/// Returns false if already seen (out = existing value). /// Returns false if already seen (out = existing value).
/// </summary> /// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryGetOrAddKey(TId key, out object? existing) public bool TryGetOrAddKey(TKey key, out TValue? existing)
{ {
// NOTE: SmallInt path DISABLED for deserializer.
// The 512KB _smallValues sparse array causes cache misses.
// Hash table has better cache locality.
//// Small int fast path
//if (_useSmallInt && IsInt32)
//{
// var intKey = Unsafe.As<TId, int>(ref key);
// if ((uint)intKey < SmallSize)
// {
// return TryGetOrAddSmallInt(intKey, out existing);
// }
//}
// Hash table path
if (!TryAddHash(key, out var slotIndex)) if (!TryAddHash(key, out var slotIndex))
{ {
existing = _entries![slotIndex].Value; existing = _entries![slotIndex].Value;
return false; return false;
} }
existing = null; existing = default;
return true;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool TryGetOrAddSmallInt(int key, out object? existing)
{
// Lazy init bitmap only - NOT _smallValues!
// This method is for tracking (key seen?), not value storage.
if (_smallBitmap == null)
{
_smallBitmap = ArrayPool<ulong>.Shared.Rent(SmallBitmapSize);
Array.Clear(_smallBitmap, 0, SmallBitmapSize);
}
var wordIdx = key >> 6;
var bit = 1UL << (key & 63);
ref var word = ref _smallBitmap[wordIdx];
if ((word & bit) != 0)
{
// Bitmap says seen - return stored value if any
existing = _smallValues?[key];
return false; // Already seen
}
// First occurrence - mark as seen (don't touch _smallValues!)
word |= bit;
existing = null;
return true; return true;
} }
/// <summary> /// <summary>
/// Gets existing value or adds new value for key (deserialization with object storage). /// Gets existing value or adds new value for key (deserialization with value storage).
/// Returns the existing value if key was seen before, or stores and returns newValue. /// Returns the existing value if key was seen before, or stores and returns newValue.
/// </summary> /// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public object TryGetOrAddValue(TId key, object newValue) public TValue TryGetOrAddValue(TKey key, TValue newValue)
{ {
// NOTE: SmallInt path DISABLED for deserializer value storage.
// The 512KB _smallValues sparse array causes cache misses.
// Hash table has better cache locality (only stores actual entries).
// Bitmap is only useful for tracking (serializer) - not for value storage.
//// Small int fast path
//if (_useSmallInt && IsInt32)
//{
// var intKey = Unsafe.As<TId, int>(ref key);
// if ((uint)intKey < SmallSize)
// {
// return TryGetOrAddSmallIntValue(intKey, newValue);
// }
//}
// Hash table path
if (!TryAddHash(key, out var slotIndex)) if (!TryAddHash(key, out var slotIndex))
{ {
var existing = _entries![slotIndex].Value; var existing = _entries![slotIndex].Value;
@ -388,71 +330,16 @@ public sealed class IdentityMap<TId> : IIdentityMap where TId : notnull
return newValue; return newValue;
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private object TryGetOrAddSmallIntValue(int key, object newValue)
{
// Lazy init bitmap
if (_smallBitmap == null)
{
_smallBitmap = ArrayPool<ulong>.Shared.Rent(SmallBitmapSize);
Array.Clear(_smallBitmap, 0, SmallBitmapSize);
}
// Lazy init values array (only when needed for deserialization)
if (_smallValues == null)
{
_smallValues = ArrayPool<object?>.Shared.Rent(SmallSize);
Array.Clear(_smallValues, 0, SmallSize);
}
var wordIdx = key >> 6;
var bit = 1UL << (key & 63);
ref var word = ref _smallBitmap[wordIdx];
if ((word & bit) != 0)
{
// Bitmap says seen - check if value exists
var existing = _smallValues![key];
if (existing != null)
return existing;
}
// First occurrence or no value yet - store new value
word |= bit;
_smallValues![key] = newValue;
return newValue;
}
/// <summary> /// <summary>
/// Tries to get the value for a key (ObjectRef lookup). /// Tries to get the value for a key.
/// Returns true if found, false if not. /// Returns true if found, false if not.
/// </summary> /// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryGetValue(TId key, out object? value) public bool TryGetValue(TKey key, out TValue? value)
{ {
//// Small int fast path - bitmap check first (cache-friendly, 512 bytes)
//if (_useSmallInt && IsInt32)
//{
// var intKey = Unsafe.As<TId, int>(ref key);
// if ((uint)intKey < SmallSize && _smallBitmap != null)
// {
// var wordIdx = intKey >> 6;
// var bit = 1UL << (intKey & 63);
// if ((_smallBitmap[wordIdx] & bit) != 0)
// {
// // Bitmap says seen - return value if exists
// value = _smallValues?[intKey];
// return value != null;
// }
// value = null;
// return false;
// }
//}
// Hash table path
if (_buckets == null) if (_buckets == null)
{ {
value = null; value = default;
return false; return false;
} }
@ -467,10 +354,20 @@ public sealed class IdentityMap<TId> : IIdentityMap where TId : notnull
return true; return true;
} }
} }
value = null; value = default;
return false; return false;
} }
/// <summary>
/// Returns a reference to the value at the given slot index.
/// Use with slotIndex from TryAddHash for in-place value modification.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ref TValue GetValueRef(int slotIndex)
{
return ref _entries![slotIndex].Value;
}
/// <summary> /// <summary>
/// Resets the identity map for reuse. /// Resets the identity map for reuse.
/// Small arrays (≤ InitialHashCapacity*5): keep and clear (faster than pool round-trip). /// Small arrays (≤ InitialHashCapacity*5): keep and clear (faster than pool round-trip).
@ -479,29 +376,6 @@ public sealed class IdentityMap<TId> : IIdentityMap where TId : notnull
/// <param name="preRentBuckets">If true, pre-rent arrays at next capacity (useful for async Clear to shift work from hot path)</param> /// <param name="preRentBuckets">If true, pre-rent arrays at next capacity (useful for async Clear to shift work from hot path)</param>
public void Reset(bool preRentBuckets = false) public void Reset(bool preRentBuckets = false)
{ {
if (_smallValues != null)
{
//Array.Clear(_smallValues, 0, SmallSize);
// Végigmegyünk a bitmap-en és csak a set bit-ekhez tartozó értékeket töröljük
for (var wordIdx = 0; wordIdx < SmallBitmapSize; wordIdx++)
{
var word = _smallBitmap[wordIdx];
if (word == 0) continue;
var baseIdx = wordIdx << 6;
while (word != 0)
{
var bitPos = BitOperations.TrailingZeroCount(word);
_smallValues[baseIdx + bitPos] = null;
word &= word - 1; // Clear lowest set bit
}
}
//ArrayPool<object?>.Shared.Return(_smallValues, clearArray: true); // Clear to release object refs
//_smallValues = null;
}
if (_smallBitmap != null) if (_smallBitmap != null)
{ {
Array.Clear(_smallBitmap, 0, SmallBitmapSize); Array.Clear(_smallBitmap, 0, SmallBitmapSize);
@ -538,20 +412,20 @@ public sealed class IdentityMap<TId> : IIdentityMap where TId : notnull
ArrayPool<int>.Shared.Return(_buckets); ArrayPool<int>.Shared.Return(_buckets);
ArrayPool<HashSlot>.Shared.Return(_entries!,false); ArrayPool<HashSlot>.Shared.Return(_entries!,false);
ArrayPool<TId>.Shared.Return(_keys!, false); ArrayPool<TKey>.Shared.Return(_keys!, false);
if (preRentBuckets) if (preRentBuckets)
{ {
// Pre-rent arrays now (async background) so Pool.Get() is faster // Pre-rent arrays now (async background) so Pool.Get() is faster
_buckets = ArrayPool<int>.Shared.Rent(nextCapacity); _buckets = ArrayPool<int>.Shared.Rent(nextCapacity);
_bucketsLength = nextCapacity; _bucketsLength = _buckets.Length;
Array.Fill(_buckets, -1, 0, nextCapacity); Array.Fill(_buckets, -1, 0, _bucketsLength);
_entries = ArrayPool<HashSlot>.Shared.Rent(nextCapacity); _entries = ArrayPool<HashSlot>.Shared.Rent(nextCapacity);
_entriesLength = nextCapacity; _entriesLength = _entries.Length;
//Array.Clear(_entries, 0, _entriesLength); //Array.Clear(_entries, 0, _entriesLength);
_keys = ArrayPool<TId>.Shared.Rent(nextCapacity); _keys = ArrayPool<TKey>.Shared.Rent(nextCapacity);
//Array.Clear(_keys, 0, nextCapacity); //Array.Clear(_keys, 0, nextCapacity);
} }
else else
@ -568,108 +442,3 @@ public sealed class IdentityMap<TId> : IIdentityMap where TId : notnull
} }
#endregion #endregion
//#region IId Reference Tracking
///// <summary>
///// Specifies the accessor type for IId.Id property to enable typed getter dispatch without boxing.
///// </summary>
//public enum IdAccessorType : byte
//{
// None = 0,
// /// <summary>Id is int (most common).</summary>
// Int32 = 1,
// /// <summary>Id is long.</summary>
// Int64 = 2,
// /// <summary>Id is Guid.</summary>
// Guid = 3,
//}
///// <summary>
///// Interface for identity maps used in serialization tracking.
///// Enables type-safe Reset() without knowing the generic type parameter.
///// </summary>
//public interface IIdentityMap
//{
// /// <summary>
// /// Resets the identity map for reuse between serializations.
// /// </summary>
// void Reset();
//}
///// <summary>
///// Generic identity map for tracking IId values during serialization/deserialization.
///// Uses Dictionary internally for unified tracking + object storage.
///// </summary>
///// <typeparam name="TId">The ID type (int, long, Guid)</typeparam>
//public sealed class IdentityMap<TId> : IIdentityMap where TId : notnull
//{
// private readonly Dictionary<TId, object?> _tracked;
// public IdentityMap()
// {
// _tracked = new Dictionary<TId, object?>(EqualityComparer<TId>.Default);
// }
// /// <summary>
// /// Tries to add a key to tracking (serialization).
// /// Returns true if first occurrence (key was added).
// /// Returns false if already seen.
// /// </summary>
// [MethodImpl(MethodImplOptions.AggressiveInlining)]
// public bool TryAddKey(TId key)
// {
// return _tracked.TryAdd(key, null);
// }
// [MethodImpl(MethodImplOptions.AggressiveInlining)]
// public bool HasKey(TId key) => _tracked.ContainsKey(key);
// /// <summary>
// /// Checks if key exists and returns existing value, or adds new key (deserialization).
// /// Returns true if first occurrence (key was added, out = null).
// /// Returns false if already seen (out = existing value).
// /// </summary>
// [MethodImpl(MethodImplOptions.AggressiveInlining)]
// public bool TryGetOrAddKey(TId key, out object? existing)
// {
// ref var slot = ref CollectionsMarshal.GetValueRefOrAddDefault(_tracked, key, out var exists);
// existing = exists ? slot : null;
// return !exists;
// }
// /// <summary>
// /// Gets existing value or adds new value for key (deserialization with object storage).
// /// Returns the existing value if key was seen before, or stores and returns newValue.
// /// </summary>
// [MethodImpl(MethodImplOptions.AggressiveInlining)]
// public object TryGetOrAddValue(TId key, object newValue)
// {
// ref var slot = ref CollectionsMarshal.GetValueRefOrAddDefault(_tracked, key, out var exists);
// if (exists && slot != null) return slot;
// slot = newValue;
// return newValue;
// }
// /// <summary>
// /// Tries to get the value for a key (ObjectRef lookup).
// /// Returns true if found, false if not.
// /// </summary>
// [MethodImpl(MethodImplOptions.AggressiveInlining)]
// public bool TryGetValue(TId key, out object? value)
// {
// return _tracked.TryGetValue(key, out value);
// }
// /// <summary>
// /// Resets the identity map for reuse.
// /// </summary>
// public void Reset()
// {
// _tracked.Clear();
// }
//}
//#endregion

View File

@ -40,18 +40,17 @@ public sealed class TypeMetadataWrapper<TMetadata> where TMetadata : TypeMetadat
/// <summary> /// <summary>
/// Typed IdentityMap for Int32 IDs. Direct access, no type check. /// Typed IdentityMap for Int32 IDs. Direct access, no type check.
/// </summary> /// </summary>
internal IdentityMap<int>? IdentityMapInt32; internal IdentityMap<int, object?>? IdentityMapInt32;
/// <summary> /// <summary>
/// Typed IdentityMap for Int64 IDs. Direct access, no type check. /// Typed IdentityMap for Int64 IDs. Direct access, no type check.
/// </summary> /// </summary>
internal IdentityMap<long>? IdentityMapInt64; internal IdentityMap<long, object?>? IdentityMapInt64;
/// <summary> /// <summary>
/// Typed IdentityMap for Guid IDs. Direct access, no type check. /// Typed IdentityMap for Guid IDs. Direct access, no type check.
/// </summary> /// </summary>
internal IdentityMap<Guid>? IdentityMapGuid; internal IdentityMap<Guid, object?>? IdentityMapGuid;
#endregion #endregion
@ -152,7 +151,7 @@ public sealed class TypeMetadataWrapper<TMetadata> where TMetadata : TypeMetadat
{ {
if (id == 0) return newObj; // Default Id - no tracking if (id == 0) return newObj; // Default Id - no tracking
var map = IdentityMapInt32 ??= new IdentityMap<int>(); var map = IdentityMapInt32 ??= new IdentityMap<int, object?>();
return map.TryGetOrAddValue(id, newObj); return map.TryGetOrAddValue(id, newObj);
} }
@ -166,7 +165,7 @@ public sealed class TypeMetadataWrapper<TMetadata> where TMetadata : TypeMetadat
var id = RefIdGetterInt32!(instance); var id = RefIdGetterInt32!(instance);
if (id == 0) return; if (id == 0) return;
var map = IdentityMapInt32 ??= new IdentityMap<int>(); var map = IdentityMapInt32 ??= new IdentityMap<int, object?>();
map.TryGetOrAddValue(id, instance); map.TryGetOrAddValue(id, instance);
} }
@ -189,7 +188,7 @@ public sealed class TypeMetadataWrapper<TMetadata> where TMetadata : TypeMetadat
{ {
if (id == 0) return newObj; if (id == 0) return newObj;
var map = IdentityMapInt64 ??= new IdentityMap<long>(); var map = IdentityMapInt64 ??= new IdentityMap<long, object?>();
return map.TryGetOrAddValue(id, newObj); return map.TryGetOrAddValue(id, newObj);
} }
@ -199,7 +198,7 @@ public sealed class TypeMetadataWrapper<TMetadata> where TMetadata : TypeMetadat
var id = RefIdGetterInt64!(instance); var id = RefIdGetterInt64!(instance);
if (id == 0) return; if (id == 0) return;
var map = IdentityMapInt64 ??= new IdentityMap<long>(); var map = IdentityMapInt64 ??= new IdentityMap<long, object?>();
map.TryGetOrAddValue(id, instance); map.TryGetOrAddValue(id, instance);
} }
@ -222,7 +221,7 @@ public sealed class TypeMetadataWrapper<TMetadata> where TMetadata : TypeMetadat
{ {
if (id == Guid.Empty) return newObj; if (id == Guid.Empty) return newObj;
var map = IdentityMapGuid ??= new IdentityMap<Guid>(); var map = IdentityMapGuid ??= new IdentityMap<Guid, object?>();
return map.TryGetOrAddValue(id, newObj); return map.TryGetOrAddValue(id, newObj);
} }
@ -232,7 +231,7 @@ public sealed class TypeMetadataWrapper<TMetadata> where TMetadata : TypeMetadat
var id = RefIdGetterGuid!(instance); var id = RefIdGetterGuid!(instance);
if (id == Guid.Empty) return; if (id == Guid.Empty) return;
var map = IdentityMapGuid ??= new IdentityMap<Guid>(); var map = IdentityMapGuid ??= new IdentityMap<Guid, object?>();
map.TryGetOrAddValue(id, instance); map.TryGetOrAddValue(id, instance);
} }
@ -245,23 +244,23 @@ public sealed class TypeMetadataWrapper<TMetadata> where TMetadata : TypeMetadat
/// SLOW PATH - use typed methods (TryGetValueInt32, etc.) instead! /// SLOW PATH - use typed methods (TryGetValueInt32, etc.) instead!
/// </summary> /// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public IdentityMap<TId> GetOrCreateIdentityMap<TId>() where TId : notnull public IdentityMap<TId, object?> GetOrCreateIdentityMap<TId>() where TId : notnull
{ {
// Route to typed fields based on TId // Route to typed fields based on TId
if (typeof(TId) == typeof(int)) if (typeof(TId) == typeof(int))
{ {
var map = IdentityMapInt32 ??= new IdentityMap<int>(); var map = IdentityMapInt32 ??= new IdentityMap<int, object?>();
return Unsafe.As<IdentityMap<int>, IdentityMap<TId>>(ref map); return Unsafe.As<IdentityMap<int, object?>, IdentityMap<TId, object?>>(ref map);
} }
if (typeof(TId) == typeof(long)) if (typeof(TId) == typeof(long))
{ {
var map = IdentityMapInt64 ??= new IdentityMap<long>(); var map = IdentityMapInt64 ??= new IdentityMap<long, object?>();
return Unsafe.As<IdentityMap<long>, IdentityMap<TId>>(ref map); return Unsafe.As<IdentityMap<long, object?>, IdentityMap<TId, object?>>(ref map);
} }
if (typeof(TId) == typeof(Guid)) if (typeof(TId) == typeof(Guid))
{ {
var map = IdentityMapGuid ??= new IdentityMap<Guid>(); var map = IdentityMapGuid ??= new IdentityMap<Guid, object?>();
return Unsafe.As<IdentityMap<Guid>, IdentityMap<TId>>(ref map); return Unsafe.As<IdentityMap<Guid, object?>, IdentityMap<TId, object?>>(ref map);
} }
throw new NotSupportedException($"Id type {typeof(TId)} is not supported"); throw new NotSupportedException($"Id type {typeof(TId)} is not supported");