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;
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
|
|
@ -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()
|
||||
/// <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;
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
/// <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
|
||||
|
||||
#region Reference Handling
|
||||
|
|
|
|||
|
|
@ -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>(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>
|
||||
/// Serialize object to binary with specified options.
|
||||
/// </summary>
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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