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");