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;
options.UseStringInterning = StringInterningMode.None;
byte[] bytes = AcBinarySerializer.Serialize(order, options);
// Warmup (fills caches)
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);
_ = AcBinaryDeserializer.Deserialize<TestOrder>(bytes);
}
Thread.Sleep(2000);
System.Console.WriteLine("Warmup complete. Caches are now populated.");
System.Console.WriteLine();
// 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++)
{
_ = 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();

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
#region Chain Reference Tracking

View File

@ -39,9 +39,9 @@ public static partial class AcBinaryDeserializer
return metadata.IdAccessorType switch
{
AcSerializerCommon.IdAccessorType.Int32 => TryGetOrStoreInt32(wrapper, newObj, metadata.GetIdInt32(newObj)),
AcSerializerCommon.IdAccessorType.Int64 => TryGetOrStoreLong(wrapper, newObj, metadata.GetIdInt64(newObj)),
AcSerializerCommon.IdAccessorType.Guid => TryGetOrStoreGuid(wrapper, newObj, metadata.GetIdGuid(newObj)),
IdAccessorType.Int32 => TryGetOrStoreInt32(wrapper, newObj, metadata.GetIdInt32(newObj)),
IdAccessorType.Int64 => TryGetOrStoreLong(wrapper, newObj, metadata.GetIdInt64(newObj)),
IdAccessorType.Guid => TryGetOrStoreGuid(wrapper, newObj, metadata.GetIdGuid(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.
/// </summary>
[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)
{
case AcSerializerCommon.IdAccessorType.Int32:
case IdAccessorType.Int32:
propInfo.SetInt32(target, context.ReadVarInt());
break;
case AcSerializerCommon.IdAccessorType.Int64:
case IdAccessorType.Int64:
propInfo.SetInt64(target, context.ReadVarLong());
break;
case AcSerializerCommon.IdAccessorType.Guid:
case IdAccessorType.Guid:
propInfo.SetGuid(target, context.ReadGuidUnsafe());
break;
}

View File

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

View File

@ -64,7 +64,7 @@ public static partial class AcBinarySerializer
/// </summary>
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 PropertyStateBufferMaxCache = 512;
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
switch (metadata.IdAccessorType)
{
case AcSerializerCommon.IdAccessorType.Int32:
case IdAccessorType.Int32:
if (!context.TryTrack(wrapper, value, out int intId))
{
// Already seen → ObjectRef + Id
@ -857,7 +857,7 @@ public static partial class AcBinarySerializer
context.WriteByte(BinaryTypeCode.Object);
break;
case AcSerializerCommon.IdAccessorType.Int64:
case IdAccessorType.Int64:
if (!context.TryTrack(wrapper, value, out long longId))
{
context.WriteByte(BinaryTypeCode.ObjectRef);
@ -867,7 +867,7 @@ public static partial class AcBinarySerializer
context.WriteByte(BinaryTypeCode.Object);
break;
case AcSerializerCommon.IdAccessorType.Guid:
case IdAccessorType.Guid:
if (!context.TryTrack(wrapper, value, out Guid guidId))
{
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
@ -29,10 +31,11 @@
// void Reset();
//}
///// <summary>
///// High-performance identity map for tracking IId values during serialization/deserialization.
///// 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
///// No Dictionary overhead, no per-entry allocation.
///// </summary>
@ -41,24 +44,27 @@
//{
// // Small int optimization (TId = int only, 0-4095)
// 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
// private struct Slot
// // Slot for hash table entries (generation needed for hash table validity)
// private struct HashSlot
// {
// public object? Value; // stored object for deserialization
// public object? Value;
// public int Next; // next slot index in chain (-1 = end)
// }
// // Small int storage (only used when TId is int)
// private ulong[]? _smallBitmap; // quick "seen?" check
// private Slot[]? _smallSlots; // direct indexed storage
// // Small int storage - SIMPLE: bitmap for "seen?" + direct object[] for values
// // 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 object?[]? _smallValues; // Direct value storage (NOT cleared on Reset - bitmap protects)
// // Hash table storage (for large ints and other types)
// 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 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;
@ -100,8 +106,11 @@
// // Lazy init
// if (_smallBitmap == null)
// {
// _smallBitmap = new ulong[SmallBitmapSize];
// _smallSlots = new Slot[SmallSize];
// _smallBitmap = ArrayPool<ulong>.Shared.Rent(SmallBitmapSize);
// Array.Clear(_smallBitmap, 0, SmallBitmapSize);
// _smallValues = ArrayPool<object?>.Shared.Rent(SmallSize);
// // No clear needed - bitmap protects values
// }
// var wordIdx = key >> 6;
@ -109,13 +118,41 @@
// ref var word = ref _smallBitmap[wordIdx];
// if ((word & bit) != 0)
// return false; // already seen
// return false; // Already seen (bitmap is cleared on Reset)
// word |= bit;
// _smallSlots![key] = default; // init slot
// 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)]
// private bool TryAddHash(TId key, out int slotIndex)
// {
@ -132,7 +169,8 @@
// // Search chain
// 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;
// return false; // already seen
@ -140,16 +178,16 @@
// }
// // Resize if needed
// if (_count >= _entries!.Length)
// if (_count >= _entriesLength)
// {
// Resize();
// bucketIdx = (hash & 0x7FFFFFFF) % _buckets.Length;
// bucketIdx = (hash & 0x7FFFFFFF) % _bucketsLength;
// }
// // Add new entry
// slotIndex = _count++;
// _keys![slotIndex] = key;
// _entries[slotIndex] = new Slot { Next = _buckets[bucketIdx] };
// _entries![slotIndex] = new HashSlot { Next = _buckets[bucketIdx] };
// _buckets[bucketIdx] = slotIndex;
// return true;
// }
@ -157,31 +195,41 @@
// [MethodImpl(MethodImplOptions.AggressiveInlining)]
// 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 (IsInt64) return Unsafe.As<TId, long>(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();
// }
// private void InitHashTable(int capacity)
// {
// _buckets = new int[capacity];
// Array.Fill(_buckets, -1);
// _entries = new Slot[capacity];
// _keys = new TId[capacity];
// _buckets = ArrayPool<int>.Shared.Rent(capacity);
// _bucketsLength = capacity;
// Array.Fill(_buckets, -1, 0, capacity); // MUST fill with -1
// _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;
// }
// private void Resize()
// {
// var newCapacity = _buckets!.Length * 2;
// var newBuckets = new int[newCapacity];
// Array.Fill(newBuckets, -1);
// var newEntries = new Slot[newCapacity];
// var newKeys = new TId[newCapacity];
// var newCapacity = _bucketsLength * 2;
// // 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(_keys!, newKeys, _count);
@ -193,15 +241,22 @@
// 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;
// _bucketsLength = newCapacity;
// _entries = newEntries;
// _entriesLength = newCapacity;
// _keys = newKeys;
// }
// [MethodImpl(MethodImplOptions.AggressiveInlining)]
// public bool HasKey(TId key)
// {
// // Small int fast path
// // Small int fast path - bitmap check is cache-friendly (512 bytes)
// if (IsInt32)
// {
// var intKey = Unsafe.As<TId, int>(ref key);
@ -221,7 +276,7 @@
// 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 false;
@ -261,8 +316,10 @@
// // Lazy init
// if (_smallBitmap == null)
// {
// _smallBitmap = new ulong[SmallBitmapSize];
// _smallSlots = new Slot[SmallSize];
// _smallBitmap = ArrayPool<ulong>.Shared.Rent(SmallBitmapSize);
// Array.Clear(_smallBitmap, 0, SmallBitmapSize);
// _smallValues = ArrayPool<object?>.Shared.Rent(SmallSize);
// }
// var wordIdx = key >> 6;
@ -271,12 +328,14 @@
// ref var word = ref _smallBitmap[wordIdx];
// if ((word & bit) != 0)
// {
// existing = _smallSlots![key].Value;
// return false; // already seen
// // Bitmap says seen - value is valid
// existing = _smallValues![key];
// return false; // Already seen
// }
// // First occurrence - mark as seen
// word |= bit;
// _smallSlots![key] = default;
// _smallValues![key] = null;
// existing = null;
// return true;
// }
@ -314,8 +373,10 @@
// // Lazy init
// if (_smallBitmap == null)
// {
// _smallBitmap = new ulong[SmallBitmapSize];
// _smallSlots = new Slot[SmallSize];
// _smallBitmap = ArrayPool<ulong>.Shared.Rent(SmallBitmapSize);
// Array.Clear(_smallBitmap, 0, SmallBitmapSize);
// _smallValues = ArrayPool<object?>.Shared.Rent(SmallSize);
// }
// var wordIdx = key >> 6;
@ -324,12 +385,15 @@
// ref var word = ref _smallBitmap[wordIdx];
// if ((word & bit) != 0)
// {
// var existing = _smallSlots![key].Value;
// if (existing != null) return existing;
// // 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;
// _smallSlots![key].Value = newValue;
// _smallValues![key] = newValue;
// return newValue;
// }
@ -340,7 +404,7 @@
// [MethodImpl(MethodImplOptions.AggressiveInlining)]
// 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)
// {
// var intKey = Unsafe.As<TId, int>(ref key);
@ -350,7 +414,8 @@
// var bit = 1UL << (intKey & 63);
// if ((_smallBitmap[wordIdx] & bit) != 0)
// {
// value = _smallSlots![intKey].Value;
// // Bitmap says seen - value is valid
// value = _smallValues![intKey];
// return true;
// }
// value = null;
@ -370,7 +435,7 @@
// 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;
// return true;
@ -382,25 +447,133 @@
// /// <summary>
// /// Resets the identity map for reuse.
// /// Fast Reset: only bitmap clear (512 bytes).
// /// No value array clearing needed - bitmap protects values!
// /// </summary>
// 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)
// {
// Array.Clear(_smallBitmap);
// Array.Clear(_smallSlots!);
// Array.Clear(_smallBitmap, 0, SmallBitmapSize);
// }
// // Clear hash table
// // Hash table reset
// if (_buckets != null)
// {
// Array.Fill(_buckets, -1);
// Array.Clear(_entries!);
// Array.Clear(_keys!);
// Array.Fill(_buckets, -1, 0, _bucketsLength);
// // Clear entries and keys to release object refs (O(_count), not O(capacity))
// Array.Clear(_entries!, 0, _count);
// Array.Clear(_keys!, 0, _count);
// _count = 0;
// }
// }
//}
//#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)]
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!
refId = wrapper.RefIdGetterInt32!(obj);
@ -80,7 +80,7 @@ public abstract class SerializationContextBase<TMetadata, TOptions> : AcSerializ
[MethodImpl(MethodImplOptions.AggressiveInlining)]
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!
refId = wrapper.RefIdGetterInt64!(obj);
@ -96,7 +96,7 @@ public abstract class SerializationContextBase<TMetadata, TOptions> : AcSerializ
[MethodImpl(MethodImplOptions.AggressiveInlining)]
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!
refId = wrapper.RefIdGetterGuid!(obj);

View File

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

View File

@ -40,17 +40,17 @@ public sealed class TypeMetadataWrapper<TMetadata> where TMetadata : TypeMetadat
/// <summary>
/// Typed IdentityMap for Int32 IDs. Direct access, no type check.
/// </summary>
internal AcSerializerCommon.IdentityMap<int>? IdentityMapInt32;
internal IdentityMap<int>? IdentityMapInt32;
/// <summary>
/// Typed IdentityMap for Int64 IDs. Direct access, no type check.
/// </summary>
internal AcSerializerCommon.IdentityMap<long>? IdentityMapInt64;
internal IdentityMap<long>? IdentityMapInt64;
/// <summary>
/// Typed IdentityMap for Guid IDs. Direct access, no type check.
/// </summary>
internal AcSerializerCommon.IdentityMap<Guid>? IdentityMapGuid;
internal IdentityMap<Guid>? IdentityMapGuid;
#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
switch (metadata.IdAccessorType)
{
case AcSerializerCommon.IdAccessorType.Int32:
case IdAccessorType.Int32:
RefIdGetterInt32 = (Func<object, int>)refIdGetter;
RegisterById = RegisterByInt32Id; // Method group - no allocation!
break;
case AcSerializerCommon.IdAccessorType.Int64:
case IdAccessorType.Int64:
RefIdGetterInt64 = (Func<object, long>)refIdGetter;
RegisterById = RegisterByInt64Id;
break;
case AcSerializerCommon.IdAccessorType.Guid:
case IdAccessorType.Guid:
RefIdGetterGuid = (Func<object, Guid>)refIdGetter;
RegisterById = RegisterByGuidId;
break;
@ -151,7 +151,7 @@ public sealed class TypeMetadataWrapper<TMetadata> where TMetadata : TypeMetadat
{
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);
}
@ -165,7 +165,7 @@ public sealed class TypeMetadataWrapper<TMetadata> where TMetadata : TypeMetadat
var id = RefIdGetterInt32!(instance);
if (id == 0) return;
var map = IdentityMapInt32 ??= new AcSerializerCommon.IdentityMap<int>();
var map = IdentityMapInt32 ??= new IdentityMap<int>();
map.TryGetOrAddValue(id, instance);
}
@ -188,7 +188,7 @@ public sealed class TypeMetadataWrapper<TMetadata> where TMetadata : TypeMetadat
{
if (id == 0) return newObj;
var map = IdentityMapInt64 ??= new AcSerializerCommon.IdentityMap<long>();
var map = IdentityMapInt64 ??= new IdentityMap<long>();
return map.TryGetOrAddValue(id, newObj);
}
@ -198,7 +198,7 @@ public sealed class TypeMetadataWrapper<TMetadata> where TMetadata : TypeMetadat
var id = RefIdGetterInt64!(instance);
if (id == 0) return;
var map = IdentityMapInt64 ??= new AcSerializerCommon.IdentityMap<long>();
var map = IdentityMapInt64 ??= new IdentityMap<long>();
map.TryGetOrAddValue(id, instance);
}
@ -221,7 +221,7 @@ public sealed class TypeMetadataWrapper<TMetadata> where TMetadata : TypeMetadat
{
if (id == Guid.Empty) return newObj;
var map = IdentityMapGuid ??= new AcSerializerCommon.IdentityMap<Guid>();
var map = IdentityMapGuid ??= new IdentityMap<Guid>();
return map.TryGetOrAddValue(id, newObj);
}
@ -231,7 +231,7 @@ public sealed class TypeMetadataWrapper<TMetadata> where TMetadata : TypeMetadat
var id = RefIdGetterGuid!(instance);
if (id == Guid.Empty) return;
var map = IdentityMapGuid ??= new AcSerializerCommon.IdentityMap<Guid>();
var map = IdentityMapGuid ??= new IdentityMap<Guid>();
map.TryGetOrAddValue(id, instance);
}
@ -244,23 +244,23 @@ public sealed class TypeMetadataWrapper<TMetadata> where TMetadata : TypeMetadat
/// SLOW PATH - use typed methods (TryGetValueInt32, etc.) instead!
/// </summary>
[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
if (typeof(TId) == typeof(int))
{
var map = IdentityMapInt32 ??= new AcSerializerCommon.IdentityMap<int>();
return Unsafe.As<AcSerializerCommon.IdentityMap<int>, AcSerializerCommon.IdentityMap<TId>>(ref map);
var map = IdentityMapInt32 ??= new IdentityMap<int>();
return Unsafe.As<IdentityMap<int>, IdentityMap<TId>>(ref map);
}
if (typeof(TId) == typeof(long))
{
var map = IdentityMapInt64 ??= new AcSerializerCommon.IdentityMap<long>();
return Unsafe.As<AcSerializerCommon.IdentityMap<long>, AcSerializerCommon.IdentityMap<TId>>(ref map);
var map = IdentityMapInt64 ??= new IdentityMap<long>();
return Unsafe.As<IdentityMap<long>, IdentityMap<TId>>(ref map);
}
if (typeof(TId) == typeof(Guid))
{
var map = IdentityMapGuid ??= new AcSerializerCommon.IdentityMap<Guid>();
return Unsafe.As<AcSerializerCommon.IdentityMap<Guid>, AcSerializerCommon.IdentityMap<TId>>(ref map);
var map = IdentityMapGuid ??= new IdentityMap<Guid>();
return Unsafe.As<IdentityMap<Guid>, IdentityMap<TId>>(ref map);
}
throw new NotSupportedException($"Id type {typeof(TId)} is not supported");