diff --git a/AyCode.Core.Serializers.Console/Program.cs b/AyCode.Core.Serializers.Console/Program.cs index 54f94a0..d9245e9 100644 --- a/AyCode.Core.Serializers.Console/Program.cs +++ b/AyCode.Core.Serializers.Console/Program.cs @@ -339,11 +339,6 @@ public static class Program } } - - - - - private static TestDataSet CreateDeepNestedTestData() { TestDataFactory.ResetIdCounter(); diff --git a/AyCode.Core/Serializers/AcSerializerContextBase.cs b/AyCode.Core/Serializers/AcSerializerContextBase.cs index 29d7506..b0a77f5 100644 --- a/AyCode.Core/Serializers/AcSerializerContextBase.cs +++ b/AyCode.Core/Serializers/AcSerializerContextBase.cs @@ -137,16 +137,21 @@ public abstract class AcSerializerContextBase #region Reset /// - /// Resets all wrapper tracking states for reuse. - /// Does not remove wrappers - keeps them for next operation. + /// Resets context for new operation. Sets options only. /// public virtual void Reset(TOptions options) { Options = options; + } + /// + /// Clears tracking state for pool return. Returns IdentityMap arrays to pool. + /// + public virtual void Clear() + { foreach (var wrapper in _wrappers.Values) { - wrapper.ResetTracking(); + wrapper.ResetTracking(Options.UseAsync); } } diff --git a/AyCode.Core/Serializers/AcSerializerOptions.cs b/AyCode.Core/Serializers/AcSerializerOptions.cs index f955ff7..bd3308b 100644 --- a/AyCode.Core/Serializers/AcSerializerOptions.cs +++ b/AyCode.Core/Serializers/AcSerializerOptions.cs @@ -1,4 +1,5 @@ -using System.Reflection; +using System; +using System.Reflection; namespace AyCode.Core.Serializers; @@ -11,7 +12,29 @@ public abstract class AcSerializerOptions /// Default: OnlyId (JSON serializer requires All mode, OnlyId not yet implemented) /// Note: Binary serializer supports OnlyId mode for IId-only tracking. /// - public ReferenceHandlingMode ReferenceHandling { get; set; } = ReferenceHandlingMode.OnlyId; + public ReferenceHandlingMode ReferenceHandling + { + get => _referenceHandling; + set => _referenceHandling = value; + } + + private ReferenceHandlingMode _referenceHandling = ReferenceHandlingMode.OnlyId; + private readonly byte _maxDepth = byte.MaxValue; + private readonly bool _throwOnCircularReference = true; + private readonly PropertyMapperDelegate? _propertyMapper; + + private bool _useAsync = false; + + public bool UseAsync + { + get => _useAsync && ReferenceHandling != ReferenceHandlingMode.None; + init => _useAsync = !DetectedIsWasm && value; + } + + /// + /// Cached platform detection - true if running in WebAssembly/Browser environment. + /// + protected static readonly bool DetectedIsWasm = OperatingSystem.IsBrowser(); /// /// Maximum depth for serialization/deserialization. @@ -20,7 +43,11 @@ public abstract class AcSerializerOptions /// byte.MaxValue (255) = effectively unlimited /// Default: byte.MaxValue /// - public byte MaxDepth { get; init; } = byte.MaxValue; + public byte MaxDepth + { + get => _maxDepth; + init => _maxDepth = value; + } /// /// Throw exception on circular reference detection for non-IId types. @@ -29,7 +56,11 @@ public abstract class AcSerializerOptions /// Default: true (production safety) /// Note: IId types are always tracked when ReferenceHandling != None. /// - public bool ThrowOnCircularReference { get; init; } = true; + public bool ThrowOnCircularReference + { + get => _throwOnCircularReference; + init => _throwOnCircularReference = value; + } /// /// Optional callback for custom property mapping during cross-type operations. @@ -45,7 +76,11 @@ public abstract class AcSerializerOptions /// /// Performance: ZERO overhead on same-type operations (Deserialize<T>). /// - public PropertyMapperDelegate? PropertyMapper { get; init; } + public PropertyMapperDelegate? PropertyMapper + { + get => _propertyMapper; + init => _propertyMapper = value; + } } public enum AcSerializerType : byte diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContextClass.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContextClass.cs index 39ff9ce..4f57331 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContextClass.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContextClass.cs @@ -21,8 +21,12 @@ public static partial class AcBinaryDeserializer public BinaryDeserializationContextClass() { - // Initialize with default options - will be reset with actual options before use - Reset(AcBinarySerializerOptions.Default); + } + + public override void Reset(AcBinarySerializerOptions options) + { + Clear(); + base.Reset(options); } /// diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs index 89d14ff..02f91b5 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs @@ -7,6 +7,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Intrinsics; using System.Text; +using System.Threading; using static AyCode.Core.Helpers.JsonUtilities; namespace AyCode.Core.Serializers.Binaries; @@ -30,6 +31,12 @@ public static partial class AcBinarySerializer return new BinarySerializationContext(options); } + public static void ReturnAsync(BinarySerializationContext context) + { + // 🔥 FIRE-AND-FORGET: cleanup háttérben + ThreadPool.UnsafeQueueUserWorkItem(Return, context, preferLocal: true); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Return(BinarySerializationContext context) { @@ -170,7 +177,7 @@ public static partial class AcBinarySerializer } } - public void Clear() + public override void Clear() { _position = 0; @@ -193,9 +200,8 @@ public static partial class AcBinarySerializer _propertyStateBuffer = null; } - // NOTE: GrowBufferCount �s GrowBufferTotalBytes nem null�z�dik itt, - // hogy a m�r�sek v�g�n ki tudj�k �rni az �rt�keket. - // Csak a Reset() met�dusban null�z�dnak minden �j fut�s elej�n. + // Clear wrapper tracking - returns IdentityMap arrays to pool + base.Clear(); } diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs index 81ffa59..238411f 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs @@ -254,7 +254,8 @@ public static partial class AcBinarySerializer } finally { - BinarySerializationContextPool.Return(context); + if (context.Options.UseAsync) BinarySerializationContextPool.ReturnAsync(context); + else BinarySerializationContextPool.Return(context); } } @@ -295,7 +296,8 @@ public static partial class AcBinarySerializer } finally { - BinarySerializationContextPool.Return(context); + if (context.Options.UseAsync) BinarySerializationContextPool.ReturnAsync(context); + else BinarySerializationContextPool.Return(context); } } @@ -315,7 +317,8 @@ public static partial class AcBinarySerializer } finally { - BinarySerializationContextPool.Return(context); + if (context.Options.UseAsync) BinarySerializationContextPool.ReturnAsync(context); + else BinarySerializationContextPool.Return(context); } } @@ -338,7 +341,8 @@ public static partial class AcBinarySerializer } finally { - BinarySerializationContextPool.Return(context); + if (context.Options.UseAsync) BinarySerializationContextPool.ReturnAsync(context); + else BinarySerializationContextPool.Return(context); } } diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs index 4edf5c1..e6d81e1 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs @@ -8,11 +8,6 @@ namespace AyCode.Core.Serializers.Binaries; /// public sealed class AcBinarySerializerOptions : AcSerializerOptions { - /// - /// Cached platform detection - true if running in WebAssembly/Browser environment. - /// - private static readonly bool DetectedIsWasm = OperatingSystem.IsBrowser(); - public override AcSerializerType SerializerType { get; init; } = AcSerializerType.Binary; /// diff --git a/AyCode.Core/Serializers/IdentityMap.cs b/AyCode.Core/Serializers/IdentityMap.cs index b71e206..c077027 100644 --- a/AyCode.Core/Serializers/IdentityMap.cs +++ b/AyCode.Core/Serializers/IdentityMap.cs @@ -4,476 +4,6 @@ using System.Runtime.InteropServices; namespace AyCode.Core.Serializers; -//#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(); -//} - - -///// -///// 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): bitmap for fast lookup + direct value array -///// - Large keys: custom hash table with chaining -///// No Dictionary overhead, no per-entry allocation. -///// -///// The ID type (int, long, Guid, string) -//public sealed class IdentityMap : IIdentityMap where TId : notnull -//{ -// // Small int optimization (TId = int only, 0-4095) -// private const int SmallSize = 4096; -// private const int SmallBitmapSize = SmallSize / 64; // 64 ulongs = 512 bytes (fits in L1 cache!) - -// // Slot for hash table entries (generation needed for hash table validity) -// private struct HashSlot -// { -// public object? Value; -// public int Next; // next slot index in chain (-1 = end) -// } - -// // 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 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; - -// // Type checks (JIT eliminates these at compile time) -// private static readonly bool IsInt32 = typeof(TId) == typeof(int); -// private static readonly bool IsInt64 = typeof(TId) == typeof(long); -// private static readonly bool IsGuid = typeof(TId) == typeof(Guid); -// private static readonly bool IsString = typeof(TId) == typeof(string); - -// public IdentityMap() -// { -// } - -// /// -// /// 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) -// { -// // Small int fast path -// if (IsInt32) -// { -// var intKey = Unsafe.As(ref key); -// if ((uint)intKey < SmallSize) -// { -// return TryAddSmallInt(intKey); -// } -// } - -// // Hash table path -// return TryAddHash(key, out _); -// } - -// [MethodImpl(MethodImplOptions.AggressiveInlining)] -// private bool TryAddSmallInt(int key) -// { -// // Lazy init -// if (_smallBitmap == null) -// { -// _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; -// var bit = 1UL << (key & 63); - -// ref var word = ref _smallBitmap[wordIdx]; -// if ((word & bit) != 0) -// return false; // Already seen (bitmap is cleared on Reset) - -// word |= bit; -// 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) -// { -// var hash = GetHashCode(key); - -// // Lazy init -// if (_buckets == null) -// { -// InitHashTable(InitialHashCapacity); -// } - -// var bucketIdx = (hash & 0x7FFFFFFF) % _buckets!.Length; - -// // Search chain -// for (var i = _buckets[bucketIdx]; i >= 0; i = _entries![i].Next) -// { -// //if (EqualityComparer.Default.Equals(_keys![i], key)) -// if (KeyEquals(_keys![i], key)) // Direct comparison, no virtual call -// { -// slotIndex = i; -// return false; // already seen -// } -// } - -// // Resize if needed -// if (_count >= _entriesLength) -// { -// Resize(); -// bucketIdx = (hash & 0x7FFFFFFF) % _bucketsLength; -// } - -// // Add new entry -// slotIndex = _count++; -// _keys![slotIndex] = key; -// _entries![slotIndex] = new HashSlot { Next = _buckets[bucketIdx] }; -// _buckets[bucketIdx] = slotIndex; -// return true; -// } - -// [MethodImpl(MethodImplOptions.AggressiveInlining)] -// private static int GetHashCode(TId key) -// { -// // 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 = 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 = _bucketsLength * 2; - -// // 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); - -// // Rebuild bucket chains -// for (var i = 0; i < _count; i++) -// { -// var bucketIdx = (GetHashCode(newKeys[i]) & 0x7FFFFFFF) % newCapacity; -// newEntries[i].Next = newBuckets[bucketIdx]; -// 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 - bitmap check is cache-friendly (512 bytes) -// if (IsInt32) -// { -// var intKey = Unsafe.As(ref key); -// if ((uint)intKey < SmallSize && _smallBitmap != null) -// { -// var wordIdx = intKey >> 6; -// var bit = 1UL << (intKey & 63); -// return (_smallBitmap[wordIdx] & bit) != 0; -// } -// } - -// // Hash table path -// if (_buckets == null) return false; - -// var hash = GetHashCode(key); -// var bucketIdx = (hash & 0x7FFFFFFF) % _buckets.Length; - -// for (var i = _buckets[bucketIdx]; i >= 0; i = _entries![i].Next) -// { -// if (KeyEquals(_keys![i], key)) -// return true; -// } -// return false; -// } - -// /// -// /// 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) -// { -// // Small int fast path -// if (IsInt32) -// { -// var intKey = Unsafe.As(ref key); -// if ((uint)intKey < SmallSize) -// { -// return TryGetOrAddSmallInt(intKey, out existing); -// } -// } - -// // Hash table path -// if (!TryAddHash(key, out var slotIndex)) -// { -// existing = _entries![slotIndex].Value; -// return false; -// } -// existing = null; -// return true; -// } - -// [MethodImpl(MethodImplOptions.AggressiveInlining)] -// private bool TryGetOrAddSmallInt(int key, out object? existing) -// { -// // Lazy init -// if (_smallBitmap == null) -// { -// _smallBitmap = ArrayPool.Shared.Rent(SmallBitmapSize); -// Array.Clear(_smallBitmap, 0, SmallBitmapSize); - -// _smallValues = ArrayPool.Shared.Rent(SmallSize); -// } - -// var wordIdx = key >> 6; -// var bit = 1UL << (key & 63); - -// ref var word = ref _smallBitmap[wordIdx]; -// if ((word & bit) != 0) -// { -// // Bitmap says seen - value is valid -// existing = _smallValues![key]; -// return false; // Already seen -// } - -// // First occurrence - mark as seen -// word |= bit; -// _smallValues![key] = null; -// existing = null; -// return true; -// } - -// /// -// /// 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) -// { -// // Small int fast path -// if (IsInt32) -// { -// var intKey = Unsafe.As(ref key); -// if ((uint)intKey < SmallSize) -// { -// return TryGetOrAddSmallIntValue(intKey, newValue); -// } -// } - -// // Hash table path -// if (!TryAddHash(key, out var slotIndex)) -// { -// var existing = _entries![slotIndex].Value; -// if (existing != null) return existing; -// } -// _entries![slotIndex].Value = newValue; -// return newValue; -// } - -// [MethodImpl(MethodImplOptions.AggressiveInlining)] -// private object TryGetOrAddSmallIntValue(int key, object newValue) -// { -// // Lazy init -// if (_smallBitmap == null) -// { -// _smallBitmap = ArrayPool.Shared.Rent(SmallBitmapSize); -// Array.Clear(_smallBitmap, 0, SmallBitmapSize); - -// _smallValues = ArrayPool.Shared.Rent(SmallSize); -// } - -// var wordIdx = key >> 6; -// var bit = 1UL << (key & 63); - -// ref var word = ref _smallBitmap[wordIdx]; -// if ((word & bit) != 0) -// { -// // Bitmap says seen - check if value exists -// var existing = _smallValues![key]; -// if (existing != null) -// return existing; -// } - -// // First occurrence or no value yet - store new value -// word |= bit; -// _smallValues![key] = newValue; -// return newValue; -// } - -// /// -// /// 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) -// { -// // Small int fast path - bitmap check first (cache-friendly, 512 bytes) -// if (IsInt32) -// { -// var intKey = Unsafe.As(ref key); -// if ((uint)intKey < SmallSize && _smallBitmap != null) -// { -// var wordIdx = intKey >> 6; -// var bit = 1UL << (intKey & 63); -// if ((_smallBitmap[wordIdx] & bit) != 0) -// { -// // Bitmap says seen - value is valid -// value = _smallValues![intKey]; -// return true; -// } -// value = null; -// return false; -// } -// } - -// // Hash table path -// if (_buckets == null) -// { -// value = null; -// return false; -// } - -// var hash = GetHashCode(key); -// var bucketIdx = (hash & 0x7FFFFFFF) % _buckets.Length; - -// for (var i = _buckets[bucketIdx]; i >= 0; i = _entries![i].Next) -// { -// if (KeyEquals(_keys![i], key)) -// { -// value = _entries[i].Value; -// return true; -// } -// } -// value = null; -// return false; -// } - -// /// -// /// 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 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, 0, SmallBitmapSize); -// } - -// // Hash table reset -// if (_buckets != null) -// { -// 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 /// @@ -489,7 +19,6 @@ public enum IdAccessorType : byte /// Id is Guid. Guid = 3, } - /// /// Interface for identity maps used in serialization tracking. /// Enables type-safe Reset() without knowing the generic type parameter. @@ -499,21 +28,61 @@ public interface IIdentityMap /// /// Resets the identity map for reuse between serializations. /// - void Reset(); + /// If true, pre-rent arrays at next capacity + void Reset(bool preRentBuckets = false); } + /// -/// Generic identity map for tracking IId values during serialization/deserialization. -/// Uses Dictionary internally for unified tracking + object storage. +/// 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): bitmap for fast lookup + direct value array +/// - Large keys: custom hash table with chaining +/// No Dictionary overhead, no per-entry allocation. /// -/// The ID type (int, long, Guid) +/// The ID type (int, long, Guid, string) public sealed class IdentityMap : IIdentityMap where TId : notnull { - private readonly Dictionary _tracked; + private bool _useSmallInt = false; + + // Small int optimization (TId = int only, 0-65535) + private const int SmallBitmapSize = 1024; + private const int SmallSize = SmallBitmapSize * 64; + + // Small int optimization (TId = int only, 0-4095) + //private const int SmallSize = 4096; + //private const int SmallBitmapSize = SmallSize / 64; // 64 ulongs = 512 bytes (fits in L1 cache!) + + // Slot for hash table entries (generation needed for hash table validity) + private struct HashSlot + { + public object? Value; + public int Next; // next slot index in chain (-1 = end) + } + + // 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 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; + + // Type checks (JIT eliminates these at compile time) + private static readonly bool IsInt32 = typeof(TId) == typeof(int); + private static readonly bool IsInt64 = typeof(TId) == typeof(long); + private static readonly bool IsGuid = typeof(TId) == typeof(Guid); + private static readonly bool IsString = typeof(TId) == typeof(string); public IdentityMap() { - _tracked = new Dictionary(EqualityComparer.Default); } /// @@ -524,12 +93,200 @@ public sealed class IdentityMap : IIdentityMap where TId : notnull [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool TryAddKey(TId key) { - return _tracked.TryAdd(key, null); + // Small int fast path + if (_useSmallInt && IsInt32) + { + var intKey = Unsafe.As(ref key); + if ((uint)intKey < SmallSize) + { + return TryAddSmallInt(intKey); + } + } + + // Hash table path + return TryAddHash(key, out _); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool HasKey(TId key) => _tracked.ContainsKey(key); + private bool TryAddSmallInt(int key) + { + // Lazy init - ONLY bitmap for tracking (serializer doesn't need _smallValues) + if (_smallBitmap == null) + { + _smallBitmap = ArrayPool.Shared.Rent(SmallBitmapSize); + Array.Clear(_smallBitmap, 0, SmallBitmapSize); + // _smallValues allocated lazily in TryGetOrAddSmallIntValue when needed + } + var wordIdx = key >> 6; + var bit = 1UL << (key & 63); + + ref var word = ref _smallBitmap[wordIdx]; + if ((word & bit) != 0) + return false; // Already seen (bitmap is cleared on Reset) + + word |= bit; + 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) + { + var hash = GetHashCode(key); + + // Lazy init + if (_buckets == null) + { + InitHashTable(InitialHashCapacity); + } + + var bucketIdx = (hash & 0x7FFFFFFF) % _buckets!.Length; + + // Search chain + for (var i = _buckets[bucketIdx]; i >= 0; i = _entries![i].Next) + { + //if (EqualityComparer.Default.Equals(_keys![i], key)) + if (KeyEquals(_keys![i], key)) // Direct comparison, no virtual call + { + slotIndex = i; + return false; // already seen + } + } + + // Resize if needed + if (_count >= _entriesLength) + { + Resize(); + bucketIdx = (hash & 0x7FFFFFFF) % _bucketsLength; + } + + // Add new entry + slotIndex = _count++; + _keys![slotIndex] = key; + _entries![slotIndex] = new HashSlot { Next = _buckets[bucketIdx] }; + _buckets[bucketIdx] = slotIndex; + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int GetHashCode(TId key) + { + // 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) + { + // Use remembered capacity if larger (from previous serialization) + var actualCapacity = Math.Max(capacity, _bucketsLength); + + _buckets = ArrayPool.Shared.Rent(actualCapacity); + _bucketsLength = actualCapacity; + Array.Fill(_buckets, -1, 0, actualCapacity); + + _entries = ArrayPool.Shared.Rent(actualCapacity); + _entriesLength = actualCapacity; + + _keys = ArrayPool.Shared.Rent(actualCapacity); + + _count = 0; + } + + private void Resize() + { + var newCapacity = _bucketsLength * 2; + + // 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); + + // Rebuild bucket chains + for (var i = 0; i < _count; i++) + { + var bucketIdx = (GetHashCode(newKeys[i]) & 0x7FFFFFFF) % newCapacity; + newEntries[i].Next = newBuckets[bucketIdx]; + 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 - bitmap check is cache-friendly (512 bytes) + if (_useSmallInt && IsInt32) + { + var intKey = Unsafe.As(ref key); + if ((uint)intKey < SmallSize && _smallBitmap != null) + { + var wordIdx = intKey >> 6; + var bit = 1UL << (intKey & 63); + return (_smallBitmap[wordIdx] & bit) != 0; + } + } + + // Hash table path + if (_buckets == null) return false; + + var hash = GetHashCode(key); + var bucketIdx = (hash & 0x7FFFFFFF) % _buckets.Length; + + for (var i = _buckets[bucketIdx]; i >= 0; i = _entries![i].Next) + { + if (KeyEquals(_keys![i], key)) + return true; + } + return false; + } /// /// Checks if key exists and returns existing value, or adds new key (deserialization). @@ -539,9 +296,62 @@ public sealed class IdentityMap : IIdentityMap where TId : notnull [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; + // NOTE: SmallInt path DISABLED for deserializer. + // The 512KB _smallValues sparse array causes cache misses. + // Hash table has better cache locality. + + // // Small int fast path + // if (_useSmallInt && IsInt32) + // { + // var intKey = Unsafe.As(ref key); + // if ((uint)intKey < SmallSize) + // { + // return TryGetOrAddSmallInt(intKey, out existing); + // } + // } + + // Hash table path + if (!TryAddHash(key, out var slotIndex)) + { + existing = _entries![slotIndex].Value; + return false; + } + existing = null; + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool TryGetOrAddSmallInt(int key, out object? existing) + { + // Lazy init bitmap + if (_smallBitmap == null) + { + _smallBitmap = ArrayPool.Shared.Rent(SmallBitmapSize); + Array.Clear(_smallBitmap, 0, SmallBitmapSize); + } + + // Lazy init values array (only when needed for deserialization) + if (_smallValues == null) + { + _smallValues = ArrayPool.Shared.Rent(SmallSize); + } + + var wordIdx = key >> 6; + var bit = 1UL << (key & 63); + + ref var word = ref _smallBitmap[wordIdx]; + if ((word & bit) != 0) + { + // Bitmap says seen - value is valid + existing = _smallValues![key]; + return false; // Already seen + } + + // First occurrence - mark as seen + word |= bit; + _smallValues![key] = null; + existing = null; + return true; } /// @@ -551,9 +361,62 @@ public sealed class IdentityMap : IIdentityMap where TId : notnull [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; + // NOTE: SmallInt path DISABLED for deserializer value storage. + // The 512KB _smallValues sparse array causes cache misses. + // Hash table has better cache locality (only stores actual entries). + // Bitmap is only useful for tracking (serializer) - not for value storage. + + // // Small int fast path + // if (_useSmallInt && IsInt32) + // { + // var intKey = Unsafe.As(ref key); + // if ((uint)intKey < SmallSize) + // { + // return TryGetOrAddSmallIntValue(intKey, newValue); + // } + // } + + // Hash table path + if (!TryAddHash(key, out var slotIndex)) + { + var existing = _entries![slotIndex].Value; + if (existing != null) return existing; + } + _entries![slotIndex].Value = newValue; + return newValue; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private object TryGetOrAddSmallIntValue(int key, object newValue) + { + // Lazy init bitmap + if (_smallBitmap == null) + { + _smallBitmap = ArrayPool.Shared.Rent(SmallBitmapSize); + Array.Clear(_smallBitmap, 0, SmallBitmapSize); + } + + // Lazy init values array (only when needed for deserialization) + if (_smallValues == null) + { + _smallValues = ArrayPool.Shared.Rent(SmallSize); + } + + var wordIdx = key >> 6; + var bit = 1UL << (key & 63); + + ref var word = ref _smallBitmap[wordIdx]; + if ((word & bit) != 0) + { + // Bitmap says seen - check if value exists + var existing = _smallValues![key]; + if (existing != null) + return existing; + } + + // First occurrence or no value yet - store new value + word |= bit; + _smallValues![key] = newValue; return newValue; } @@ -564,16 +427,229 @@ public sealed class IdentityMap : IIdentityMap where TId : notnull [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool TryGetValue(TId key, out object? value) { - return _tracked.TryGetValue(key, out value); + // NOTE: SmallInt path DISABLED for deserializer value lookup. + // The 512KB _smallValues sparse array causes cache misses. + // Hash table has better cache locality. + + // // Small int fast path - bitmap check first (cache-friendly, 512 bytes) + // if (_useSmallInt && IsInt32) + // { + // var intKey = Unsafe.As(ref key); + // if ((uint)intKey < SmallSize && _smallBitmap != null) + // { + // var wordIdx = intKey >> 6; + // var bit = 1UL << (intKey & 63); + // if ((_smallBitmap[wordIdx] & bit) != 0) + // { + // // Bitmap says seen - value is valid + // value = _smallValues![intKey]; + // return true; + // } + // value = null; + // return false; + // } + // } + + // Hash table path + if (_buckets == null) + { + value = null; + return false; + } + + var hash = GetHashCode(key); + var bucketIdx = (hash & 0x7FFFFFFF) % _buckets.Length; + + for (var i = _buckets[bucketIdx]; i >= 0; i = _entries![i].Next) + { + if (KeyEquals(_keys![i], key)) + { + value = _entries[i].Value; + return true; + } + } + value = null; + return false; } /// /// Resets the identity map for reuse. + /// Small arrays (≤ InitialHashCapacity*5): keep and clear (faster than pool round-trip). + /// Large arrays: return to pool, remember half capacity for next use. /// - public void Reset() + /// If true, pre-rent arrays at next capacity (useful for async Clear to shift work from hot path) + public void Reset(bool preRentBuckets = false) { - _tracked.Clear(); + if (_smallBitmap != null) + { + ArrayPool.Shared.Return(_smallBitmap); + _smallBitmap = null; + } + if (_smallValues != null) + { + ArrayPool.Shared.Return(_smallValues, clearArray: true); // Clear to release object refs + _smallValues = null; + } + + if (_buckets != null) + { + // Small arrays: keep and clear (faster than pool round-trip) + if (_bucketsLength <= InitialHashCapacity * 5) + { + Array.Fill(_buckets, -1, 0, _bucketsLength); + // Clear to release object references (GC can collect) + if (_count > 0) + { + Array.Clear(_entries!, 0, _count); + Array.Clear(_keys!, 0, _count); + } + _count = 0; + return; + } + + // Large arrays: return to pool, remember half capacity + var nextCapacity = Math.Max(_bucketsLength / 2, InitialHashCapacity); + + // Clear entries/keys to release object references before returning to pool + // Otherwise pool holds refs → GC can't collect! + if (_count > 0) + { + Array.Clear(_entries!, 0, _count); + Array.Clear(_keys!, 0, _count); + } + + ArrayPool.Shared.Return(_buckets); + ArrayPool.Shared.Return(_entries!); + ArrayPool.Shared.Return(_keys!); + + if (preRentBuckets) + { + // Pre-rent arrays now (async background) so Pool.Get() is faster + _buckets = ArrayPool.Shared.Rent(nextCapacity); + _bucketsLength = nextCapacity; + Array.Fill(_buckets, -1, 0, nextCapacity); + + _entries = ArrayPool.Shared.Rent(nextCapacity); + _entriesLength = nextCapacity; + + _keys = ArrayPool.Shared.Rent(nextCapacity); + } + else + { + _buckets = null; + _entries = null; + _keys = null; + _bucketsLength = nextCapacity; // Remember for next InitHashTable + _entriesLength = 0; + } + _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/Jsons/AcJsonSerializer.JsonSerializationContext.cs b/AyCode.Core/Serializers/Jsons/AcJsonSerializer.JsonSerializationContext.cs index b38aff4..1d44e51 100644 --- a/AyCode.Core/Serializers/Jsons/AcJsonSerializer.JsonSerializationContext.cs +++ b/AyCode.Core/Serializers/Jsons/AcJsonSerializer.JsonSerializationContext.cs @@ -1,8 +1,10 @@ +using System; using System.Buffers; using System.Collections.Concurrent; using System.Runtime.CompilerServices; using System.Text; using System.Text.Json; +using System.Threading; namespace AyCode.Core.Serializers.Jsons; @@ -24,6 +26,13 @@ public static partial class AcJsonSerializer return new JsonSerializationContext(options); } + //[MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ReturnAsync(JsonSerializationContext context) + { + // 🔥 FIRE-AND-FORGET: cleanup háttérben + ThreadPool.UnsafeQueueUserWorkItem(Return, context, preferLocal: true); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Return(JsonSerializationContext context) { @@ -32,6 +41,10 @@ public static partial class AcJsonSerializer context.Clear(); Pool.Enqueue(context); } + else + { + context.Dispose(); // ✅ Utf8JsonWriter + ArrayBufferWriter cleanup! + } } } @@ -75,11 +88,14 @@ public static partial class AcJsonSerializer } } - public void Clear() + public override void Clear() { Writer.Reset(); _buffer.Clear(); _refTracker.Reset(); + + // Clear wrapper tracking - returns IdentityMap arrays to pool + base.Clear(); } [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/AyCode.Core/Serializers/Jsons/AcJsonSerializer.cs b/AyCode.Core/Serializers/Jsons/AcJsonSerializer.cs index cc09e5d..346d002 100644 --- a/AyCode.Core/Serializers/Jsons/AcJsonSerializer.cs +++ b/AyCode.Core/Serializers/Jsons/AcJsonSerializer.cs @@ -60,13 +60,14 @@ public static partial class AcJsonSerializer { if (options.ReferenceHandling != ReferenceHandlingMode.None) ScanReferences(actualValue, context, 0); - + WriteValue(actualValue, context, 0); return context.GetResult(); } finally { - SerializationContextPool.Return(context); + if (context.Options.UseAsync) SerializationContextPool.ReturnAsync(context); + else SerializationContextPool.Return(context); } } diff --git a/AyCode.Core/Serializers/SerializationContextBase.cs b/AyCode.Core/Serializers/SerializationContextBase.cs index c769eb6..e8e24a4 100644 --- a/AyCode.Core/Serializers/SerializationContextBase.cs +++ b/AyCode.Core/Serializers/SerializationContextBase.cs @@ -15,6 +15,7 @@ public abstract class SerializationContextBase : AcSerializ where TMetadata : TypeMetadataBase where TOptions : AcSerializerOptions { + private bool _useSmallInt = false; private const int BitArraySize = 1024; private const int MaxSmallId = BitArraySize * 64; @@ -43,7 +44,7 @@ public abstract class SerializationContextBase : AcSerializ refId = wrapper.RefIdGetterInt32!(obj); // BitArray fast path for small positive IDs - return refId is >= 0 and < MaxSmallId ? TryTrackSmallId(wrapper, refId) : wrapper.TryAddKey(refId); + return (_useSmallInt && refId is >= 0 and < MaxSmallId) ? TryTrackSmallId(wrapper, refId) : wrapper.TryAddKey(refId); } [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/AyCode.Core/Serializers/TypeMetadataWrapper.cs b/AyCode.Core/Serializers/TypeMetadataWrapper.cs index 6a7bead..04b714b 100644 --- a/AyCode.Core/Serializers/TypeMetadataWrapper.cs +++ b/AyCode.Core/Serializers/TypeMetadataWrapper.cs @@ -115,15 +115,16 @@ public sealed class TypeMetadataWrapper where TMetadata : TypeMetadat /// Resets tracking state for reuse between serializations. /// Does not deallocate - just clears for reuse (pool-friendly). /// + /// If true, pre-rent arrays at next capacity (useful for async Clear) [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void ResetTracking() + public void ResetTracking(bool preRentBuckets = false) { if (SmallIdBitmap != null) Array.Clear(SmallIdBitmap); - IdentityMapInt32?.Reset(); - IdentityMapInt64?.Reset(); - IdentityMapGuid?.Reset(); + IdentityMapInt32?.Reset(preRentBuckets); + IdentityMapInt64?.Reset(preRentBuckets); + IdentityMapGuid?.Reset(preRentBuckets); } #region Direct Int32 Operations - No type check, no generic overhead