From 946148cc3d33ab0e522bbdb7936881ccdd6d1e69 Mon Sep 17 00:00:00 2001 From: Loretta Date: Thu, 29 Jan 2026 14:13:31 +0100 Subject: [PATCH] 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. --- AyCode.Core.Serializers.Console/Program.cs | 16 +- AyCode.Core/Serializers/AcSerializerCommon.cs | 104 ------- ...lizer.BinaryDeserializationContextClass.cs | 6 +- .../Binaries/AcBinaryDeserializer.Populate.cs | 8 +- .../Binaries/AcBinaryDeserializer.cs | 12 +- ...rySerializer.BinarySerializationContext.cs | 2 +- .../Binaries/AcBinarySerializer.cs | 6 +- AyCode.Core/Serializers/IdentityMap.cs | 281 ++++++++++++++---- .../Serializers/SerializationContextBase.cs | 6 +- AyCode.Core/Serializers/TypeMetadataBase.cs | 10 +- .../Serializers/TypeMetadataWrapper.cs | 38 +-- 11 files changed, 285 insertions(+), 204 deletions(-) diff --git a/AyCode.Core.Serializers.Console/Program.cs b/AyCode.Core.Serializers.Console/Program.cs index 27acf85..54f94a0 100644 --- a/AyCode.Core.Serializers.Console/Program.cs +++ b/AyCode.Core.Serializers.Console/Program.cs @@ -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(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(bytes); } + + System.Console.WriteLine("Running hot path deserialization (1000 iterations for profiling)..."); + for (var i = 0; i < 1000; i++) + { + _ = AcBinaryDeserializer.Deserialize(bytes); + } + System.Console.WriteLine("Hot path complete."); System.Console.WriteLine(); diff --git a/AyCode.Core/Serializers/AcSerializerCommon.cs b/AyCode.Core/Serializers/AcSerializerCommon.cs index 3f4e0ef..29bfb55 100644 --- a/AyCode.Core/Serializers/AcSerializerCommon.cs +++ b/AyCode.Core/Serializers/AcSerializerCommon.cs @@ -586,110 +586,6 @@ public static class AcSerializerCommon } - #endregion - - #region IId Reference Tracking - - /// - /// Specifies the accessor type for IId.Id property to enable typed getter dispatch without boxing. - /// - public enum IdAccessorType : byte - { - None = 0, - /// Id is int (most common). - Int32 = 1, - /// Id is long. - Int64 = 2, - /// Id is Guid. - Guid = 3, - } - - /// - /// Interface for identity maps used in serialization tracking. - /// Enables type-safe Reset() without knowing the generic type parameter. - /// - public interface IIdentityMap - { - /// - /// Resets the identity map for reuse between serializations. - /// - void Reset(); - } - - /// - /// Generic identity map for tracking IId values during serialization/deserialization. - /// Uses Dictionary internally for unified tracking + object storage. - /// - /// The ID type (int, long, Guid) - public sealed class IdentityMap : IIdentityMap where TId : notnull - { - private readonly Dictionary _tracked; - - public IdentityMap() - { - _tracked = new Dictionary(EqualityComparer.Default); - } - - /// - /// Tries to add a key to tracking (serialization). - /// Returns true if first occurrence (key was added). - /// Returns false if already seen. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool TryAddKey(TId key) - { - return _tracked.TryAdd(key, null); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool HasKey(TId key) => _tracked.ContainsKey(key); - - - /// - /// 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). - /// - [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; - } - - /// - /// 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. - /// - [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; - } - - /// - /// Tries to get the value for a key (ObjectRef lookup). - /// Returns true if found, false if not. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool TryGetValue(TId key, out object? value) - { - return _tracked.TryGetValue(key, out value); - } - - /// - /// Resets the identity map for reuse. - /// - public void Reset() - { - _tracked.Clear(); - } - } - #endregion #region Chain Reference Tracking diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContextClass.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContextClass.cs index acf654d..39ff9ce 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContextClass.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContextClass.cs @@ -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 }; } diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.Populate.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.Populate.cs index 3006412..50cdd0d 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.Populate.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.Populate.cs @@ -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. /// [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; } diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs index 19ed51b..b3965cf 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs @@ -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 }; } diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs index bc364b2..89d14ff 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs @@ -64,7 +64,7 @@ public static partial class AcBinarySerializer /// internal sealed class BinarySerializationContext : SerializationContextBase, 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; diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs index 87a9f36..81ffa59 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs @@ -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); diff --git a/AyCode.Core/Serializers/IdentityMap.cs b/AyCode.Core/Serializers/IdentityMap.cs index acca254..b71e206 100644 --- a/AyCode.Core/Serializers/IdentityMap.cs +++ b/AyCode.Core/Serializers/IdentityMap.cs @@ -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(); //} + ///// ///// 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. ///// @@ -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 int Next; // next slot index in chain (-1 = end) +// 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.Shared.Rent(SmallBitmapSize); +// Array.Clear(_smallBitmap, 0, SmallBitmapSize); + +// _smallValues = ArrayPool.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(ref a) == Unsafe.As(ref b); +// } +// if (IsString) +// { +// var strA = Unsafe.As(ref a); +// var strB = Unsafe.As(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(ref a) == Unsafe.As(ref b); +// } +// if (IsGuid) +// { +// return Unsafe.As(ref a) == Unsafe.As(ref b); +// } +// // Fallback for other types +// return EqualityComparer.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.Default.Equals(_keys![i], key)) +// //if (EqualityComparer.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(ref key); // if (IsInt64) return Unsafe.As(ref key).GetHashCode(); // if (IsGuid) return Unsafe.As(ref key).GetHashCode(); +// if (IsString) return string.GetHashCode(Unsafe.As(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.Shared.Rent(capacity); +// _bucketsLength = capacity; +// Array.Fill(_buckets, -1, 0, capacity); // MUST fill with -1 + +// _entries = ArrayPool.Shared.Rent(capacity); +// _entriesLength = capacity; +// // No clear needed - generation counter handles validity + +// _keys = ArrayPool.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.Shared.Rent(newCapacity); +// Array.Fill(newBuckets, -1, 0, newCapacity); // MUST fill with -1 +// var newEntries = ArrayPool.Shared.Rent(newCapacity); +// var newKeys = ArrayPool.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.Shared.Return(_buckets!); +// ArrayPool.Shared.Return(_entries!); +// ArrayPool.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(ref key); @@ -221,7 +276,7 @@ // for (var i = _buckets[bucketIdx]; i >= 0; i = _entries![i].Next) // { -// if (EqualityComparer.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.Shared.Rent(SmallBitmapSize); +// Array.Clear(_smallBitmap, 0, SmallBitmapSize); + +// _smallValues = ArrayPool.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.Shared.Rent(SmallBitmapSize); +// Array.Clear(_smallBitmap, 0, SmallBitmapSize); + +// _smallValues = ArrayPool.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(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.Default.Equals(_keys![i], key)) +// if (KeyEquals(_keys![i], key)) // { // value = _entries[i].Value; // return true; @@ -382,25 +447,133 @@ // /// // /// Resets the identity map for reuse. +// /// Fast Reset: only bitmap clear (512 bytes). +// /// No value array clearing needed - bitmap protects values! // /// // 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 \ No newline at end of file +//#endregion + + +#region IId Reference Tracking + +/// +/// Specifies the accessor type for IId.Id property to enable typed getter dispatch without boxing. +/// +public enum IdAccessorType : byte +{ + None = 0, + /// Id is int (most common). + Int32 = 1, + /// Id is long. + Int64 = 2, + /// Id is Guid. + Guid = 3, +} + +/// +/// Interface for identity maps used in serialization tracking. +/// Enables type-safe Reset() without knowing the generic type parameter. +/// +public interface IIdentityMap +{ + /// + /// Resets the identity map for reuse between serializations. + /// + void Reset(); +} + +/// +/// Generic identity map for tracking IId values during serialization/deserialization. +/// Uses Dictionary internally for unified tracking + object storage. +/// +/// The ID type (int, long, Guid) +public sealed class IdentityMap : IIdentityMap where TId : notnull +{ + private readonly Dictionary _tracked; + + public IdentityMap() + { + _tracked = new Dictionary(EqualityComparer.Default); + } + + /// + /// Tries to add a key to tracking (serialization). + /// Returns true if first occurrence (key was added). + /// Returns false if already seen. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryAddKey(TId key) + { + return _tracked.TryAdd(key, null); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool HasKey(TId key) => _tracked.ContainsKey(key); + + + /// + /// 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). + /// + [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; + } + + /// + /// 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. + /// + [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; + } + + /// + /// Tries to get the value for a key (ObjectRef lookup). + /// Returns true if found, false if not. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryGetValue(TId key, out object? value) + { + return _tracked.TryGetValue(key, out value); + } + + /// + /// Resets the identity map for reuse. + /// + public void Reset() + { + _tracked.Clear(); + } +} + +#endregion \ No newline at end of file diff --git a/AyCode.Core/Serializers/SerializationContextBase.cs b/AyCode.Core/Serializers/SerializationContextBase.cs index 82a9c8d..c769eb6 100644 --- a/AyCode.Core/Serializers/SerializationContextBase.cs +++ b/AyCode.Core/Serializers/SerializationContextBase.cs @@ -37,7 +37,7 @@ public abstract class SerializationContextBase : AcSerializ [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool TryTrack(TypeMetadataWrapper 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 : AcSerializ [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool TryTrack(TypeMetadataWrapper 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 : AcSerializ [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool TryTrack(TypeMetadataWrapper 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); diff --git a/AyCode.Core/Serializers/TypeMetadataBase.cs b/AyCode.Core/Serializers/TypeMetadataBase.cs index 205a23c..7d70b24 100644 --- a/AyCode.Core/Serializers/TypeMetadataBase.cs +++ b/AyCode.Core/Serializers/TypeMetadataBase.cs @@ -87,7 +87,7 @@ public abstract class TypeMetadataBase /// The accessor type for IId.Id property. /// Used for fast typed getter dispatch without boxing. /// - public AcSerializerCommon.IdAccessorType IdAccessorType { get; } + public IdAccessorType IdAccessorType { get; } /// /// 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(type, idProp); } else if (ReferenceEquals(IdType, LongType)) { - IdAccessorType = AcSerializerCommon.IdAccessorType.Int64; + IdAccessorType = IdAccessorType.Int64; TypedIdGetter = AcSerializerCommon.CreateTypedGetter(type, idProp); } else if (ReferenceEquals(IdType, GuidType)) { - IdAccessorType = AcSerializerCommon.IdAccessorType.Guid; + IdAccessorType = IdAccessorType.Guid; TypedIdGetter = AcSerializerCommon.CreateTypedGetter(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 } } diff --git a/AyCode.Core/Serializers/TypeMetadataWrapper.cs b/AyCode.Core/Serializers/TypeMetadataWrapper.cs index 73ca349..6a7bead 100644 --- a/AyCode.Core/Serializers/TypeMetadataWrapper.cs +++ b/AyCode.Core/Serializers/TypeMetadataWrapper.cs @@ -40,17 +40,17 @@ public sealed class TypeMetadataWrapper where TMetadata : TypeMetadat /// /// Typed IdentityMap for Int32 IDs. Direct access, no type check. /// - internal AcSerializerCommon.IdentityMap? IdentityMapInt32; + internal IdentityMap? IdentityMapInt32; /// /// Typed IdentityMap for Int64 IDs. Direct access, no type check. /// - internal AcSerializerCommon.IdentityMap? IdentityMapInt64; + internal IdentityMap? IdentityMapInt64; /// /// Typed IdentityMap for Guid IDs. Direct access, no type check. /// - internal AcSerializerCommon.IdentityMap? IdentityMapGuid; + internal IdentityMap? IdentityMapGuid; #endregion @@ -90,15 +90,15 @@ public sealed class TypeMetadataWrapper 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)refIdGetter; RegisterById = RegisterByInt32Id; // Method group - no allocation! break; - case AcSerializerCommon.IdAccessorType.Int64: + case IdAccessorType.Int64: RefIdGetterInt64 = (Func)refIdGetter; RegisterById = RegisterByInt64Id; break; - case AcSerializerCommon.IdAccessorType.Guid: + case IdAccessorType.Guid: RefIdGetterGuid = (Func)refIdGetter; RegisterById = RegisterByGuidId; break; @@ -151,7 +151,7 @@ public sealed class TypeMetadataWrapper where TMetadata : TypeMetadat { if (id == 0) return newObj; // Default Id - no tracking - var map = IdentityMapInt32 ??= new AcSerializerCommon.IdentityMap(); + var map = IdentityMapInt32 ??= new IdentityMap(); return map.TryGetOrAddValue(id, newObj); } @@ -165,7 +165,7 @@ public sealed class TypeMetadataWrapper where TMetadata : TypeMetadat var id = RefIdGetterInt32!(instance); if (id == 0) return; - var map = IdentityMapInt32 ??= new AcSerializerCommon.IdentityMap(); + var map = IdentityMapInt32 ??= new IdentityMap(); map.TryGetOrAddValue(id, instance); } @@ -188,7 +188,7 @@ public sealed class TypeMetadataWrapper where TMetadata : TypeMetadat { if (id == 0) return newObj; - var map = IdentityMapInt64 ??= new AcSerializerCommon.IdentityMap(); + var map = IdentityMapInt64 ??= new IdentityMap(); return map.TryGetOrAddValue(id, newObj); } @@ -198,7 +198,7 @@ public sealed class TypeMetadataWrapper where TMetadata : TypeMetadat var id = RefIdGetterInt64!(instance); if (id == 0) return; - var map = IdentityMapInt64 ??= new AcSerializerCommon.IdentityMap(); + var map = IdentityMapInt64 ??= new IdentityMap(); map.TryGetOrAddValue(id, instance); } @@ -221,7 +221,7 @@ public sealed class TypeMetadataWrapper where TMetadata : TypeMetadat { if (id == Guid.Empty) return newObj; - var map = IdentityMapGuid ??= new AcSerializerCommon.IdentityMap(); + var map = IdentityMapGuid ??= new IdentityMap(); return map.TryGetOrAddValue(id, newObj); } @@ -231,7 +231,7 @@ public sealed class TypeMetadataWrapper where TMetadata : TypeMetadat var id = RefIdGetterGuid!(instance); if (id == Guid.Empty) return; - var map = IdentityMapGuid ??= new AcSerializerCommon.IdentityMap(); + var map = IdentityMapGuid ??= new IdentityMap(); map.TryGetOrAddValue(id, instance); } @@ -244,23 +244,23 @@ public sealed class TypeMetadataWrapper where TMetadata : TypeMetadat /// SLOW PATH - use typed methods (TryGetValueInt32, etc.) instead! /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public AcSerializerCommon.IdentityMap GetOrCreateIdentityMap() where TId : notnull + public IdentityMap GetOrCreateIdentityMap() where TId : notnull { // Route to typed fields based on TId if (typeof(TId) == typeof(int)) { - var map = IdentityMapInt32 ??= new AcSerializerCommon.IdentityMap(); - return Unsafe.As, AcSerializerCommon.IdentityMap>(ref map); + var map = IdentityMapInt32 ??= new IdentityMap(); + return Unsafe.As, IdentityMap>(ref map); } if (typeof(TId) == typeof(long)) { - var map = IdentityMapInt64 ??= new AcSerializerCommon.IdentityMap(); - return Unsafe.As, AcSerializerCommon.IdentityMap>(ref map); + var map = IdentityMapInt64 ??= new IdentityMap(); + return Unsafe.As, IdentityMap>(ref map); } if (typeof(TId) == typeof(Guid)) { - var map = IdentityMapGuid ??= new AcSerializerCommon.IdentityMap(); - return Unsafe.As, AcSerializerCommon.IdentityMap>(ref map); + var map = IdentityMapGuid ??= new IdentityMap(); + return Unsafe.As, IdentityMap>(ref map); } throw new NotSupportedException($"Id type {typeof(TId)} is not supported");