Refactor identity map for pooled, high-perf reference tracking

Move IdentityMap and IdAccessorType to dedicated file and update all usages. Introduce pooling and cache-friendly optimizations for small int keys and hash tables, minimizing allocations and speeding up resets. Update buffer sizes and profiling loops. Add extensive comments and preserve old implementation for reference. This prepares the codebase for more efficient serialization reference tracking.
This commit is contained in:
Loretta 2026-01-29 14:13:31 +01:00
parent 2eca18ca3f
commit 946148cc3d
11 changed files with 285 additions and 204 deletions

View File

@ -128,21 +128,33 @@ public static class Program
var options = AcBinarySerializerOptions.WithoutReferenceHandling; var options = AcBinarySerializerOptions.WithoutReferenceHandling;
options.UseStringInterning = StringInterningMode.None; options.UseStringInterning = StringInterningMode.None;
byte[] bytes = AcBinarySerializer.Serialize(order, options);
// Warmup (fills caches) // Warmup (fills caches)
System.Console.WriteLine("Warming up (10 iterations)..."); System.Console.WriteLine("Warming up (10 iterations)...");
for (var i = 0; i < 10; i++) for (var i = 0; i < 1000; i++)
{ {
_ = AcBinarySerializer.Serialize(order, options); _ = AcBinarySerializer.Serialize(order, options);
_ = AcBinaryDeserializer.Deserialize<TestOrder>(bytes);
} }
Thread.Sleep(2000);
System.Console.WriteLine("Warmup complete. Caches are now populated."); System.Console.WriteLine("Warmup complete. Caches are now populated.");
System.Console.WriteLine(); System.Console.WriteLine();
// HOT PATH - this is what the profiler should capture! // HOT PATH - this is what the profiler should capture!
System.Console.WriteLine("Running hot path (1000 iterations for profiling)..."); System.Console.WriteLine("Running hot path serialization (1000 iterations for profiling)...");
for (var i = 0; i < 1000; i++) for (var i = 0; i < 1000; i++)
{ {
_ = AcBinarySerializer.Serialize(order, options); _ = AcBinarySerializer.Serialize(order, options);
_ = AcBinaryDeserializer.Deserialize<TestOrder>(bytes);
} }
System.Console.WriteLine("Running hot path deserialization (1000 iterations for profiling)...");
for (var i = 0; i < 1000; i++)
{
_ = AcBinaryDeserializer.Deserialize<TestOrder>(bytes);
}
System.Console.WriteLine("Hot path complete."); System.Console.WriteLine("Hot path complete.");
System.Console.WriteLine(); System.Console.WriteLine();

View File

@ -586,110 +586,6 @@ public static class AcSerializerCommon
} }
#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 #endregion
#region Chain Reference Tracking #region Chain Reference Tracking

View File

@ -39,9 +39,9 @@ public static partial class AcBinaryDeserializer
return metadata.IdAccessorType switch return metadata.IdAccessorType switch
{ {
AcSerializerCommon.IdAccessorType.Int32 => TryGetOrStoreInt32(wrapper, newObj, metadata.GetIdInt32(newObj)), IdAccessorType.Int32 => TryGetOrStoreInt32(wrapper, newObj, metadata.GetIdInt32(newObj)),
AcSerializerCommon.IdAccessorType.Int64 => TryGetOrStoreLong(wrapper, newObj, metadata.GetIdInt64(newObj)), IdAccessorType.Int64 => TryGetOrStoreLong(wrapper, newObj, metadata.GetIdInt64(newObj)),
AcSerializerCommon.IdAccessorType.Guid => TryGetOrStoreGuid(wrapper, newObj, metadata.GetIdGuid(newObj)), IdAccessorType.Guid => TryGetOrStoreGuid(wrapper, newObj, metadata.GetIdGuid(newObj)),
_ => newObj _ => newObj
}; };
} }

View File

