From c1dc203dad5601629d0407c9609b04311f9dfb6f Mon Sep 17 00:00:00 2001 From: Loretta Date: Sat, 7 Feb 2026 15:26:39 +0100 Subject: [PATCH] Refactor header writing; add PooledBufferWriter & IBufferWriter support Refactored AcBinarySerializer to write the binary header directly after scanning for duplicates, removing the placeholder and patching logic for improved performance and simplicity. Introduced a high-performance, ArrayPool-backed PooledBufferWriter for efficient buffer management and pooling. Added a new Serialize overload supporting direct serialization to IBufferWriter, enabling zero-copy and high-throughput scenarios. These changes streamline serialization, reduce memory copying, and enhance extensibility. --- ...rySerializer.BinarySerializationContext.cs | 148 ++---------------- .../Binaries/AcBinarySerializer.cs | 19 ++- .../Binaries/PooledBufferWriter.cs | 136 ++++++++++++++++ 3 files changed, 158 insertions(+), 145 deletions(-) create mode 100644 AyCode.Core/Serializers/Binaries/PooledBufferWriter.cs diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs index 2ed0ab4..cc94b8e 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs @@ -552,96 +552,6 @@ public static partial class AcBinarySerializer _position += 10; } - /// - /// Patches a previously written VarUInt at the specified position. - /// Works correctly only if the new value requires the same or fewer bytes. - /// For property counts < 128, this is always 1 byte. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void PatchVarUInt(int position, uint value) - { - // Fast path: single byte (covers 0-127, which is most property counts) - if (value < 0x80) - { - _buffer[position] = (byte)value; - return; - } - - // Multi-byte case - need to shift buffer if new encoding is longer - // For simplicity, we'll rewrite from the position - // This is rare for property counts - PatchVarUIntSlow(position, value); - } - - [MethodImpl(MethodImplOptions.NoInlining)] - private void PatchVarUIntSlow(int position, uint value) - { - // Calculate current size at position (read until no continuation bit) - var currentSize = 0; - var pos = position; - while (pos < _position && (_buffer[pos] & 0x80) != 0) - { - currentSize++; - pos++; - } - currentSize++; // Include final byte without continuation bit - - // Calculate new size needed - var newSize = GetVarUIntSize(value); - - if (newSize == currentSize) - { - // Same size - just overwrite - var tempPos = position; - while (value >= 0x80) - { - _buffer[tempPos++] = (byte)(value | 0x80); - value >>= 7; - } - _buffer[tempPos] = (byte)value; - } - else if (newSize < currentSize) - { - // New is smaller - shift data left - var delta = currentSize - newSize; - Array.Copy(_buffer, position + currentSize, _buffer, position + newSize, _position - position - currentSize); - _position -= delta; - - var tempPos = position; - while (value >= 0x80) - { - _buffer[tempPos++] = (byte)(value | 0x80); - value >>= 7; - } - _buffer[tempPos] = (byte)value; - } - else - { - // New is larger - shift data right - var delta = newSize - currentSize; - EnsureCapacity(delta); - Array.Copy(_buffer, position + currentSize, _buffer, position + newSize, _position - position - currentSize); - _position += delta; - - var tempPos = position; - while (value >= 0x80) - { - _buffer[tempPos++] = (byte)(value | 0x80); - value >>= 7; - } - _buffer[tempPos] = (byte)value; - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static int GetVarUIntSize(uint value) - { - if (value < 0x80) return 1; - if (value < 0x4000) return 2; - if (value < 0x200000) return 3; - if (value < 0x10000000) return 4; - return 5; - } public void WriteVarInt(int value) { @@ -950,33 +860,19 @@ public static partial class AcBinarySerializer #endregion - #region Header and Metadata - - private int _headerPosition; + #region Header // Marker-based interning: no footer needed // Header: [version][flags][cacheCount (VarUInt, if caching enabled)] // Body: data with markers (StringInternFirst, ObjectRefFirst, etc.) - public void WriteHeaderPlaceholder() + /// + /// Writes the binary header directly. Call AFTER ScanForDuplicates (cacheCount is known). + /// No placeholder, no shift — single forward write. + /// Layout: [version (1b)][flags (1b)][cacheCount (VarUInt, if caching)] + /// + public void WriteHeader() { - // Header layout: - // [0] version (1 byte) - // [1] flags (1 byte) - // [2+] cache count (VarUInt, max 5 bytes, if caching enabled) - EnsureCapacity(HasCaching ? 7 : 2); - _headerPosition = _position; - _position += HasCaching ? 7 : 2; // Reserve max VarUInt size - } - - public void FinalizeHeaderSections() - { - var cacheCount = GetCacheCount(); - - // Rewrite markers for first occurrences (String→StringInternFirst, Object→ObjectRefFirst, etc.) - //RewriteMarkers(); - - // Write header var flags = BinaryTypeCode.HeaderFlagsBase; if (UseMetadata) flags |= BinaryTypeCode.HeaderFlag_Metadata; @@ -987,39 +883,15 @@ public static partial class AcBinarySerializer if (HasCaching) flags |= BinaryTypeCode.HeaderFlag_HasCacheCount; - _buffer[_headerPosition] = AcBinarySerializerOptions.FormatVersion; - _buffer[_headerPosition + 1] = flags; + WriteByte(AcBinarySerializerOptions.FormatVersion); + WriteByte(flags); - // Write cache count and compact header if needed if (HasCaching) { - var headerEnd = WriteVarUIntAt(_headerPosition + 2, (uint)cacheCount); - var reserved = _headerPosition + 7; - if (headerEnd < reserved) - { - // Shift body left to remove unused header bytes - var shift = reserved - headerEnd; - _buffer.AsSpan(reserved, _position - reserved).CopyTo(_buffer.AsSpan(headerEnd)); - _position -= shift; - } + WriteVarUInt((uint)GetCacheCount()); } } - /// - /// Writes VarUInt at specific position and returns new position. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private int WriteVarUIntAt(int pos, uint value) - { - while (value >= 0x80) - { - _buffer[pos++] = (byte)(value | 0x80); - value >>= 7; - } - _buffer[pos++] = (byte)value; - return pos; - } - #endregion #region Reference Handling diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs index 4ebdd40..afa4a8c 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs @@ -90,9 +90,8 @@ public static partial class AcBinarySerializer }; // Run serialization to trigger callbacks - context.WriteHeaderPlaceholder(); + context.WriteHeader(); WriteValue(value, runtimeType, context, 0); - context.FinalizeHeaderSections(); return result; } @@ -220,6 +219,12 @@ public static partial class AcBinarySerializer [MethodImpl(MethodImplOptions.AggressiveInlining)] public static byte[] Serialize(T value) => Serialize(value, AcBinarySerializerOptions.Default); + /// + /// Serialize object to an IBufferWriter with default options. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Serialize(T value, IBufferWriter writer) => Serialize(value, writer, AcBinarySerializerOptions.Default); + /// /// Serialize object to binary with specified options. /// @@ -379,15 +384,15 @@ public static partial class AcBinarySerializer BinarySerializationContext.GrowBufferTotalBytes = 0; #endif var context = BinarySerializationContextPool.Get(options); - context.WriteHeaderPlaceholder(); - // Two-pass serialization when caching is enabled: + // Two-pass serialization: // 1. Scan pass: identify duplicates (strings + objects), assign CacheIndex - // 2. Serialize pass: write data with references + // 2. Write header (cacheCount is now known - no placeholder, no shift) + // 3. Serialize pass: write body with references ScanForDuplicates(value, runtimeType, context); - + context.WriteHeader(); WriteValue(value, runtimeType, context, 0); - context.FinalizeHeaderSections(); + return context; } diff --git a/AyCode.Core/Serializers/Binaries/PooledBufferWriter.cs b/AyCode.Core/Serializers/Binaries/PooledBufferWriter.cs new file mode 100644 index 0000000..144b352 --- /dev/null +++ b/AyCode.Core/Serializers/Binaries/PooledBufferWriter.cs @@ -0,0 +1,136 @@ +using System; +using System.Buffers; +using System.Runtime.CompilerServices; + +namespace AyCode.Core.Serializers.Binaries; + +/// +/// High-performance ArrayPool-backed IBufferWriter. +/// Designed for pooling and reuse - supports Reset() without deallocation. +/// Unlike BCL ArrayBufferWriter, this uses ArrayPool for zero-alloc buffer management. +/// +internal sealed class PooledBufferWriter : IBufferWriter, IDisposable +{ + private byte[] _buffer; + private int _written; + + public PooledBufferWriter(int initialCapacity) + { + _buffer = ArrayPool.Shared.Rent(Math.Max(initialCapacity, 256)); + } + + /// Written data as ReadOnlySpan (no allocation). + public ReadOnlySpan WrittenSpan + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _buffer.AsSpan(0, _written); + } + + /// Written data as ReadOnlyMemory. + public ReadOnlyMemory WrittenMemory + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _buffer.AsMemory(0, _written); + } + + /// Number of bytes written so far. + public int WrittenCount + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _written; + } + + /// Direct access to backing array for Unsafe.WriteUnaligned operations. + public byte[] RawBuffer + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _buffer; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Advance(int count) => _written += count; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Memory GetMemory(int sizeHint = 0) + { + EnsureCapacity(sizeHint); + return _buffer.AsMemory(_written); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Span GetSpan(int sizeHint = 0) + { + EnsureCapacity(sizeHint); + return _buffer.AsSpan(_written); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void EnsureCapacity(int sizeHint) + { + if (sizeHint <= _buffer.Length - _written) return; + Grow(sizeHint); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void Grow(int sizeHint) + { + var newSize = Math.Max(_buffer.Length * 2, _written + Math.Max(sizeHint, 1)); + var newBuffer = ArrayPool.Shared.Rent(newSize); + _buffer.AsSpan(0, _written).CopyTo(newBuffer); + ArrayPool.Shared.Return(_buffer); + _buffer = newBuffer; + } + + /// Copy written data to a new exactly-sized array. + public byte[] ToArray() + { + var result = GC.AllocateUninitializedArray(_written); + _buffer.AsSpan(0, _written).CopyTo(result); + return result; + } + + /// Copy written data to an external IBufferWriter (single memcpy). + public void CopyTo(IBufferWriter destination) + { + var span = destination.GetSpan(_written); + _buffer.AsSpan(0, _written).CopyTo(span); + destination.Advance(_written); + } + + /// + /// Detach the backing buffer for zero-copy transfer. + /// Caller is responsible for returning the buffer to ArrayPool. + /// + public (byte[] Buffer, int Length) Detach() + { + var buf = _buffer; + var len = _written; + _buffer = ArrayPool.Shared.Rent(256); + _written = 0; + return (buf, len); + } + + /// Reset for reuse (no deallocation). + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Reset() => _written = 0; + + /// Reset with optional buffer resize if too small. + public void Reset(int newMinCapacity) + { + _written = 0; + if (_buffer.Length < newMinCapacity) + { + ArrayPool.Shared.Return(_buffer); + _buffer = ArrayPool.Shared.Rent(newMinCapacity); + } + } + + public void Dispose() + { + if (_buffer != null) + { + ArrayPool.Shared.Return(_buffer); + _buffer = null!; + } + } +}