From 5601c0d3e25adca519b6d6e6b221c720de17a670 Mon Sep 17 00:00:00 2001 From: Loretta Date: Sun, 14 Dec 2025 10:20:07 +0100 Subject: [PATCH] Refactor serialization core, add pooled buffer support Centralize serialization steps into a new SerializeCore method for consistency and maintainability. Rework property metadata registration to operate on types instead of instances, improving efficiency. Replace property index tracking with stack-allocated or pooled buffers to reduce allocations. Add SerializeToPooledBuffer and BinarySerializationResult for zero-copy serialization with proper buffer pooling and disposal. Simplify string writing logic and use GC.AllocateUninitializedArray for result arrays. Refactor and add helper methods for buffer management and metadata handling. --- AyCode.Core/Extensions/AcBinarySerializer.cs | 415 +++++++++++++------ 1 file changed, 287 insertions(+), 128 deletions(-) diff --git a/AyCode.Core/Extensions/AcBinarySerializer.cs b/AyCode.Core/Extensions/AcBinarySerializer.cs index 881abe4..562336d 100644 --- a/AyCode.Core/Extensions/AcBinarySerializer.cs +++ b/AyCode.Core/Extensions/AcBinarySerializer.cs @@ -1,6 +1,7 @@ using System.Buffers; using System.Collections; using System.Collections.Concurrent; +using System.Collections.Generic; using System.Globalization; using System.Linq.Expressions; using System.Reflection; @@ -47,34 +48,10 @@ public static class AcBinarySerializer return [BinaryTypeCode.Null]; } - var type = value.GetType(); - - // Use context-based serialization for all types to ensure consistent format - var context = BinarySerializationContextPool.Get(options); + var runtimeType = value.GetType(); + var context = SerializeCore(value, runtimeType, options); try { - // Reserve space for header - context.WriteHeaderPlaceholder(); - - // Phase 1: If reference handling enabled, scan for multi-referenced objects - if (options.UseReferenceHandling && !IsPrimitiveOrStringFast(type)) - { - ScanReferences(value, context, 0); - } - - // Phase 2: If metadata enabled, collect property names - if (options.UseMetadata && !IsPrimitiveOrStringFast(type)) - { - CollectPropertyNames(value, context, 0); - } - - // Write metadata section - context.WriteMetadata(); - - // Phase 3: Write the actual data - WriteValue(value, type, context, 0); - - // Finalize and return return context.ToArray(); } finally @@ -97,26 +74,10 @@ public static class AcBinarySerializer return; } - var type = value.GetType(); - var context = BinarySerializationContextPool.Get(options); + var runtimeType = value.GetType(); + var context = SerializeCore(value, runtimeType, options); try { - context.WriteHeaderPlaceholder(); - - if (options.UseReferenceHandling && !IsPrimitiveOrStringFast(type)) - { - ScanReferences(value, context, 0); - } - - if (options.UseMetadata && !IsPrimitiveOrStringFast(type)) - { - CollectPropertyNames(value, context, 0); - } - - context.WriteMetadata(); - WriteValue(value, type, context, 0); - - // Write directly to the IBufferWriter instead of creating a new array context.WriteTo(writer); } finally @@ -133,25 +94,10 @@ public static class AcBinarySerializer { if (value == null) return 1; - var type = value.GetType(); - var context = BinarySerializationContextPool.Get(options); + var runtimeType = value.GetType(); + var context = SerializeCore(value, runtimeType, options); try { - context.WriteHeaderPlaceholder(); - - if (options.UseReferenceHandling && !IsPrimitiveOrStringFast(type)) - { - ScanReferences(value, context, 0); - } - - if (options.UseMetadata && !IsPrimitiveOrStringFast(type)) - { - CollectPropertyNames(value, context, 0); - } - - context.WriteMetadata(); - WriteValue(value, type, context, 0); - return context.Position; } finally @@ -160,6 +106,49 @@ public static class AcBinarySerializer } } + /// + /// Serialize object and keep the pooled buffer for zero-copy consumers. + /// Caller must dispose the returned result to release the buffer. + /// + public static BinarySerializationResult SerializeToPooledBuffer(T value, AcBinarySerializerOptions options) + { + if (value == null) + { + return BinarySerializationResult.FromImmutable([BinaryTypeCode.Null]); + } + + var runtimeType = value.GetType(); + var context = SerializeCore(value, runtimeType, options); + try + { + return context.DetachResult(); + } + finally + { + BinarySerializationContextPool.Return(context); + } + } + + private static BinarySerializationContext SerializeCore(object value, Type runtimeType, AcBinarySerializerOptions options) + { + var context = BinarySerializationContextPool.Get(options); + context.WriteHeaderPlaceholder(); + + if (options.UseReferenceHandling && !IsPrimitiveOrStringFast(runtimeType)) + { + ScanReferences(value, context, 0); + } + + if (options.UseMetadata && !IsPrimitiveOrStringFast(runtimeType)) + { + RegisterMetadataForType(runtimeType, context); + } + + context.WriteMetadata(); + WriteValue(value, runtimeType, context, 0); + return context; + } + #endregion #region Reference Scanning @@ -205,33 +194,28 @@ public static class AcBinarySerializer #endregion - #region Property Name Collection + #region Property Metadata Registration - private static void CollectPropertyNames(object? value, BinarySerializationContext context, int depth) + private static void RegisterMetadataForType(Type type, BinarySerializationContext context, HashSet? visited = null) { - if (value == null || depth > context.MaxDepth) return; - - var type = value.GetType(); if (IsPrimitiveOrStringFast(type)) return; - if (value is byte[]) return; + visited ??= new HashSet(); + if (!visited.Add(type)) return; - if (value is IDictionary dictionary) + if (IsDictionaryType(type, out var keyType, out var valueType)) { - foreach (DictionaryEntry entry in dictionary) - { - if (entry.Value != null) - CollectPropertyNames(entry.Value, context, depth + 1); - } + if (keyType != null) RegisterMetadataForType(keyType, context, visited); + if (valueType != null) RegisterMetadataForType(valueType, context, visited); return; } - if (value is IEnumerable enumerable && !ReferenceEquals(type, StringType)) + if (typeof(IEnumerable).IsAssignableFrom(type) && !ReferenceEquals(type, StringType)) { - foreach (var item in enumerable) + var elementType = GetCollectionElementType(type); + if (elementType != null) { - if (item != null) - CollectPropertyNames(item, context, depth + 1); + RegisterMetadataForType(elementType, context, visited); } return; } @@ -240,12 +224,46 @@ public static class AcBinarySerializer foreach (var prop in metadata.Properties) { context.RegisterPropertyName(prop.Name); - var propValue = prop.GetValue(value); - if (propValue != null) - CollectPropertyNames(propValue, context, depth + 1); + + if (TryResolveNestedMetadataType(prop.PropertyType, out var nestedType)) + { + RegisterMetadataForType(nestedType, context, visited); + } } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool TryResolveNestedMetadataType(Type propertyType, out Type nestedType) + { + nestedType = Nullable.GetUnderlyingType(propertyType) ?? propertyType; + + if (IsPrimitiveOrStringFast(nestedType)) + return false; + + if (IsDictionaryType(nestedType, out var _, out var valueType) && valueType != null) + { + if (!IsPrimitiveOrStringFast(valueType)) + { + nestedType = valueType; + return true; + } + return false; + } + + if (typeof(IEnumerable).IsAssignableFrom(nestedType) && !ReferenceEquals(nestedType, StringType)) + { + var elementType = GetCollectionElementType(nestedType); + if (elementType != null && !IsPrimitiveOrStringFast(elementType)) + { + nestedType = elementType; + return true; + } + return false; + } + + return true; + } + #endregion #region Value Writing @@ -606,27 +624,35 @@ public static class AcBinarySerializer var properties = metadata.Properties; var propCount = properties.Length; - // Single-pass: count and collect non-null, non-default properties - // Use stackalloc for small property counts to avoid allocation - Span validIndices = propCount <= 32 ? stackalloc int[propCount] : new int[propCount]; + const int StackThreshold = 64; + byte[]? rentedStates = null; + Span propertyStates = propCount <= StackThreshold + ? stackalloc byte[propCount] + : (rentedStates = context.RentPropertyStateBuffer(propCount)).AsSpan(0, propCount); + propertyStates.Clear(); var writtenCount = 0; for (var i = 0; i < propCount; i++) { - var prop = properties[i]; - if (IsPropertyDefaultOrNull(value, prop)) + if (IsPropertyDefaultOrNull(value, properties[i])) + { + propertyStates[i] = 0; continue; - validIndices[writtenCount++] = i; + } + + propertyStates[i] = 1; + writtenCount++; } context.WriteVarUInt((uint)writtenCount); - // Write only the valid properties - for (var j = 0; j < writtenCount; j++) + for (var i = 0; i < propCount; i++) { - var prop = properties[validIndices[j]]; + if (propertyStates[i] == 0) + continue; + + var prop = properties[i]; - // Write property index or name if (context.UseMetadata) { var propIndex = context.GetPropertyNameIndex(prop.Name); @@ -637,9 +663,13 @@ public static class AcBinarySerializer WriteString(prop.Name, context); } - // Use typed writers to avoid boxing WritePropertyValue(value, prop, context, nextDepth); } + + if (rentedStates != null) + { + context.ReturnPropertyStateBuffer(rentedStates); + } } /// @@ -1089,9 +1119,12 @@ public static class AcBinarySerializer { private byte[] _buffer; private int _position; + private int _initialBufferSize; // Minimum buffer size for ArrayPool (reduces fragmentation) private const int MinBufferSize = 256; + private const int PropertyIndexBufferMaxCache = 512; + private const int PropertyStateBufferMaxCache = 512; // Reference handling private Dictionary? _scanOccurrences; @@ -1106,6 +1139,8 @@ public static class AcBinarySerializer // Property name table private Dictionary? _propertyNames; private List? _propertyNameList; + private int[]? _propertyIndexBuffer; + private byte[]? _propertyStateBuffer; public bool UseReferenceHandling { get; private set; } public bool UseStringInterning { get; private set; } @@ -1115,8 +1150,8 @@ public static class AcBinarySerializer public BinarySerializationContext(AcBinarySerializerOptions options) { - var size = Math.Max(options.InitialBufferCapacity, MinBufferSize); - _buffer = ArrayPool.Shared.Rent(size); + _initialBufferSize = Math.Max(options.InitialBufferCapacity, MinBufferSize); + _buffer = ArrayPool.Shared.Rent(_initialBufferSize); Reset(options); } @@ -1129,6 +1164,13 @@ public static class AcBinarySerializer UseMetadata = options.UseMetadata; MaxDepth = options.MaxDepth; MinStringInternLength = options.MinStringInternLength; + _initialBufferSize = Math.Max(options.InitialBufferCapacity, MinBufferSize); + + if (_buffer.Length < _initialBufferSize) + { + ArrayPool.Shared.Return(_buffer); + _buffer = ArrayPool.Shared.Rent(_initialBufferSize); + } } public void Clear() @@ -1146,29 +1188,17 @@ public static class AcBinarySerializer _internedStringList?.Clear(); _propertyNameList?.Clear(); - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void ClearAndTrimIfNeeded(Dictionary? dict, int maxCapacity) where TKey : notnull - { - if (dict == null) return; - dict.Clear(); - // TrimExcess only if the dictionary grew significantly beyond initial capacity - if (dict.EnsureCapacity(0) > maxCapacity) + if (_propertyIndexBuffer != null && _propertyIndexBuffer.Length > PropertyIndexBufferMaxCache) { - dict.TrimExcess(); + ArrayPool.Shared.Return(_propertyIndexBuffer); + _propertyIndexBuffer = null; } - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void ClearAndTrimIfNeeded(HashSet? set, int maxCapacity) - { - if (set == null) return; - set.Clear(); - // TrimExcess only if the set grew significantly beyond initial capacity - if (set.EnsureCapacity(0) > maxCapacity) + if (_propertyStateBuffer != null && _propertyStateBuffer.Length > PropertyStateBufferMaxCache) { - set.TrimExcess(); + ArrayPool.Shared.Return(_propertyStateBuffer); + _propertyStateBuffer = null; } } @@ -1179,6 +1209,18 @@ public static class AcBinarySerializer ArrayPool.Shared.Return(_buffer); _buffer = null!; } + + if (_propertyIndexBuffer != null) + { + ArrayPool.Shared.Return(_propertyIndexBuffer); + _propertyIndexBuffer = null; + } + + if (_propertyStateBuffer != null) + { + ArrayPool.Shared.Return(_propertyStateBuffer); + _propertyStateBuffer = null; + } } #region Optimized Buffer Writing @@ -1341,22 +1383,11 @@ public static class AcBinarySerializer [MethodImpl(MethodImplOptions.AggressiveInlining)] public void WriteStringUtf8(string value) { - // For small strings, use stackalloc to avoid GetByteCount call - if (value.Length <= 128) - { - Span tempBuffer = stackalloc byte[value.Length * 3]; // Max UTF8 expansion - var bytesWritten = Encoding.UTF8.GetBytes(value.AsSpan(), tempBuffer); - WriteVarUInt((uint)bytesWritten); - WriteBytes(tempBuffer.Slice(0, bytesWritten)); - } - else - { - var utf8Length = Encoding.UTF8.GetByteCount(value); - WriteVarUInt((uint)utf8Length); - EnsureCapacity(utf8Length); - Encoding.UTF8.GetBytes(value, _buffer.AsSpan(_position, utf8Length)); - _position += utf8Length; - } + var byteCount = Utf8NoBom.GetByteCount(value); + WriteVarUInt((uint)byteCount); + EnsureCapacity(byteCount); + Utf8NoBom.GetBytes(value.AsSpan(), _buffer.AsSpan(_position, byteCount)); + _position += byteCount; } #endregion @@ -1620,15 +1651,78 @@ public static class AcBinarySerializer return _propertyNames!.TryGetValue(name, out var index) ? index : -1; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int[] RentPropertyIndexBuffer(int minimumLength) + { + if (_propertyIndexBuffer != null && _propertyIndexBuffer.Length >= minimumLength) + { + var buffer = _propertyIndexBuffer; + _propertyIndexBuffer = null; + return buffer; + } + + return ArrayPool.Shared.Rent(minimumLength); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ReturnPropertyIndexBuffer(int[] buffer) + { + if (_propertyIndexBuffer == null && buffer.Length <= PropertyIndexBufferMaxCache) + { + _propertyIndexBuffer = buffer; + return; + } + + ArrayPool.Shared.Return(buffer); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public byte[] RentPropertyStateBuffer(int minimumLength) + { + if (_propertyStateBuffer != null && _propertyStateBuffer.Length >= minimumLength) + { + var buffer = _propertyStateBuffer; + _propertyStateBuffer = null; + return buffer; + } + + return ArrayPool.Shared.Rent(minimumLength); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ReturnPropertyStateBuffer(byte[] buffer) + { + if (_propertyStateBuffer == null && buffer.Length <= PropertyStateBufferMaxCache) + { + _propertyStateBuffer = buffer; + return; + } + + ArrayPool.Shared.Return(buffer); + } + #endregion public byte[] ToArray() { - var result = new byte[_position]; + var result = GC.AllocateUninitializedArray(_position); _buffer.AsSpan(0, _position).CopyTo(result); return result; } + public BinarySerializationResult DetachResult() + { + var buffer = _buffer; + var length = _position; + var result = new BinarySerializationResult(buffer, length, pooled: true); + + var newSize = Math.Max(_initialBufferSize, MinBufferSize); + _buffer = ArrayPool.Shared.Rent(newSize); + _position = 0; + + return result; + } + public void WriteTo(IBufferWriter writer) { // Directly write the internal buffer to the IBufferWriter @@ -1638,6 +1732,71 @@ public static class AcBinarySerializer } public int Position => _position; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void ClearAndTrimIfNeeded(Dictionary? dict, int maxCapacity) where TKey : notnull + { + if (dict == null) return; + dict.Clear(); + if (dict.EnsureCapacity(0) > maxCapacity) + { + dict.TrimExcess(); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void ClearAndTrimIfNeeded(HashSet? set, int maxCapacity) + { + if (set == null) return; + set.Clear(); + if (set.EnsureCapacity(0) > maxCapacity) + { + set.TrimExcess(); + } + } + } + + #endregion + + #region Serialization Result + + public sealed class BinarySerializationResult : IDisposable + { + private readonly bool _pooled; + private bool _disposed; + + internal BinarySerializationResult(byte[] buffer, int length, bool pooled) + { + Buffer = buffer; + Length = length; + _pooled = pooled; + } + + public byte[] Buffer { get; } + public int Length { get; } + public ReadOnlySpan Span => Buffer.AsSpan(0, Length); + public ReadOnlyMemory Memory => new(Buffer, 0, Length); + + public byte[] ToArray() + { + var result = GC.AllocateUninitializedArray(Length); + Buffer.AsSpan(0, Length).CopyTo(result); + return result; + } + + public void Dispose() + { + if (_disposed) return; + _disposed = true; + + if (_pooled) + { + ArrayPool.Shared.Return(Buffer); + } + } + + internal static BinarySerializationResult FromImmutable(byte[] buffer) + => new(buffer, buffer.Length, pooled: false); } #endregion