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:
parent
9b4fa1159a
commit
c1dc203dad
|
|
@ -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,38 +883,14 @@ 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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue