From bc62488965018ddad57d3e67258a90741d326ca1 Mon Sep 17 00:00:00 2001 From: Loretta Date: Mon, 2 Feb 2026 10:11:45 +0100 Subject: [PATCH] Pool arrays for dup data and intern cache in deserializer Reduce allocations by pooling int[] and object?[] arrays used for duplicate data and interned references during binary deserialization. Arrays are now rented and reused via ArrayPool, with logic to clear or return them as appropriate. This improves performance and reduces GC pressure in steady-state scenarios. --- ...serializer.BinaryDeserializationContext.cs | 8 +- ...lizer.BinaryDeserializationContextClass.cs | 94 +++++++++++++++++++ 2 files changed, 99 insertions(+), 3 deletions(-) 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();