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<T> overload supporting direct serialization to IBufferWriter<byte>, enabling zero-copy and high-throughput scenarios. These changes streamline serialization, reduce memory copying, and enhance extensibility.
This commit is contained in:
Loretta 2026-02-07 15:26:39 +01:00
parent 9b4fa1159a
commit c1dc203dad
3 changed files with 158 additions and 145 deletions

View File

@ -552,96 +552,6 @@ public static partial class AcBinarySerializer
_position += 10; _position += 10;
} }
/// <summary>
/// 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.
/// </summary>
[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) public void WriteVarInt(int value)
{ {
@ -950,33 +860,19 @@ public static partial class AcBinarySerializer
#endregion #endregion
#region Header and Metadata #region Header
private int _headerPosition;
// Marker-based interning: no footer needed // Marker-based interning: no footer needed
// Header: [version][flags][cacheCount (VarUInt, if caching enabled)] // Header: [version][flags][cacheCount (VarUInt, if caching enabled)]
// Body: data with markers (StringInternFirst, ObjectRefFirst, etc.) // Body: data with markers (StringInternFirst, ObjectRefFirst, etc.)
public void WriteHeaderPlaceholder() /// <summary>
/// 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)]
/// </summary>
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; var flags = BinaryTypeCode.HeaderFlagsBase;
if (UseMetadata) if (UseMetadata)
flags |= BinaryTypeCode.HeaderFlag_Metadata; flags |= BinaryTypeCode.HeaderFlag_Metadata;
@ -987,39 +883,15 @@ public static partial class AcBinarySerializer
if (HasCaching) if (HasCaching)
flags |= BinaryTypeCode.HeaderFlag_HasCacheCount; flags |= BinaryTypeCode.HeaderFlag_HasCacheCount;
_buffer[_headerPosition] = AcBinarySerializerOptions.FormatVersion; WriteByte(AcBinarySerializerOptions.FormatVersion);
_buffer[_headerPosition + 1] = flags; WriteByte(flags);
// Write cache count and compact header if needed
if (HasCaching) if (HasCaching)
{ {
var headerEnd = WriteVarUIntAt(_headerPosition + 2, (uint)cacheCount); WriteVarUInt((uint)GetCacheCount());
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;
}
} }
} }
/// <summary>
/// Writes VarUInt at specific position and returns new position.
/// </summary>
[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 #endregion
#region Reference Handling #region Reference Handling

View File

@ -90,9 +90,8 @@ public static partial class AcBinarySerializer
}; };
// Run serialization to trigger callbacks // Run serialization to trigger callbacks
context.WriteHeaderPlaceholder(); context.WriteHeader();
WriteValue(value, runtimeType, context, 0); WriteValue(value, runtimeType, context, 0);
context.FinalizeHeaderSections();
return result; return result;
} }
@ -220,6 +219,12 @@ public static partial class AcBinarySerializer
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static byte[] Serialize<T>(T value) => Serialize(value, AcBinarySerializerOptions.Default); public static byte[] Serialize<T>(T value) => Serialize(value, AcBinarySerializerOptions.Default);
/// <summary>
/// Serialize object to an IBufferWriter with default options.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Serialize<T>(T value, IBufferWriter<byte> writer) => Serialize(value, writer, AcBinarySerializerOptions.Default);
/// <summary> /// <summary>
/// Serialize object to binary with specified options. /// Serialize object to binary with specified options.
/// </summary> /// </summary>
@ -379,15 +384,15 @@ public static partial class AcBinarySerializer
BinarySerializationContext.GrowBufferTotalBytes = 0; BinarySerializationContext.GrowBufferTotalBytes = 0;
#endif #endif
var context = BinarySerializationContextPool.Get(options); 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 // 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); ScanForDuplicates(value, runtimeType, context);
context.WriteHeader();
WriteValue(value, runtimeType, context, 0); WriteValue(value, runtimeType, context, 0);
context.FinalizeHeaderSections();
return context; return context;
} }

View File

@ -0,0 +1,136 @@
using System;
using System.Buffers;
using System.Runtime.CompilerServices;
namespace AyCode.Core.Serializers.Binaries;
/// <summary>
/// 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.
/// </summary>
internal sealed class PooledBufferWriter : IBufferWriter<byte>, IDisposable
{
private byte[] _buffer;
private int _written;
public PooledBufferWriter(int initialCapacity)
{
_buffer = ArrayPool<byte>.Shared.Rent(Math.Max(initialCapacity, 256));
}
/// <summary>Written data as ReadOnlySpan (no allocation).</summary>
public ReadOnlySpan<byte> WrittenSpan
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _buffer.AsSpan(0, _written);
}
/// <summary>Written data as ReadOnlyMemory.</summary>
public ReadOnlyMemory<byte> WrittenMemory
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _buffer.AsMemory(0, _written);
}
/// <summary>Number of bytes written so far.</summary>
public int WrittenCount
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _written;
}
/// <summary>Direct access to backing array for Unsafe.WriteUnaligned operations.</summary>
public byte[] RawBuffer
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _buffer;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Advance(int count) => _written += count;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Memory<byte> GetMemory(int sizeHint = 0)
{
EnsureCapacity(sizeHint);
return _buffer.AsMemory(_written);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Span<byte> 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<byte>.Shared.Rent(newSize);
_buffer.AsSpan(0, _written).CopyTo(newBuffer);
ArrayPool<byte>.Shared.Return(_buffer);
_buffer = newBuffer;
}
/// <summary>Copy written data to a new exactly-sized array.</summary>
public byte[] ToArray()
{
var result = GC.AllocateUninitializedArray<byte>(_written);
_buffer.AsSpan(0, _written).CopyTo(result);
return result;
}
/// <summary>Copy written data to an external IBufferWriter (single memcpy).</summary>
public void CopyTo(IBufferWriter<byte> destination)
{
var span = destination.GetSpan(_written);
_buffer.AsSpan(0, _written).CopyTo(span);
destination.Advance(_written);
}
/// <summary>
/// Detach the backing buffer for zero-copy transfer.
/// Caller is responsible for returning the buffer to ArrayPool.
/// </summary>
public (byte[] Buffer, int Length) Detach()
{
var buf = _buffer;
var len = _written;
_buffer = ArrayPool<byte>.Shared.Rent(256);
_written = 0;
return (buf, len);
}
/// <summary>Reset for reuse (no deallocation).</summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Reset() => _written = 0;
/// <summary>Reset with optional buffer resize if too small.</summary>
public void Reset(int newMinCapacity)
{
_written = 0;
if (_buffer.Length < newMinCapacity)
{
ArrayPool<byte>.Shared.Return(_buffer);
_buffer = ArrayPool<byte>.Shared.Rent(newMinCapacity);
}
}
public void Dispose()
{
if (_buffer != null)
{
ArrayPool<byte>.Shared.Return(_buffer);
_buffer = null!;
}
}
}