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<T>, with logic to clear or return them as appropriate. This improves performance and reduces GC pressure in steady-state scenarios.
This commit is contained in:
Loretta 2026-02-02 10:11:45 +01:00
parent 9b151fd6cf
commit bc62488965
2 changed files with 99 additions and 3 deletions

View File

@ -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.
/// </summary>
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];
}

View File

@ -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.
/// </summary>
internal sealed class BinaryDeserializationContextClass : AcSerializerContextBase<BinaryDeserializeTypeMetadata, AcBinarySerializerOptions>
{
@ -17,10 +19,102 @@ public static partial class AcBinaryDeserializer
protected override Func<Type, BinaryDeserializeTypeMetadata> 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()
{
}
/// <summary>
/// Gets or allocates a pooled int[] for dup data (position/cacheIndex pairs).
/// </summary>
public int[] RentDupData(int minLength)
{
if (_pooledDupData != null && _pooledDupDataLength >= minLength)
return _pooledDupData;
// Too small - return old and rent bigger
if (_pooledDupData != null)
ArrayPool<int>.Shared.Return(_pooledDupData);
_pooledDupData = ArrayPool<int>.Shared.Rent(minLength);
_pooledDupDataLength = _pooledDupData.Length;
return _pooledDupData;
}
/// <summary>
/// Gets or allocates a pooled object?[] for intern cache.
/// </summary>
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<object?>.Shared.Return(_pooledInternCache, clearArray: true);
_pooledInternCache = ArrayPool<object?>.Shared.Rent(minLength);
_pooledInternCacheLength = _pooledInternCache.Length;
_lastInternCacheUsed = 0;
return _pooledInternCache;
}
/// <summary>
/// Tracks how many intern cache slots were used (for targeted Clear).
/// </summary>
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<object?>.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<int>.Shared.Return(_pooledDupData);
_pooledDupData = null;
_pooledDupDataLength = 0;
}
}
public override void Reset(AcBinarySerializerOptions options)
{
Clear();