@ -512,17 +512,17 @@ public static partial class AcBinaryDeserializer
/// Reads Id value without type marker. The serializer didn't write a marker for IId types. /// Reads Id value without type marker. The serializer didn't write a marker for IId types.
/// </summary> /// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void ReadIdValueWithoutMarker(ref BinaryDeserializationContext context, object target, BinaryPropertySetterInfo propInfo, AcSerializerCommon.IdAccessorType idType) private static void ReadIdValueWithoutMarker(ref BinaryDeserializationContext context, object target, BinaryPropertySetterInfo propInfo, IdAccessorType idType)
{ {
switch (idType) switch (idType)
{ {
case AcSerializerCommon.IdAccessorType.Int32: case IdAccessorType.Int32:
propInfo.SetInt32(target, context.ReadVarInt()); propInfo.SetInt32(target, context.ReadVarInt());
break; break;
case AcSerializerCommon.IdAccessorType.Int64: case IdAccessorType.Int64:
propInfo.SetInt64(target, context.ReadVarLong()); propInfo.SetInt64(target, context.ReadVarLong());
break; break;
case AcSerializerCommon.IdAccessorType.Guid: case IdAccessorType.Guid:
propInfo.SetGuid(target, context.ReadGuidUnsafe()); propInfo.SetGuid(target, context.ReadGuidUnsafe());
break; break;
} }

View File

@ -942,9 +942,9 @@ public static partial class AcBinaryDeserializer
// IId: [ObjectRef][Id érték] - lookup by Id // IId: [ObjectRef][Id érték] - lookup by Id
return metadata.IdAccessorType switch return metadata.IdAccessorType switch
{ {
AcSerializerCommon.IdAccessorType.Int32 => ReadObjectRefInt32(ref context, ref wrapper), IdAccessorType.Int32 => ReadObjectRefInt32(ref context, ref wrapper),
AcSerializerCommon.IdAccessorType.Int64 => ReadObjectRefInt64(ref context, ref wrapper), IdAccessorType.Int64 => ReadObjectRefInt64(ref context, ref wrapper),
AcSerializerCommon.IdAccessorType.Guid => ReadObjectRefGuid(ref context, ref wrapper), IdAccessorType.Guid => ReadObjectRefGuid(ref context, ref wrapper),
_ => throw new AcBinaryDeserializationException( _ => throw new AcBinaryDeserializationException(
$"IId type '{metadata.SourceType.Name}' must have valid IdAccessorType", $"IId type '{metadata.SourceType.Name}' must have valid IdAccessorType",
context.Position, metadata.SourceType) context.Position, metadata.SourceType)
@ -1509,9 +1509,9 @@ public static partial class AcBinaryDeserializer
{ {
return metadata.IdAccessorType switch return metadata.IdAccessorType switch
{ {
AcSerializerCommon.IdAccessorType.Int32 => metadata.GetIdInt32(instance), IdAccessorType.Int32 => metadata.GetIdInt32(instance),
AcSerializerCommon.IdAccessorType.Int64 => metadata.GetIdInt64(instance), IdAccessorType.Int64 => metadata.GetIdInt64(instance),
AcSerializerCommon.IdAccessorType.Guid => metadata.GetIdGuid(instance), IdAccessorType.Guid => metadata.GetIdGuid(instance),
_ => null _ => null
}; };
} }

View File

@ -64,7 +64,7 @@ public static partial class AcBinarySerializer
/// </summary> /// </summary>
internal sealed class BinarySerializationContext : SerializationContextBase<BinarySerializeTypeMetadata, AcBinarySerializerOptions>, IDisposable internal sealed class BinarySerializationContext : SerializationContextBase<BinarySerializeTypeMetadata, AcBinarySerializerOptions>, IDisposable
{ {
private const int MinBufferSize = 256; private const int MinBufferSize = 512;
private const int PropertyIndexBufferMaxCache = 512; private const int PropertyIndexBufferMaxCache = 512;
private const int PropertyStateBufferMaxCache = 512; private const int PropertyStateBufferMaxCache = 512;
private const int InitialInternCapacity = 32; private const int InitialInternCapacity = 32;

View File

@ -845,7 +845,7 @@ public static partial class AcBinarySerializer
// IId típus: track by Id, ObjectRef writes Id // IId típus: track by Id, ObjectRef writes Id
switch (metadata.IdAccessorType) switch (metadata.IdAccessorType)
{ {
case AcSerializerCommon.IdAccessorType.Int32: case IdAccessorType.Int32:
if (!context.TryTrack(wrapper, value, out int intId)) if (!context.TryTrack(wrapper, value, out int intId))
{ {
// Already seen → ObjectRef + Id // Already seen → ObjectRef + Id
@ -857,7 +857,7 @@ public static partial class AcBinarySerializer
context.WriteByte(BinaryTypeCode.Object); context.WriteByte(BinaryTypeCode.Object);
break; break;
case AcSerializerCommon.IdAccessorType.Int64: case IdAccessorType.Int64:
if (!context.TryTrack(wrapper, value, out long longId)) if (!context.TryTrack(wrapper, value, out long longId))
{ {
context.WriteByte(BinaryTypeCode.ObjectRef); context.WriteByte(BinaryTypeCode.ObjectRef);
@ -867,7 +867,7 @@ public static partial class AcBinarySerializer
context.WriteByte(BinaryTypeCode.Object); context.WriteByte(BinaryTypeCode.Object);
break; break;
case AcSerializerCommon.IdAccessorType.Guid: case IdAccessorType.Guid:
if (!context.TryTrack(wrapper, value, out Guid guidId)) if (!context.TryTrack(wrapper, value, out Guid guidId))
{ {
context.WriteByte(BinaryTypeCode.ObjectRef); context.WriteByte(BinaryTypeCode.ObjectRef);

View File

@ -1,6 +1,8 @@
//using System.Runtime.CompilerServices; using System.Buffers;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
//namespace AyCode.Core.Serializers; namespace AyCode.Core.Serializers;
//#region IId Reference Tracking //#region IId Reference Tracking
@ -29,10 +31,11 @@
// void Reset(); // void Reset();
//} //}
///// <summary> ///// <summary>
///// High-performance identity map for tracking IId values during serialization/deserialization. ///// High-performance identity map for tracking IId values during serialization/deserialization.
///// Uses custom hash table optimized for our use case: ///// Uses custom hash table optimized for our use case:
///// - Small int keys (0-4095): direct array access with bitmap ///// - Small int keys (0-4095): bitmap for fast lookup + direct value array
///// - 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>
@ -41,24 +44,27 @@
//{ //{
// // Small int optimization (TId = int only, 0-4095) // // Small int optimization (TId = int only, 0-4095)
// private const int SmallSize = 4096; // private const int SmallSize = 4096;
// private const int SmallBitmapSize = SmallSize / 64; // 64 ulongs // private const int SmallBitmapSize = SmallSize / 64; // 64 ulongs = 512 bytes (fits in L1 cache!)
// // Slot for storing tracked values // // Slot for hash table entries (generation needed for hash table validity)
// private struct Slot // private struct HashSlot
// { // {
// public object? Value; // stored object for deserialization // public object? Value;
// public int Next; // next slot index in chain (-1 = end) // public int Next; // next slot index in chain (-1 = end)
// } // }
// // Small int storage (only used when TId is int) // // Small int storage - SIMPLE: bitmap for "seen?" + direct object[] for values
// private ulong[]? _smallBitmap; // quick "seen?" check // // Bitmap protects values - no generation needed, no value array clearing on Reset!
// private Slot[]? _smallSlots; // direct indexed storage // 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 Slot[]? _entries; // hash table entries // private HashSlot[]? _entries; // hash table entries
// private TId[]? _keys; // keys for equality check // private TId[]? _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 _entriesLength; // actual capacity
// private const int InitialHashCapacity = 16; // private const int InitialHashCapacity = 16;
@ -100,8 +106,11 @@
// // Lazy init // // Lazy init
// if (_smallBitmap == null) // if (_smallBitmap == null)
// { // {
// _smallBitmap = new ulong[SmallBitmapSize]; // _smallBitmap = ArrayPool<ulong>.Shared.Rent(SmallBitmapSize);
// _smallSlots = new Slot[SmallSize]; // Array.Clear(_smallBitmap, 0, SmallBitmapSize);
// _smallValues = ArrayPool<object?>.Shared.Rent(SmallSize);
// // No clear needed - bitmap protects values
// } // }
// var wordIdx = key >> 6; // var wordIdx = key >> 6;
@ -109,13 +118,41 @@
// ref var word = ref _smallBitmap[wordIdx]; // ref var word = ref _smallBitmap[wordIdx];
// if ((word & bit) != 0) // if ((word & bit) != 0)
// return false; // already seen // return false; // Already seen (bitmap is cleared on Reset)
// word |= bit; // word |= bit;
// _smallSlots![key] = default; // init slot
// return true; // return true;
// } // }
// [MethodImpl(MethodImplOptions.AggressiveInlining)]
// private static bool KeyEquals(TId a, TId b)
// {
// // JIT eliminates these branches at compile time for each TId instantiation
// if (IsInt32)
// {
// return Unsafe.As<TId, int>(ref a) == Unsafe.As<TId, int>(ref b);
// }
// if (IsString)
// {
// var strA = Unsafe.As<TId, string>(ref a);
// var strB = Unsafe.As<TId, string>(ref b);
// // Fast path: reference equality (interned strings)
// if (ReferenceEquals(strA, strB)) return true;
// // Ordinal comparison is fastest for non-interned
// return string.Equals(strA, strB, StringComparison.Ordinal);
// }
// if (IsInt64)
// {
// return Unsafe.As<TId, long>(ref a) == Unsafe.As<TId, long>(ref b);
// }
// if (IsGuid)
// {
// return Unsafe.As<TId, Guid>(ref a) == Unsafe.As<TId, Guid>(ref b);
// }
// // Fallback for other types
// return EqualityComparer<TId>.Default.Equals(a, b);
// }
// [MethodImpl(MethodImplOptions.AggressiveInlining)] // [MethodImpl(MethodImplOptions.AggressiveInlining)]
// private bool TryAddHash(TId key, out int slotIndex) // private bool TryAddHash(TId key, out int slotIndex)
// { // {
@ -132,7 +169,8 @@
// // 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<TId>.Default.Equals(_keys![i], key))
// if (KeyEquals(_keys![i], key)) // Direct comparison, no virtual call
// { // {
// slotIndex = i; // slotIndex = i;
// return false; // already seen // return false; // already seen
@ -140,16 +178,16 @@
// } // }
// // Resize if needed // // Resize if needed
// if (_count >= _entries!.Length) // if (_count >= _entriesLength)
// { // {
// Resize(); // Resize();
// bucketIdx = (hash & 0x7FFFFFFF) % _buckets.Length; // bucketIdx = (hash & 0x7FFFFFFF) % _bucketsLength;
// } // }
// // Add new entry // // Add new entry
// slotIndex = _count++; // slotIndex = _count++;
// _keys![slotIndex] = key; // _keys![slotIndex] = key;
// _entries[slotIndex] = new Slot { Next = _buckets[bucketIdx] }; // _entries![slotIndex] = new HashSlot { Next = _buckets[bucketIdx] };
// _buckets[bucketIdx] = slotIndex; // _buckets[bucketIdx] = slotIndex;
// return true; // return true;
// } // }
@ -157,31 +195,41 @@
// [MethodImpl(MethodImplOptions.AggressiveInlining)] // [MethodImpl(MethodImplOptions.AggressiveInlining)]
// private static int GetHashCode(TId key) // private static int GetHashCode(TId key)
// { // {
// // Specialized hash for known types // // Specialized hash for known types - JIT eliminates branches
// if (IsInt32) return Unsafe.As<TId, int>(ref key); // if (IsInt32) return Unsafe.As<TId, int>(ref key);
// if (IsInt64) return Unsafe.As<TId, long>(ref key).GetHashCode(); // if (IsInt64) return Unsafe.As<TId, long>(ref key).GetHashCode();
// if (IsGuid) return Unsafe.As<TId, Guid>(ref key).GetHashCode(); // if (IsGuid) return Unsafe.As<TId, Guid>(ref key).GetHashCode();
// if (IsString) return string.GetHashCode(Unsafe.As<TId, string>(ref key), StringComparison.Ordinal);
// return key.GetHashCode(); // return key.GetHashCode();
// } // }
// private void InitHashTable(int capacity) // private void InitHashTable(int capacity)
// { // {
// _buckets = new int[capacity]; // _buckets = ArrayPool<int>.Shared.Rent(capacity);
// Array.Fill(_buckets, -1); // _bucketsLength = capacity;
// _entries = new Slot[capacity]; // Array.Fill(_buckets, -1, 0, capacity); // MUST fill with -1
// _keys = new TId[capacity];
// _entries = ArrayPool<HashSlot>.Shared.Rent(capacity);
// _entriesLength = capacity;
// // No clear needed - generation counter handles validity
// _keys = ArrayPool<TId>.Shared.Rent(capacity);
// // No clear needed - tracked by _count
// _count = 0; // _count = 0;
// } // }
// private void Resize() // private void Resize()
// { // {
// var newCapacity = _buckets!.Length * 2; // var newCapacity = _bucketsLength * 2;
// var newBuckets = new int[newCapacity];
// Array.Fill(newBuckets, -1);
// var newEntries = new Slot[newCapacity];
// var newKeys = new TId[newCapacity];
// // Copy entries // // Rent new arrays
// var newBuckets = ArrayPool<int>.Shared.Rent(newCapacity);
// Array.Fill(newBuckets, -1, 0, newCapacity); // MUST fill with -1
// var newEntries = ArrayPool<HashSlot>.Shared.Rent(newCapacity);
// var newKeys = ArrayPool<TId>.Shared.Rent(newCapacity);
// // Copy entries (no clear needed)
// Array.Copy(_entries!, newEntries, _count); // Array.Copy(_entries!, newEntries, _count);
// Array.Copy(_keys!, newKeys, _count); // Array.Copy(_keys!, newKeys, _count);
@ -193,15 +241,22 @@
// newBuckets[bucketIdx] = i; // newBuckets[bucketIdx] = i;
// } // }
// // Return old arrays to pool
// ArrayPool<int>.Shared.Return(_buckets!);
// ArrayPool<HashSlot>.Shared.Return(_entries!);
// ArrayPool<TId>.Shared.Return(_keys!);
// _buckets = newBuckets; // _buckets = newBuckets;
// _bucketsLength = newCapacity;
// _entries = newEntries; // _entries = newEntries;
// _entriesLength = newCapacity;
// _keys = newKeys; // _keys = newKeys;
// } // }
// [MethodImpl(MethodImplOptions.AggressiveInlining)] // [MethodImpl(MethodImplOptions.AggressiveInlining)]
// public bool HasKey(TId key) // public bool HasKey(TId key)
// { // {
// // Small int fast path // // Small int fast path - bitmap check is cache-friendly (512 bytes)
// if (IsInt32) // if (IsInt32)
// { // {
// var intKey = Unsafe.As<TId, int>(ref key); // var intKey = Unsafe.As<TId, int>(ref key);
@ -221,7 +276,7 @@
// 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 (KeyEquals(_keys![i], key))
// return true; // return true;
// } // }
// return false; // return false;
@ -261,8 +316,10 @@
// // Lazy init // // Lazy init
// if (_smallBitmap == null) // if (_smallBitmap == null)
// { // {
// _smallBitmap = new ulong[SmallBitmapSize]; // _smallBitmap = ArrayPool<ulong>.Shared.Rent(SmallBitmapSize);
// _smallSlots = new Slot[SmallSize]; // Array.Clear(_smallBitmap, 0, SmallBitmapSize);
// _smallValues = ArrayPool<object?>.Shared.Rent(SmallSize);
// } // }
// var wordIdx = key >> 6; // var wordIdx = key >> 6;
@ -271,12 +328,14 @@
// ref var word = ref _smallBitmap[wordIdx]; // ref var word = ref _smallBitmap[wordIdx];
// if ((word & bit) != 0) // if ((word & bit) != 0)
// { // {
// existing = _smallSlots![key].Value; // // Bitmap says seen - value is valid
// return false; // already seen // existing = _smallValues![key];
// return false; // Already seen
// } // }
// // First occurrence - mark as seen
// word |= bit; // word |= bit;
// _smallSlots![key] = default; // _smallValues![key] = null;
// existing = null; // existing = null;
// return true; // return true;
// } // }
@ -314,8 +373,10 @@
// // Lazy init // // Lazy init
// if (_smallBitmap == null) // if (_smallBitmap == null)
// { // {
// _smallBitmap = new ulong[SmallBitmapSize]; // _smallBitmap = ArrayPool<ulong>.Shared.Rent(SmallBitmapSize);
// _smallSlots = new Slot[SmallSize]; // Array.Clear(_smallBitmap, 0, SmallBitmapSize);
// _smallValues = ArrayPool<object?>.Shared.Rent(SmallSize);
// } // }
// var wordIdx = key >> 6; // var wordIdx = key >> 6;
@ -324,12 +385,15 @@
// ref var word = ref _smallBitmap[wordIdx]; // ref var word = ref _smallBitmap[wordIdx];
// if ((word & bit) != 0) // if ((word & bit) != 0)
// { // {
// var existing = _smallSlots![key].Value; // // Bitmap says seen - check if value exists
// if (existing != null) return existing; // var existing = _smallValues![key];
// if (existing != null)
// return existing;
// } // }
// // First occurrence or no value yet - store new value
// word |= bit; // word |= bit;
// _smallSlots![key].Value = newValue; // _smallValues![key] = newValue;
// return newValue; // return newValue;
// } // }
@ -340,7 +404,7 @@
// [MethodImpl(MethodImplOptions.AggressiveInlining)] // [MethodImpl(MethodImplOptions.AggressiveInlining)]
// public bool TryGetValue(TId key, out object? value) // public bool TryGetValue(TId key, out object? value)
// { // {
// // Small int fast path // // Small int fast path - bitmap check first (cache-friendly, 512 bytes)
// if (IsInt32) // if (IsInt32)
// { // {
// var intKey = Unsafe.As<TId, int>(ref key); // var intKey = Unsafe.As<TId, int>(ref key);
@ -350,7 +414,8 @@
// var bit = 1UL << (intKey & 63); // var bit = 1UL << (intKey & 63);
// if ((_smallBitmap[wordIdx] & bit) != 0) // if ((_smallBitmap[wordIdx] & bit) != 0)
// { // {
// value = _smallSlots![intKey].Value; // // Bitmap says seen - value is valid
// value = _smallValues![intKey];
// return true; // return true;
// } // }
// value = null; // value = null;
@ -370,7 +435,7 @@
// 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 (KeyEquals(_keys![i], key))
// { // {
// value = _entries[i].Value; // value = _entries[i].Value;
// return true; // return true;
@ -382,25 +447,133 @@
// /// <summary> // /// <summary>
// /// Resets the identity map for reuse. // /// Resets the identity map for reuse.
// /// Fast Reset: only bitmap clear (512 bytes).
// /// No value array clearing needed - bitmap protects values!
// /// </summary> // /// </summary>
// public void Reset() // public void Reset()
// { // {
// // Clear small int storage // // Clear only bitmap (512 bytes - fits in L1 cache, very fast)
// // Values array is NOT cleared - bitmap protects old values from being read
// if (_smallBitmap != null) // if (_smallBitmap != null)
// { // {
// Array.Clear(_smallBitmap); // Array.Clear(_smallBitmap, 0, SmallBitmapSize);
// Array.Clear(_smallSlots!);
// } // }
// // Clear hash table // // Hash table reset
// if (_buckets != null) // if (_buckets != null)
// { // {
// Array.Fill(_buckets, -1); // Array.Fill(_buckets, -1, 0, _bucketsLength);
// Array.Clear(_entries!); // // Clear entries and keys to release object refs (O(_count), not O(capacity))
// Array.Clear(_keys!); // Array.Clear(_entries!, 0, _count);
// Array.Clear(_keys!, 0, _count);
// _count = 0; // _count = 0;
// } // }
// } // }
//} //}
//#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

@ -37,7 +37,7 @@ public abstract class SerializationContextBase<TMetadata, TOptions> : AcSerializ
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryTrack(TypeMetadataWrapper<TMetadata> wrapper, object obj, out int refId) public bool TryTrack(TypeMetadataWrapper<TMetadata> wrapper, object obj, out int refId)
{ {
Debug.Assert(wrapper.Metadata.IdAccessorType == AcSerializerCommon.IdAccessorType.Int32); Debug.Assert(wrapper.Metadata.IdAccessorType == IdAccessorType.Int32);
// Use pre-cast getter - no cast overhead! // Use pre-cast getter - no cast overhead!
refId = wrapper.RefIdGetterInt32!(obj); refId = wrapper.RefIdGetterInt32!(obj);
@ -80,7 +80,7 @@ public abstract class SerializationContextBase<TMetadata, TOptions> : AcSerializ
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryTrack(TypeMetadataWrapper<TMetadata> wrapper, object obj, out long refId) public bool TryTrack(TypeMetadataWrapper<TMetadata> wrapper, object obj, out long refId)
{ {
Debug.Assert(wrapper.Metadata.IdAccessorType == AcSerializerCommon.IdAccessorType.Int64); Debug.Assert(wrapper.Metadata.IdAccessorType == IdAccessorType.Int64);
// Use pre-cast getter - no cast overhead! // Use pre-cast getter - no cast overhead!
refId = wrapper.RefIdGetterInt64!(obj); refId = wrapper.RefIdGetterInt64!(obj);
@ -96,7 +96,7 @@ public abstract class SerializationContextBase<TMetadata, TOptions> : AcSerializ
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryTrack(TypeMetadataWrapper<TMetadata> wrapper, object obj, out Guid refId) public bool TryTrack(TypeMetadataWrapper<TMetadata> wrapper, object obj, out Guid refId)
{ {
Debug.Assert(wrapper.Metadata.IdAccessorType == AcSerializerCommon.IdAccessorType.Guid); Debug.Assert(wrapper.Metadata.IdAccessorType == IdAccessorType.Guid);
// Use pre-cast getter - no cast overhead! // Use pre-cast getter - no cast overhead!
refId = wrapper.RefIdGetterGuid!(obj); refId = wrapper.RefIdGetterGuid!(obj);

View File

@ -87,7 +87,7 @@ public abstract class TypeMetadataBase
/// The accessor type for IId.Id property. /// The accessor type for IId.Id property.
/// Used for fast typed getter dispatch without boxing. /// Used for fast typed getter dispatch without boxing.
/// </summary> /// </summary>
public AcSerializerCommon.IdAccessorType IdAccessorType { get; } public IdAccessorType IdAccessorType { get; }
/// <summary> /// <summary>
/// The Id property info if IsIId is true, null otherwise. /// The Id property info if IsIId is true, null otherwise.
@ -173,17 +173,17 @@ public abstract class TypeMetadataBase
// Create typed getter for the three common Id types to avoid boxing // Create typed getter for the three common Id types to avoid boxing
if (ReferenceEquals(IdType, IntType)) if (ReferenceEquals(IdType, IntType))
{ {
IdAccessorType = AcSerializerCommon.IdAccessorType.Int32; IdAccessorType = IdAccessorType.Int32;
TypedIdGetter = AcSerializerCommon.CreateTypedGetter<int>(type, idProp); TypedIdGetter = AcSerializerCommon.CreateTypedGetter<int>(type, idProp);
} }
else if (ReferenceEquals(IdType, LongType)) else if (ReferenceEquals(IdType, LongType))
{ {
IdAccessorType = AcSerializerCommon.IdAccessorType.Int64; IdAccessorType = IdAccessorType.Int64;
TypedIdGetter = AcSerializerCommon.CreateTypedGetter<long>(type, idProp); TypedIdGetter = AcSerializerCommon.CreateTypedGetter<long>(type, idProp);
} }
else if (ReferenceEquals(IdType, GuidType)) else if (ReferenceEquals(IdType, GuidType))
{ {
IdAccessorType = AcSerializerCommon.IdAccessorType.Guid; IdAccessorType = IdAccessorType.Guid;
TypedIdGetter = AcSerializerCommon.CreateTypedGetter<Guid>(type, idProp); TypedIdGetter = AcSerializerCommon.CreateTypedGetter<Guid>(type, idProp);
} }
else else
@ -195,7 +195,7 @@ public abstract class TypeMetadataBase
{ {
// Non-IId types: use RuntimeHelpers.GetHashCode (int) // Non-IId types: use RuntimeHelpers.GetHashCode (int)
// RefIdGetter is created in TypeMetadataWrapper.CreateRefIdGetter() // RefIdGetter is created in TypeMetadataWrapper.CreateRefIdGetter()
IdAccessorType = AcSerializerCommon.IdAccessorType.Int32; IdAccessorType = IdAccessorType.Int32;
// TypedIdGetter remains null - wrapper uses GetHashCode directly // TypedIdGetter remains null - wrapper uses GetHashCode directly
} }
} }

View File

@ -40,17 +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 AcSerializerCommon.IdentityMap<int>? IdentityMapInt32; internal IdentityMap<int>? 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 AcSerializerCommon.IdentityMap<long>? IdentityMapInt64; internal IdentityMap<long>? 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 AcSerializerCommon.IdentityMap<Guid>? IdentityMapGuid; internal IdentityMap<Guid>? IdentityMapGuid;
#endregion #endregion
@ -90,15 +90,15 @@ public sealed class TypeMetadataWrapper<TMetadata> where TMetadata : TypeMetadat
// Pre-cast typed getters AND set RegisterById delegate - avoids switch in every call // Pre-cast typed getters AND set RegisterById delegate - avoids switch in every call
switch (metadata.IdAccessorType) switch (metadata.IdAccessorType)
{ {
case AcSerializerCommon.IdAccessorType.Int32: case IdAccessorType.Int32:
RefIdGetterInt32 = (Func<object, int>)refIdGetter; RefIdGetterInt32 = (Func<object, int>)refIdGetter;
RegisterById = RegisterByInt32Id; // Method group - no allocation! RegisterById = RegisterByInt32Id; // Method group - no allocation!
break; break;
case AcSerializerCommon.IdAccessorType.Int64: case IdAccessorType.Int64:
RefIdGetterInt64 = (Func<object, long>)refIdGetter; RefIdGetterInt64 = (Func<object, long>)refIdGetter;
RegisterById = RegisterByInt64Id; RegisterById = RegisterByInt64Id;
break; break;
case AcSerializerCommon.IdAccessorType.Guid: case IdAccessorType.Guid:
RefIdGetterGuid = (Func<object, Guid>)refIdGetter; RefIdGetterGuid = (Func<object, Guid>)refIdGetter;
RegisterById = RegisterByGuidId; RegisterById = RegisterByGuidId;
break; break;
@ -151,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 AcSerializerCommon.IdentityMap<int>(); var map = IdentityMapInt32 ??= new IdentityMap<int>();
return map.TryGetOrAddValue(id, newObj); return map.TryGetOrAddValue(id, newObj);
} }
@ -165,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 AcSerializerCommon.IdentityMap<int>(); var map = IdentityMapInt32 ??= new IdentityMap<int>();
map.TryGetOrAddValue(id, instance); map.TryGetOrAddValue(id, instance);
} }
@ -188,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 AcSerializerCommon.IdentityMap<long>(); var map = IdentityMapInt64 ??= new IdentityMap<long>();
return map.TryGetOrAddValue(id, newObj); return map.TryGetOrAddValue(id, newObj);
} }
@ -198,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 AcSerializerCommon.IdentityMap<long>(); var map = IdentityMapInt64 ??= new IdentityMap<long>();
map.TryGetOrAddValue(id, instance); map.TryGetOrAddValue(id, instance);
} }
@ -221,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 AcSerializerCommon.IdentityMap<Guid>(); var map = IdentityMapGuid ??= new IdentityMap<Guid>();
return map.TryGetOrAddValue(id, newObj); return map.TryGetOrAddValue(id, newObj);
} }
@ -231,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 AcSerializerCommon.IdentityMap<Guid>(); var map = IdentityMapGuid ??= new IdentityMap<Guid>();
map.TryGetOrAddValue(id, instance); map.TryGetOrAddValue(id, instance);
} }
@ -244,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 AcSerializerCommon.IdentityMap<TId> GetOrCreateIdentityMap<TId>() where TId : notnull public IdentityMap<TId> 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 AcSerializerCommon.IdentityMap<int>(); var map = IdentityMapInt32 ??= new IdentityMap<int>();
return Unsafe.As<AcSerializerCommon.IdentityMap<int>, AcSerializerCommon.IdentityMap<TId>>(ref map); return Unsafe.As<IdentityMap<int>, IdentityMap<TId>>(ref map);
} }
if (typeof(TId) == typeof(long)) if (typeof(TId) == typeof(long))
{ {
var map = IdentityMapInt64 ??= new AcSerializerCommon.IdentityMap<long>(); var map = IdentityMapInt64 ??= new IdentityMap<long>();
return Unsafe.As<AcSerializerCommon.IdentityMap<long>, AcSerializerCommon.IdentityMap<TId>>(ref map); return Unsafe.As<IdentityMap<long>, IdentityMap<TId>>(ref map);
} }
if (typeof(TId) == typeof(Guid)) if (typeof(TId) == typeof(Guid))
{ {
var map = IdentityMapGuid ??= new AcSerializerCommon.IdentityMap<Guid>(); var map = IdentityMapGuid ??= new IdentityMap<Guid>();
return Unsafe.As<AcSerializerCommon.IdentityMap<Guid>, AcSerializerCommon.IdentityMap<TId>>(ref map); return Unsafe.As<IdentityMap<Guid>, IdentityMap<TId>>(ref map);
} }
throw new NotSupportedException($"Id type {typeof(TId)} is not supported"); throw new NotSupportedException($"Id type {typeof(TId)} is not supported");