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:
parent
9b151fd6cf
commit
bc62488965
|
|
@ -172,6 +172,7 @@ public static partial class AcBinaryDeserializer
|
||||||
/// Reads intern footer: [dupCount][pos0][idx0][pos1][idx1]...
|
/// Reads intern footer: [dupCount][pos0][idx0][pos1][idx1]...
|
||||||
/// Shared for string interning AND IId object references.
|
/// Shared for string interning AND IId object references.
|
||||||
/// VarUInt format read into flat int[] for fast hot path access.
|
/// VarUInt format read into flat int[] for fast hot path access.
|
||||||
|
/// Arrays are pooled via ContextClass for zero steady-state allocation.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void ReadFooterIndices(int footerPosition)
|
private void ReadFooterIndices(int footerPosition)
|
||||||
{
|
{
|
||||||
|
|
@ -191,16 +192,17 @@ public static partial class AcBinaryDeserializer
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Read VarUInt pairs into flat int[]
|
// Read VarUInt pairs into pooled flat int[]
|
||||||
var intCount = dupCount * 2;
|
var intCount = dupCount * 2;
|
||||||
_dupData = new int[intCount];
|
_dupData = ContextClass.RentDupData(intCount);
|
||||||
for (var i = 0; i < dupCount; i++)
|
for (var i = 0; i < dupCount; i++)
|
||||||
{
|
{
|
||||||
_dupData[i * 2] = (int)ReadVarUInt(); // position
|
_dupData[i * 2] = (int)ReadVarUInt(); // position
|
||||||
_dupData[i * 2 + 1] = (int)ReadVarUInt(); // cacheIndex
|
_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
|
// Cache first dup position for ultra-fast hot path
|
||||||
_nextDupPosition = _dupData[0];
|
_nextDupPosition = _dupData[0];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using System;
|
using System;
|
||||||
|
using System.Buffers;
|
||||||
|
|
||||||
namespace AyCode.Core.Serializers.Binaries;
|
namespace AyCode.Core.Serializers.Binaries;
|
||||||
|
|
||||||
|
|
@ -8,6 +9,7 @@ public static partial class AcBinaryDeserializer
|
||||||
/// Heap-allocated context class for binary deserialization.
|
/// Heap-allocated context class for binary deserialization.
|
||||||
/// Inherits from AcSerializerContextBase for unified metadata caching and IId-based reference tracking.
|
/// Inherits from AcSerializerContextBase for unified metadata caching and IId-based reference tracking.
|
||||||
/// Used in composition with the ref struct BinaryDeserializationContext.
|
/// Used in composition with the ref struct BinaryDeserializationContext.
|
||||||
|
/// Holds pooled arrays for intern cache reuse across deserializations.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal sealed class BinaryDeserializationContextClass : AcSerializerContextBase<BinaryDeserializeTypeMetadata, AcBinarySerializerOptions>
|
internal sealed class BinaryDeserializationContextClass : AcSerializerContextBase<BinaryDeserializeTypeMetadata, AcBinarySerializerOptions>
|
||||||
{
|
{
|
||||||
|
|
@ -17,10 +19,102 @@ public static partial class AcBinaryDeserializer
|
||||||
protected override Func<Type, BinaryDeserializeTypeMetadata> MetadataFactory
|
protected override Func<Type, BinaryDeserializeTypeMetadata> MetadataFactory
|
||||||
=> static t => new BinaryDeserializeTypeMetadata(t);
|
=> 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()
|
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)
|
public override void Reset(AcBinarySerializerOptions options)
|
||||||
{
|
{
|
||||||
Clear();
|
Clear();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue