diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.cs
index dc0d113..3b5ad3a 100644
--- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.cs
+++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.cs
@@ -11,22 +11,21 @@ public static partial class AcBinaryDeserializer
{
///
/// Binary deserialization context. Public for generated serializers.
- /// Uses composition with BinaryDeserializationContextClass for IId-based tracking.
+ /// Uses composition with pooled BinaryDeserializationContextClass for zero-alloc deserialization.
+ /// String cache and intern cache index are delegated to ContextClass for pool reuse.
///
internal ref struct BinaryDeserializationContext
{
private readonly ReadOnlySpan _buffer;
private int _position;
- private Dictionary? _stringCache;
// Marker-based interning: sequential cache (no footer needed)
// StringInternFirst/ObjectRefFirst markers register values in order
private object?[]? _internCache; // Shared cache for interned strings AND IId objects
- private int _nextCacheIndex; // Next index to assign when registering
///
/// Heap-allocated context class for IId-based reference tracking.
- /// Also holds Options - all options-derived properties delegate to ContextClass.Options.
+ /// Pooled via DeserializationContextClassPool — holds caches, options, metadata.
///
public readonly BinaryDeserializationContextClass ContextClass;
@@ -56,33 +55,23 @@ public static partial class AcBinaryDeserializer
///
public readonly bool IsChainMode => ChainTracker != null;
- public BinaryDeserializationContext(ReadOnlySpan data)
- : this(data, AcBinarySerializerOptions.Default, new BinaryDeserializationContextClass())
- {
- }
-
- public BinaryDeserializationContext(ReadOnlySpan data, AcBinarySerializerOptions options)
- : this(data, options, new BinaryDeserializationContextClass())
- {
- }
-
- public BinaryDeserializationContext(ReadOnlySpan data, AcBinarySerializerOptions options, BinaryDeserializationContextClass contextClass)
+ ///
+ /// Creates a deserialization context with a pooled ContextClass.
+ /// ContextClass must already be Reset() with options before passing in.
+ ///
+ public BinaryDeserializationContext(ReadOnlySpan data, BinaryDeserializationContextClass contextClass)
{
_buffer = data;
_position = 0;
- _stringCache = null;
// Marker-based interning fields
_internCache = null;
- _nextCacheIndex = 0;
HasMetadata = false;
IsMergeMode = false;
RemoveOrphanedItems = false;
ChainTracker = null;
ContextClass = contextClass;
- // Reset ContextClass with options - this sets Options and clears any previous state
- ContextClass.Reset(options);
}
public void ReadHeader()
@@ -422,9 +411,9 @@ public static partial class AcBinaryDeserializer
var slice = _buffer.Slice(_position, length);
var hash = ComputeStringHashFull(slice);
- _stringCache ??= new Dictionary(128);
+ var stringCache = ContextClass.GetOrCreateStringCache();
- if (_stringCache.TryGetValue(hash, out var cached))
+ if (stringCache.TryGetValue(hash, out var cached))
{
// Hash includes all bytes for short strings, so collision is extremely unlikely
// For longer strings, we still verify length as a sanity check
@@ -437,7 +426,7 @@ public static partial class AcBinaryDeserializer
}
var value = Utf8NoBom.GetString(slice);
- _stringCache[hash] = value;
+ stringCache[hash] = value;
_position += length;
return value;
}
@@ -513,7 +502,7 @@ public static partial class AcBinaryDeserializer
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void RegisterNextInternedValue(object value)
{
- _internCache![_nextCacheIndex++] = value;
+ _internCache![ContextClass.NextCacheIndexRef++] = value;
}
///
diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContextClass.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContextClass.cs
index 366187c..e5d056a 100644
--- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContextClass.cs
+++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContextClass.cs
@@ -1,5 +1,6 @@
using System;
using System.Buffers;
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
@@ -7,11 +8,44 @@ namespace AyCode.Core.Serializers.Binaries;
public static partial class AcBinaryDeserializer
{
+ ///
+ /// Pool for BinaryDeserializationContextClass instances.
+ /// Eliminates per-call heap allocation — mirrors BinarySerializationContextPool pattern.
+ ///
+ private static class DeserializationContextClassPool
+ {
+ private static readonly ConcurrentQueue Pool = new();
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static BinaryDeserializationContextClass Get(AcBinarySerializerOptions options)
+ {
+ if (Pool.TryDequeue(out var ctx))
+ {
+ ctx.Reset(options);
+ return ctx;
+ }
+
+ var newCtx = new BinaryDeserializationContextClass();
+ newCtx.Reset(options);
+ return newCtx;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void Return(BinaryDeserializationContextClass ctx)
+ {
+ if (Pool.Count < ctx.Options.MaxContextPoolSize)
+ {
+ ctx.Clear();
+ Pool.Enqueue(ctx);
+ }
+ }
+ }
+
///
/// 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.
+ /// Holds pooled arrays and caches for reuse across deserializations.
///
internal sealed class BinaryDeserializationContextClass : AcSerializerContextBase
{
@@ -31,6 +65,55 @@ public static partial class AcBinaryDeserializer
// Small arrays: keep across calls. Large arrays: return to pool in Clear().
private const int SmallArrayThreshold = 256;
+ // String cache - moved from ref struct for pool reuse (WASM optimization)
+ private Dictionary? _stringCache;
+
+ // Intern cache index counter - moved from ref struct for pool reuse
+ private int _nextCacheIndex;
+
+ // Linearized buffer for ReadOnlySequence input
+ private byte[]? _linearizedBuffer;
+
+ ///
+ /// String cache for WASM optimization. Reused across deserializations.
+ ///
+ internal Dictionary? StringCache => _stringCache;
+
+ ///
+ /// Gets or lazily creates the string cache with initial capacity.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal Dictionary GetOrCreateStringCache()
+ {
+ return _stringCache ??= new Dictionary(128);
+ }
+
+ ///
+ /// Next intern cache index to assign when registering interned values.
+ ///
+ internal ref int NextCacheIndexRef
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => ref _nextCacheIndex;
+ }
+
+ ///
+ /// Rents a linearized buffer for ReadOnlySequence multi-segment input.
+ /// Buffer is pooled and reused across deserializations.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal byte[] RentLinearizedBuffer(int minSize)
+ {
+ if (_linearizedBuffer != null && _linearizedBuffer.Length >= minSize)
+ return _linearizedBuffer;
+
+ if (_linearizedBuffer != null)
+ ArrayPool.Shared.Return(_linearizedBuffer);
+
+ _linearizedBuffer = ArrayPool.Shared.Rent(minSize);
+ return _linearizedBuffer;
+ }
+
///
/// Inline metadata bejegyzések flat array-ben.
/// A propNameHash alapján lineárisan keresünk (kis számú típus per stream).
@@ -146,6 +229,10 @@ public static partial class AcBinaryDeserializer
base.Clear();
_metadataEntryCount = 0;
+ _nextCacheIndex = 0;
+
+ // String cache: clear content but keep dictionary allocated for reuse
+ _stringCache?.Clear();
// Intern cache: clear GC roots, return large arrays to pool
if (_pooledInternCache != null)
@@ -172,6 +259,13 @@ public static partial class AcBinaryDeserializer
_pooledDupData = null;
_pooledDupDataLength = 0;
}
+
+ // Linearized buffer: no GC roots (byte[]), keep small, return large
+ if (_linearizedBuffer != null && _linearizedBuffer.Length > SmallArrayThreshold)
+ {
+ ArrayPool.Shared.Return(_linearizedBuffer);
+ _linearizedBuffer = null;
+ }
}
public override void Reset(AcBinarySerializerOptions options)
diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.CrossType.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.CrossType.cs
index 4bc79be..a496ae6 100644
--- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.CrossType.cs
+++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.CrossType.cs
@@ -55,7 +55,8 @@ public static partial class AcBinaryDeserializer
// Cross-type path: use index mapping
var indexMapping = GetIndexMapping(sourceType, destType, options.PropertyMapper);
- var context = new BinaryDeserializationContext(data, options);
+ var ctxClass = DeserializationContextClassPool.Get(options);
+ var context = new BinaryDeserializationContext(data, ctxClass);
try
{
@@ -73,6 +74,10 @@ public static partial class AcBinaryDeserializer
$"Failed to deserialize binary data from '{sourceType.Name}' to '{destType.Name}': {ex.Message}",
context.Position, destType, ex);
}
+ finally
+ {
+ DeserializationContextClassPool.Return(ctxClass);
+ }
}
///
@@ -136,7 +141,8 @@ public static partial class AcBinaryDeserializer
// Cross-type path: use index mapping
var indexMapping = GetIndexMapping(sourceType, destType, options.PropertyMapper);
- var context = new BinaryDeserializationContext(data, options);
+ var ctxClass = DeserializationContextClassPool.Get(options);
+ var context = new BinaryDeserializationContext(data, ctxClass);
try
{
@@ -171,6 +177,10 @@ public static partial class AcBinaryDeserializer
$"Failed to populate target of type '{destType.Name}' with data from '{sourceType.Name}': {ex.Message}",
context.Position, destType, ex);
}
+ finally
+ {
+ DeserializationContextClassPool.Return(ctxClass);
+ }
}
///
diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs
index d66fb5a..f92f6f3 100644
--- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs
+++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs
@@ -1,4 +1,5 @@
using System;
+using System.Buffers;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Frozen;
@@ -135,7 +136,8 @@ public static partial class AcBinaryDeserializer
return (T?)(object?)DeserializeExpression(data, targetType, options);
}
- var context = new BinaryDeserializationContext(data, options);
+ var ctxClass = DeserializationContextClassPool.Get(options);
+ var context = new BinaryDeserializationContext(data, ctxClass);
try
{
@@ -154,6 +156,10 @@ public static partial class AcBinaryDeserializer
$"Failed to deserialize binary data to type '{targetType.Name}': {ex.Message}",
context.Position, targetType, ex);
}
+ finally
+ {
+ DeserializationContextClassPool.Return(ctxClass);
+ }
}
///
@@ -176,7 +182,8 @@ public static partial class AcBinaryDeserializer
return DeserializeExpression(data, targetType, options);
}
- var context = new BinaryDeserializationContext(data, options);
+ var ctxClass = DeserializationContextClassPool.Get(options);
+ var context = new BinaryDeserializationContext(data, ctxClass);
try
{
@@ -195,6 +202,144 @@ public static partial class AcBinaryDeserializer
$"Failed to deserialize binary data to type '{targetType.Name}': {ex.Message}",
context.Position, targetType, ex);
}
+ finally
+ {
+ DeserializationContextClassPool.Return(ctxClass);
+ }
+ }
+
+ ///
+ /// Deserialize binary data from a ReadOnlySequence (e.g., from SignalR/Pipes).
+ /// Single-segment: zero-copy via FirstSpan. Multi-segment: linearize into pooled buffer.
+ ///
+ public static T? Deserialize(ReadOnlySequence data)
+ => Deserialize(data, AcBinarySerializerOptions.Default);
+
+ ///
+ /// Deserialize binary data from a ReadOnlySequence with options.
+ ///
+ public static T? Deserialize(ReadOnlySequence data, AcBinarySerializerOptions options)
+ {
+ if (data.Length == 0) return default;
+
+ if (data.IsSingleSegment)
+ return Deserialize(data.FirstSpan, options);
+
+ var ctxClass = DeserializationContextClassPool.Get(options);
+ try
+ {
+ var buffer = ctxClass.RentLinearizedBuffer((int)data.Length);
+ data.CopyTo(buffer);
+ return Deserialize(buffer.AsSpan(0, (int)data.Length), ctxClass);
+ }
+ finally
+ {
+ DeserializationContextClassPool.Return(ctxClass);
+ }
+ }
+
+ ///
+ /// Deserialize binary data from a ReadOnlySequence to specified type.
+ ///
+ public static object? Deserialize(ReadOnlySequence data, Type targetType)
+ => Deserialize(data, targetType, AcBinarySerializerOptions.Default);
+
+ ///
+ /// Deserialize binary data from a ReadOnlySequence to specified type with options.
+ ///
+ public static object? Deserialize(ReadOnlySequence data, Type targetType, AcBinarySerializerOptions options)
+ {
+ if (data.Length == 0) return null;
+
+ if (data.IsSingleSegment)
+ return Deserialize(data.FirstSpan, targetType, options);
+
+ var ctxClass = DeserializationContextClassPool.Get(options);
+ try
+ {
+ var buffer = ctxClass.RentLinearizedBuffer((int)data.Length);
+ data.CopyTo(buffer);
+ return Deserialize(buffer.AsSpan(0, (int)data.Length), targetType, ctxClass);
+ }
+ finally
+ {
+ DeserializationContextClassPool.Return(ctxClass);
+ }
+ }
+
+ ///
+ /// Internal: Deserialize with pre-pooled ContextClass (used by ReadOnlySequence multi-segment path).
+ ///
+ private static T? Deserialize(ReadOnlySpan data, BinaryDeserializationContextClass ctxClass)
+ {
+ if (data.Length == 1 && data[0] == BinaryTypeCode.Null) return default;
+
+ var targetType = typeof(T);
+ if (AcSerializerCommon.IsExpressionType(targetType))
+ return (T?)(object?)DeserializeExpression(data, targetType, ctxClass);
+
+ var context = new BinaryDeserializationContext(data, ctxClass);
+ try
+ {
+ context.ReadHeader();
+ return (T?)ReadValue(ref context, targetType, 0);
+ }
+ catch (AcBinaryDeserializationException) { throw; }
+ catch (Exception ex)
+ {
+ throw new AcBinaryDeserializationException(
+ $"Failed to deserialize binary data to type '{targetType.Name}': {ex.Message}",
+ context.Position, targetType, ex);
+ }
+ }
+
+ ///
+ /// Internal: Deserialize with pre-pooled ContextClass (used by ReadOnlySequence multi-segment path).
+ ///
+ private static object? Deserialize(ReadOnlySpan data, Type targetType, BinaryDeserializationContextClass ctxClass)
+ {
+ if (data.Length == 1 && data[0] == BinaryTypeCode.Null) return null;
+
+ if (AcSerializerCommon.IsExpressionType(targetType))
+ return DeserializeExpression(data, targetType, ctxClass);
+
+ var context = new BinaryDeserializationContext(data, ctxClass);
+ try
+ {
+ context.ReadHeader();
+ return ReadValue(ref context, targetType, 0);
+ }
+ catch (AcBinaryDeserializationException) { throw; }
+ catch (Exception ex)
+ {
+ throw new AcBinaryDeserializationException(
+ $"Failed to deserialize binary data to type '{targetType.Name}': {ex.Message}",
+ context.Position, targetType, ex);
+ }
+ }
+
+ ///
+ /// Internal: DeserializeExpression with pre-pooled ContextClass.
+ ///
+ private static Expression? DeserializeExpression(ReadOnlySpan data, Type targetExpressionType, BinaryDeserializationContextClass ctxClass)
+ {
+ var context = new BinaryDeserializationContext(data, ctxClass);
+ try
+ {
+ context.ReadHeader();
+ var node = (AcExpressionNode?)ReadValue(ref context, typeof(AcExpressionNode), 0);
+ if (node == null) return null;
+
+ var entityType = AcSerializerCommon.GetExpressionEntityType(targetExpressionType);
+ return AcExpressionRebuilder.FromNode(node, entityType);
+ }
+ catch (AcBinaryDeserializationException) { throw; }
+ catch (Exception ex)
+ {
+ throw new AcBinaryDeserializationException(
+ $"Failed to deserialize Expression from binary data: {ex.Message}",
+ context.Position, targetExpressionType, ex);
+ }
}
///
@@ -203,7 +348,8 @@ public static partial class AcBinaryDeserializer
///
private static Expression? DeserializeExpression(ReadOnlySpan data, Type targetExpressionType, AcBinarySerializerOptions options)
{
- var context = new BinaryDeserializationContext(data, options);
+ var ctxClass = DeserializationContextClassPool.Get(options);
+ var context = new BinaryDeserializationContext(data, ctxClass);
try
{
@@ -225,6 +371,10 @@ public static partial class AcBinaryDeserializer
$"Failed to deserialize Expression from binary data: {ex.Message}",
context.Position, targetExpressionType, ex);
}
+ finally
+ {
+ DeserializationContextClassPool.Return(ctxClass);
+ }
}
///
@@ -255,7 +405,8 @@ public static partial class AcBinaryDeserializer
if (data.Length == 1 && data[0] == BinaryTypeCode.Null) return;
var targetType = target.GetType();
- var context = new BinaryDeserializationContext(data, options);
+ var ctxClass = DeserializationContextClassPool.Get(options);
+ var context = new BinaryDeserializationContext(data, ctxClass);
try
{
@@ -297,6 +448,10 @@ public static partial class AcBinaryDeserializer
$"Failed to populate object of type '{targetType.Name}': {ex.Message}",
context.Position, targetType, ex);
}
+ finally
+ {
+ DeserializationContextClassPool.Return(ctxClass);
+ }
}
///
@@ -320,7 +475,8 @@ public static partial class AcBinaryDeserializer
var opts = options ?? AcBinarySerializerOptions.Default;
var targetType = target.GetType();
- var context = new BinaryDeserializationContext(data, opts)
+ var ctxClass = DeserializationContextClassPool.Get(opts);
+ var context = new BinaryDeserializationContext(data, ctxClass)
{
IsMergeMode = true,
RemoveOrphanedItems = opts.RemoveOrphanedItems
@@ -382,6 +538,10 @@ public static partial class AcBinaryDeserializer
$"Failed to populate/merge object of type '{targetType.Name}': {ex.Message}",
context.Position, targetType, ex);
}
+ finally
+ {
+ DeserializationContextClassPool.Return(ctxClass);
+ }
}
#endregion
@@ -408,7 +568,8 @@ public static partial class AcBinaryDeserializer
// Copy data to array for chain storage
var dataArray = data.ToArray();
var chainTracker = new AcSerializerCommon.ChainReferenceTracker();
- var context = new BinaryDeserializationContext(dataArray, options) { ChainTracker = chainTracker };
+ var ctxClass = DeserializationContextClassPool.Get(options);
+ var context = new BinaryDeserializationContext(dataArray, ctxClass) { ChainTracker = chainTracker };
try
{
@@ -417,9 +578,9 @@ public static partial class AcBinaryDeserializer
// Position-based string interning - no validation needed
return new BinaryDeserializeChain(dataArray, options, chainTracker, (T?)result);
}
- catch
+ finally
{
- throw;
+ DeserializationContextClassPool.Return(ctxClass);
}
}
@@ -453,7 +614,8 @@ public static partial class AcBinaryDeserializer
ThrowIfDisposed();
var targetType = typeof(TResult);
- var context = new BinaryDeserializationContext(_data, _options) { ChainTracker = _chainTracker };
+ var ctxClass = DeserializationContextClassPool.Get(_options);
+ var context = new BinaryDeserializationContext(_data, ctxClass) { ChainTracker = _chainTracker };
try
{
@@ -469,6 +631,10 @@ public static partial class AcBinaryDeserializer
$"Failed to deserialize to type '{targetType.Name}' in chain: {ex.Message}",
0, targetType, ex);
}
+ finally
+ {
+ DeserializationContextClassPool.Return(ctxClass);
+ }
}
public IDeserializeChain ThenPopulate(object target)
@@ -477,7 +643,8 @@ public static partial class AcBinaryDeserializer
ThrowIfDisposed();
var targetType = target.GetType();
- var context = new BinaryDeserializationContext(_data, _options) { ChainTracker = _chainTracker };
+ var ctxClass = DeserializationContextClassPool.Get(_options);
+ var context = new BinaryDeserializationContext(_data, ctxClass) { ChainTracker = _chainTracker };
try
{
@@ -517,6 +684,10 @@ public static partial class AcBinaryDeserializer
$"Failed to populate object of type '{targetType.Name}' in chain: {ex.Message}",
0, targetType, ex);
}
+ finally
+ {
+ DeserializationContextClassPool.Return(ctxClass);
+ }
}
private void ThrowIfDisposed()
diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs
index 927b822..f58ef46 100644
--- a/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs
+++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs
@@ -82,7 +82,7 @@ public sealed class AcBinarySerializerOptions : AcSerializerOptions
/// allowing the deserializer to match properties by name between different types.
/// Default: false (no overhead)
///
- public bool UseMetadata { get; init; } = true;
+ public bool UseMetadata { get; init; } = false;
///
/// When true, checks for duplicate property name hashes during serialization (UseMetadata mode).