diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.cs index 486952a..2c8e9d9 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.cs @@ -172,6 +172,7 @@ public static partial class AcBinaryDeserializer /// Reads intern footer: [dupCount][pos0][idx0][pos1][idx1]... /// Shared for string interning AND IId object references. /// VarUInt format read into flat int[] for fast hot path access. + /// Arrays are pooled via ContextClass for zero steady-state allocation. /// private void ReadFooterIndices(int footerPosition) { @@ -191,16 +192,17 @@ public static partial class AcBinaryDeserializer } else { - // Read VarUInt pairs into flat int[] + // Read VarUInt pairs into pooled flat int[] var intCount = dupCount * 2; - _dupData = new int[intCount]; + _dupData = ContextClass.RentDupData(intCount); for (var i = 0; i < dupCount; i++) { _dupData[i * 2] = (int)ReadVarUInt(); // position _dupData[i * 2 + 1] = (int)ReadVarUInt(); // cacheIndex } - _internCache = new object?[dupCount]; + _internCache = ContextClass.RentInternCache(dupCount); + ContextClass.SetInternCacheUsed(dupCount); // Cache first dup position for ultra-fast hot path _nextDupPosition = _dupData[0]; } diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContextClass.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContextClass.cs index 41604d7..7a4be57 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContextClass.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContextClass.cs @@ -1,4 +1,5 @@ using System; +using System.Buffers; namespace AyCode.Core.Serializers.Binaries; @@ -8,6 +9,7 @@ public static partial class AcBinaryDeserializer /// Heap-allocated context class for binary deserialization. /// Inherits from AcSerializerContextBase for unified metadata caching and IId-based reference tracking. /// Used in composition with the ref struct BinaryDeserializationContext. + /// Holds pooled arrays for intern cache reuse across deserializations. /// internal sealed class BinaryDeserializationContextClass : AcSerializerContextBase { @@ -17,10 +19,102 @@ public static partial class AcBinaryDeserializer protected override Func MetadataFactory => static t => new BinaryDeserializeTypeMetadata(t); + // Pooled arrays - reused across deserializations, zero steady-state allocation + private int[]? _pooledDupData; + private object?[]? _pooledInternCache; + private int _pooledDupDataLength; + private int _pooledInternCacheLength; + private int _lastInternCacheUsed; // how many slots were used (for targeted Clear) + + // Small arrays: keep across calls. Large arrays: return to pool in Clear(). + private const int SmallArrayThreshold = 256; + public BinaryDeserializationContextClass() { } + /// + /// Gets or allocates a pooled int[] for dup data (position/cacheIndex pairs). + /// + public int[] RentDupData(int minLength) + { + if (_pooledDupData != null && _pooledDupDataLength >= minLength) + return _pooledDupData; + + // Too small - return old and rent bigger + if (_pooledDupData != null) + ArrayPool.Shared.Return(_pooledDupData); + + _pooledDupData = ArrayPool.Shared.Rent(minLength); + _pooledDupDataLength = _pooledDupData.Length; + return _pooledDupData; + } + + /// + /// Gets or allocates a pooled object?[] for intern cache. + /// + public object?[] RentInternCache(int minLength) + { + if (_pooledInternCache != null && _pooledInternCacheLength >= minLength) + { + // Reuse - clear previous content (release GC roots) + if (_lastInternCacheUsed > 0) + { + Array.Clear(_pooledInternCache, 0, _lastInternCacheUsed); + _lastInternCacheUsed = 0; + } + return _pooledInternCache; + } + + // Too small - return old and rent bigger + if (_pooledInternCache != null) + ArrayPool.Shared.Return(_pooledInternCache, clearArray: true); + + _pooledInternCache = ArrayPool.Shared.Rent(minLength); + _pooledInternCacheLength = _pooledInternCache.Length; + _lastInternCacheUsed = 0; + return _pooledInternCache; + } + + /// + /// Tracks how many intern cache slots were used (for targeted Clear). + /// + public void SetInternCacheUsed(int count) + { + _lastInternCacheUsed = count; + } + + public override void Clear() + { + base.Clear(); + + // Intern cache: clear GC roots, return large arrays to pool + if (_pooledInternCache != null) + { + if (_pooledInternCacheLength > SmallArrayThreshold) + { + // Large: return to pool - no pre-rent needed, ReadFooterIndices will rent on demand + ArrayPool.Shared.Return(_pooledInternCache, clearArray: true); + _pooledInternCache = null; + _pooledInternCacheLength = 0; + } + else if (_lastInternCacheUsed > 0) + { + // Small: keep array, just clear content (release GC roots) + Array.Clear(_pooledInternCache, 0, _lastInternCacheUsed); + } + _lastInternCacheUsed = 0; + } + + // Dup data: no GC roots (int[]), return large arrays to pool + if (_pooledDupData != null && _pooledDupDataLength > SmallArrayThreshold) + { + ArrayPool.Shared.Return(_pooledDupData); + _pooledDupData = null; + _pooledDupDataLength = 0; + } + } + public override void Reset(AcBinarySerializerOptions options) { Clear();