Refactor: move buffer/position to context for zero dispatch

Major serialization pipeline refactor: all hot-path buffer and position management is now owned by BinarySerializationContext<TOutput>, with all write methods inlined for zero virtual dispatch. TOutput (now struct, IBinaryOutputBase) handles only cold-path buffer management (Initialize, Grow, GetTotalPosition). ArrayBinaryOutput and BufferWriterBinaryOutput are simplified to buffer managers. IBinaryOutput and BinaryOutputBase are removed. All serialization logic now uses context write methods (~130 call sites updated). This yields significant performance gains by eliminating virtual/interface calls on the serialization hot path.
This commit is contained in:
Loretta 2026-02-11 13:02:24 +01:00
parent 270f1b8265
commit 991e8f6038
11 changed files with 1089 additions and 1274 deletions

View File

@ -32,7 +32,8 @@
"Bash(find:*)",
"Bash(dir:*)",
"Bash(git stash:*)",
"WebFetch(domain:github.com)"
"WebFetch(domain:github.com)",
"Bash(del \"H:\\\\Applications\\\\Aycode\\\\Source\\\\AyCode.Core\\\\AyCode.Core\\\\Serializers\\\\Binaries\\\\IBinaryOutput.cs\")"
]
}
}

210
.plan Normal file
View File

@ -0,0 +1,210 @@
# Buffer-in-Context terv: `_buffer`/`_position` visszahelyezése a context-be
## Probléma
A TOutput generic refaktorálás ~30-40%-os serialization regressziót okozott.
Ok: .NET JIT reference type generikusoknál SHARED kódot generál → minden `output.WriteByte()` virtuális dispatch, még sealed osztályoknál is.
## Megoldás
`_buffer` + `_position` visszakerül a `BinarySerializationContext<TOutput>`-ba.
Minden hot path write metódus inline context metódus lesz: `_buffer[_position++] = value`.
A `TOutput Output` kizárólag cold path Grow/Flush-t kezel.
---
## 1. Új BinaryOutputBase (3 absztrakt metódus)
A jelenlegi 19 abstract + 9 virtual metódus helyett:
```csharp
public abstract class BinaryOutputBase
{
public abstract void Initialize(out byte[] buffer, out int position, out int bufferEnd);
public abstract void Grow(ref byte[] buffer, ref int position, ref int bufferEnd, int needed);
public abstract int GetTotalPosition(int currentPosition);
}
```
- `Initialize`: kezdeti buffer kiadása
- `Grow`: cold path — buffer betelik → ArrayPool.Rent/copy VAGY Advance+GetMemory
- `GetTotalPosition`: Position property-hez (cold path, 1x hívás per serialize)
- `IBinaryOutput.cs` törlése (nem implementálja többé senki)
## 2. BinarySerializationContext<TOutput> — új mezők + write metódusok
### Új mezők:
```csharp
internal byte[] _buffer = null!;
internal int _position;
internal int _bufferEnd; // writeable terület vége (_position < _bufferEnd)
```
### Position property:
```csharp
public int Position => Output.GetTotalPosition(_position);
// ArrayBinaryOutput: return currentPosition (CommittedBytes=0, bufferStart=0)
// BufferWriterBinaryOutput: return _committedBytes + (currentPosition - _chunkStart)
```
### EnsureCapacity (privát):
```csharp
[AggressiveInlining]
private void EnsureCapacity(int additionalBytes)
{
if (_position + additionalBytes > _bufferEnd)
Output.Grow(ref _buffer, ref _position, ref _bufferEnd, additionalBytes);
}
```
### Write metódusok (mind [AggressiveInlining], az ArrayBinaryOutput implementációiból portolva):
1. WriteByte(byte)
2. WriteTwoBytes(byte, byte)
3. WriteBytes(ReadOnlySpan<byte>)
4. WriteRaw<T>(T) where T : unmanaged
5. WriteTypeCodeAndRaw<T>(byte, T)
6. WriteVarUInt(uint) — fast path < 0x80
7. WriteVarInt(int) — ZigZag + WriteVarUInt
8. WriteVarULong(ulong)
9. WriteVarLong(long)
10. WriteDecimalBits(decimal)
11. WriteDateTimeBits(DateTime)
12. WriteGuidBits(Guid)
13. WriteDateTimeOffsetBits(DateTimeOffset)
14. WriteStringUtf8(string)
15. WriteFixStr(string)
16. WriteFixStrDirect(string)
17. WriteFixStrBytes(ReadOnlySpan<byte>)
18. WritePreencodedPropertyName(ReadOnlySpan<byte>)
19. WriteDoubleArrayBulk(double[])
20. WriteFloatArrayBulk(float[])
21. WriteGuidArrayBulk(Guid[])
22. WriteInt32ArrayOptimized(int[])
23. WriteLongArrayOptimized(long[])
24. WriteBytesSimd(ReadOnlySpan<byte>)
Mind `_buffer[_position++]` pattern-nel — NULLA virtual dispatch a hot path-on.
### WriteHeader / WriteInlineMetadata:
- `Output.WriteByte()` → `WriteByte()` (self)
- `WriteInlineMetadata` signature: `output` param eltávolítása
## 3. ArrayBinaryOutput (~430 → ~120 sor)
```csharp
public sealed class ArrayBinaryOutput : BinaryOutputBase, IDisposable
{
private byte[] _rentedBuffer;
public override void Initialize(out byte[] buffer, out int position, out int bufferEnd)
{
buffer = _rentedBuffer; position = 0; bufferEnd = _rentedBuffer.Length;
}
[NoInlining]
public override void Grow(ref byte[] buffer, ref int position, ref int bufferEnd, int needed)
{
// ArrayPool.Rent bigger + copy + return old
// position marad, bufferEnd = newBuffer.Length
_rentedBuffer = newBuffer;
}
public override int GetTotalPosition(int currentPosition) => currentPosition;
// Eredmény metódusok — buffer/position paramétert kapnak a context-ből:
public ReadOnlySpan<byte> AsSpan(byte[] buffer, int position);
public byte[] ToArray(byte[] buffer, int position);
public BinarySerializationResult DetachResult(byte[] buffer, int position);
public void WriteTo(IBufferWriter<byte> writer, byte[] buffer, int position);
}
```
## 4. BufferWriterBinaryOutput (~350 → ~100 sor)
```csharp
public sealed class BufferWriterBinaryOutput : BinaryOutputBase
{
private readonly IBufferWriter<byte> _writer;
private int _committedBytes;
private int _currentChunkStart;
private bool _ownedBuffer;
public override void Initialize(out byte[] buffer, out int position, out int bufferEnd)
{
_committedBytes = 0;
AcquireChunk(MinChunkRequest, out buffer, out position, out bufferEnd);
_currentChunkStart = position;
}
[NoInlining]
public override void Grow(ref byte[] buffer, ref int position, ref int bufferEnd, int needed)
{
// 1. Advance current chunk: _writer.Advance(position - _currentChunkStart)
// 2. _committedBytes += bytesInChunk
// 3. AcquireChunk(needed, out buffer, out position, out bufferEnd)
// 4. _currentChunkStart = position
}
public override int GetTotalPosition(int currentPosition)
=> _committedBytes + (currentPosition - _currentChunkStart);
public void Flush(byte[] buffer, int position)
{
// Utolsó chunk commit-ja
}
private void AcquireChunk(int requestSize, out byte[] buffer, out int position, out int bufferEnd)
{
// GetMemory() + TryGetArray() → buffer=segment.Array, position=segment.Offset
// Fallback: ArrayPool.Rent owned buffer
}
}
```
## 5. AcBinarySerializer.cs (~130 call site változás)
### Signature változások:
- `WriteInt32<TOutput>(int, TOutput output)` → `WriteInt32<TOutput>(int, BinarySerializationContext<TOutput> context)`
- `WriteString<TOutput>(string, TOutput output, context)` → `WriteString<TOutput>(string, BinarySerializationContext<TOutput> context)`
- Minden helper: `output` param eltávolítása, `context` marad
- `var output = context.Output;` sorok törlése
- `output.WriteByte(...)` → `context.WriteByte(...)`
### TryWritePrimitiveArrayCore:
- Jelenleg non-generic `BinaryOutputBase output` param
- Új: generic `BinarySerializationContext<TOutput> context` param (2 JIT copy elfogadható, per-array hívás)
### Public API metódusok:
```csharp
// Serialize<T> (byte[]):
context.Output.Initialize(out context._buffer, out context._position, out context._bufferEnd);
// ... serialize ...
return context.Output.ToArray(context._buffer, context._position);
// Serialize<T> (IBufferWriter):
output.Initialize(out context._buffer, out context._position, out context._bufferEnd);
// ... serialize ...
output.Flush(context._buffer, context._position);
```
## 6. ScanPass.cs — NINCS VÁLTOZÁS
Már most is csak context-et kap, nem ír semmit.
## 7. IBinaryOutput.cs — TÖRLÉS
Senki nem implementálja többé.
## 8. Implementációs sorrend
1. Context: `_buffer`/`_position`/`_bufferEnd` mezők + 24 write metódus hozzáadása
2. BinaryOutputBase: 28 metódus → 3 (Initialize, Grow, GetTotalPosition)
3. ArrayBinaryOutput: egyszerűsítés (Grow + result metódusok)
4. BufferWriterBinaryOutput: egyszerűsítés (Grow + Flush + AcquireChunk)
5. AcBinarySerializer.cs: ~130 hívás átírása output→context
6. Public API: Initialize/Flush/ToArray hívások buffer/position paraméterekkel
7. Context WriteHeader/WriteInlineMetadata: output param eltávolítása
8. IBinaryOutput.cs törlése
9. Build + teszt
## Várható eredmény
- Hot path: `_buffer[_position++]` — nulla virtual dispatch (baseline szintű teljesítmény)
- Cold path (Grow): 1 virtual call buffer beteltkor → elhanyagolható
- Position: 1 virtual call, de csak 1x hívódik per serialize → elhanyagolható
- IBufferWriter streaming: 100% megmarad (Grow = Advance + GetMemory)

View File

@ -1,15 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
</PropertyGroup>
<Import Project="..//AyCode.Core.targets" />
<ItemGroup>
<ProjectReference Include="..\AyCode.Utils\AyCode.Utils.csproj" />
<ProjectReference Include="..\AyCode.Core.Serializers.SourceGenerator\AyCode.Core.Serializers.SourceGenerator.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
<ProjectReference Include="..\AyCode.Core.Serializers.SourceGenerator\AyCode.Core.Serializers.SourceGenerator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
</ItemGroup>
<ItemGroup>

View File

@ -2,7 +2,10 @@ using System;
using System.Buffers;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using static AyCode.Core.Helpers.JsonUtilities;
@ -10,7 +13,7 @@ namespace AyCode.Core.Serializers.Binaries;
public static partial class AcBinarySerializer
{
private static class BinarySerializationContextPool<TOutput> where TOutput : BinaryOutputBase
private static class BinarySerializationContextPool<TOutput> where TOutput : struct, IBinaryOutputBase
{
private static readonly ConcurrentQueue<BinarySerializationContext<TOutput>> Pool = new();
@ -48,26 +51,46 @@ public static partial class AcBinarySerializer
}
/// <summary>
/// Binary serialization context. Generic on TOutput for JIT devirtualization.
/// TOutput is the binary output target (ArrayBinaryOutput for byte[], BufferWriterBinaryOutput for IBufferWriter).
/// All write operations are delegated to Output — the context only manages serialization state
/// (string interning, reference tracking, metadata, property filtering).
/// Binary serialization context. Generic on TOutput for output strategy selection.
/// Owns _buffer/_position for zero virtual dispatch on the hot path.
/// All write operations (WriteByte, WriteVarUInt, etc.) are inline methods here.
/// TOutput Output handles only cold-path buffer management (Grow/Initialize) and finalization.
/// </summary>
internal sealed class BinarySerializationContext<TOutput>
: SerializationContextBase<BinarySerializeTypeMetadata, AcBinarySerializerOptions>, IDisposable
where TOutput : BinaryOutputBase
where TOutput : struct, IBinaryOutputBase
{
private static readonly Encoding Utf8NoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
private const int PropertyIndexBufferMaxCache = 512;
private const int PropertyStateBufferMaxCache = 512;
/// <summary>
/// Strongly-typed output target for JIT devirtualization in the write pass.
/// Output target — handles only Grow (cold path) and finalization (AsSpan/ToArray/Flush).
/// </summary>
public TOutput Output = default!;
public TOutput Output;
/// <summary>
/// True if Output has been assigned (struct can't be null-checked).
/// </summary>
public bool OutputInitialized;
#region Buffer State owned by context for zero virtual dispatch
/// <summary>Current writable buffer (from ArrayPool or IBufferWriter chunk).</summary>
internal byte[] _buffer = null!;
/// <summary>Current write position within _buffer.</summary>
internal int _position;
/// <summary>One past the last writable index in _buffer. Write must satisfy _position &lt; _bufferEnd.</summary>
internal int _bufferEnd;
#endregion
private IdentityMap<string, InternEntry>? _stringInternMap;
private int _nextCacheIndex; // Next dense cache index to assign (starts at 0, uses ++_nextCacheIndex)
private int _nextFirstIndex; // Next first occurrence index to assign (scan pass)
public int NextFirstIndex; // Next first occurrence index for scan pass. Direct access for performance.
/// <summary>
/// Next cache index reference for scan pass. Direct ref access for TryTrack methods.
@ -78,17 +101,6 @@ public static partial class AcBinarySerializer
get => ref _nextCacheIndex;
}
/// <summary>
/// Next first occurrence index for scan pass. Direct access for performance.
/// </summary>
public int NextFirstIndex
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _nextFirstIndex;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
set => _nextFirstIndex = value;
}
private int[]? _propertyIndexBuffer;
private byte[]? _propertyStateBuffer;
@ -130,12 +142,13 @@ public static partial class AcBinarySerializer
public bool HasPropertyFilter { get; private set; }
/// <summary>
/// Current output position (delegates to Output).
/// Current output position (total bytes written so far).
/// Cold path — uses virtual dispatch through Output.GetTotalPosition.
/// </summary>
public int Position
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => Output.Position;
get => Output.GetTotalPosition(_position);
}
public BinarySerializationContext(AcBinarySerializerOptions options)
@ -160,7 +173,7 @@ public static partial class AcBinarySerializer
{
_stringInternMap?.Reset();
_nextCacheIndex = 0;
_nextFirstIndex = 0;
NextFirstIndex = 0;
if (_propertyIndexBuffer != null && _propertyIndexBuffer.Length > PropertyIndexBufferMaxCache)
{
@ -197,6 +210,341 @@ public static partial class AcBinarySerializer
disposableOutput.Dispose();
}
#region Write Methods inline, zero virtual dispatch
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void EnsureCapacity(int additionalBytes)
{
if (_position + additionalBytes > _bufferEnd)
Output.Grow(ref _buffer, ref _position, ref _bufferEnd, additionalBytes);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteByte(byte value)
{
if (_position >= _bufferEnd)
Output.Grow(ref _buffer, ref _position, ref _bufferEnd, 1);
_buffer[_position++] = value;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteTwoBytes(byte b1, byte b2)
{
EnsureCapacity(2);
_buffer[_position++] = b1;
_buffer[_position++] = b2;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteBytes(ReadOnlySpan<byte> data)
{
EnsureCapacity(data.Length);
data.CopyTo(_buffer.AsSpan(_position));
_position += data.Length;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteRaw<T>(T value) where T : unmanaged
{
var size = Unsafe.SizeOf<T>();
EnsureCapacity(size);
Unsafe.WriteUnaligned(ref _buffer[_position], value);
_position += size;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteTypeCodeAndRaw<T>(byte typeCode, T value) where T : unmanaged
{
var size = 1 + Unsafe.SizeOf<T>();
EnsureCapacity(size);
_buffer[_position++] = typeCode;
Unsafe.WriteUnaligned(ref _buffer[_position], value);
_position += Unsafe.SizeOf<T>();
}
#endregion
#region VarInt Encoding inline
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteVarUInt(uint value)
{
if (value < 0x80)
{
if (_position >= _bufferEnd)
Output.Grow(ref _buffer, ref _position, ref _bufferEnd, 1);
_buffer[_position++] = (byte)value;
return;
}
EnsureCapacity(5);
while (value >= 0x80)
{
_buffer[_position++] = (byte)(value | 0x80);
value >>= 7;
}
_buffer[_position++] = (byte)value;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteVarInt(int value)
{
var encoded = (uint)((value << 1) ^ (value >> 31));
WriteVarUInt(encoded);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteVarULong(ulong value)
{
if (value < 0x80)
{
if (_position >= _bufferEnd)
Output.Grow(ref _buffer, ref _position, ref _bufferEnd, 1);
_buffer[_position++] = (byte)value;
return;
}
EnsureCapacity(10);
while (value >= 0x80)
{
_buffer[_position++] = (byte)(value | 0x80);
value >>= 7;
}
_buffer[_position++] = (byte)value;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteVarLong(long value)
{
var encoded = (ulong)((value << 1) ^ (value >> 63));
WriteVarULong(encoded);
}
#endregion
#region Specialized Types inline
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteDecimalBits(decimal value)
{
EnsureCapacity(16);
Span<int> bits = stackalloc int[4];
decimal.TryGetBits(value, bits, out _);
MemoryMarshal.AsBytes(bits).CopyTo(_buffer.AsSpan(_position, 16));
_position += 16;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteDateTimeBits(DateTime value)
{
EnsureCapacity(9);
Unsafe.WriteUnaligned(ref _buffer[_position], value.Ticks);
_buffer[_position + 8] = (byte)value.Kind;
_position += 9;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteGuidBits(Guid value)
{
EnsureCapacity(16);
value.TryWriteBytes(_buffer.AsSpan(_position, 16));
_position += 16;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteDateTimeOffsetBits(DateTimeOffset value)
{
EnsureCapacity(10);
Unsafe.WriteUnaligned(ref _buffer[_position], value.UtcTicks);
Unsafe.WriteUnaligned(ref _buffer[_position + 8], (short)value.Offset.TotalMinutes);
_position += 10;
}
#endregion
#region String Writes inline
public void WriteStringUtf8(string value)
{
if (Ascii.IsValid(value))
{
WriteVarUInt((uint)value.Length);
EnsureCapacity(value.Length);
Ascii.FromUtf16(value.AsSpan(), _buffer.AsSpan(_position, value.Length), out _);
_position += value.Length;
return;
}
var byteCount = Utf8NoBom.GetByteCount(value);
WriteVarUInt((uint)byteCount);
EnsureCapacity(byteCount);
Utf8NoBom.GetBytes(value.AsSpan(), _buffer.AsSpan(_position, byteCount));
_position += byteCount;
}
public void WriteFixStr(string value)
{
var length = value.Length;
EnsureCapacity(1 + length);
_buffer[_position++] = BinaryTypeCode.EncodeFixStr(length);
Ascii.FromUtf16(value.AsSpan(), _buffer.AsSpan(_position, length), out _);
_position += length;
}
public void WriteFixStrDirect(string value)
{
var length = value.Length;
EnsureCapacity(1 + length);
var destSpan = _buffer.AsSpan(_position + 1, length);
var status = Ascii.FromUtf16(value.AsSpan(), destSpan, out var bytesWritten);
if (status == OperationStatus.Done && bytesWritten == length)
{
_buffer[_position] = BinaryTypeCode.EncodeFixStr(length);
_position += 1 + length;
}
else
{
_buffer[_position++] = BinaryTypeCode.String;
WriteStringUtf8Internal(value);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteFixStrBytes(ReadOnlySpan<byte> utf8Bytes)
{
var length = utf8Bytes.Length;
EnsureCapacity(1 + length);
_buffer[_position++] = BinaryTypeCode.EncodeFixStr(length);
utf8Bytes.CopyTo(_buffer.AsSpan(_position, length));
_position += length;
}
public void WritePreencodedPropertyName(ReadOnlySpan<byte> utf8Name)
{
WriteByte(BinaryTypeCode.String);
WriteVarUInt((uint)utf8Name.Length);
WriteBytes(utf8Name);
}
private void WriteStringUtf8Internal(string value)
{
var byteCount = Utf8NoBom.GetByteCount(value);
WriteVarUInt((uint)byteCount);
EnsureCapacity(byteCount);
Utf8NoBom.GetBytes(value.AsSpan(), _buffer.AsSpan(_position, byteCount));
_position += byteCount;
}
#endregion
#region Bulk Array Writes inline
public void WriteDoubleArrayBulk(double[] array)
{
EnsureCapacity(array.Length * 9);
for (var i = 0; i < array.Length; i++)
{
_buffer[_position++] = BinaryTypeCode.Float64;
Unsafe.WriteUnaligned(ref _buffer[_position], array[i]);
_position += 8;
}
}
public void WriteFloatArrayBulk(float[] array)
{
EnsureCapacity(array.Length * 5);
for (var i = 0; i < array.Length; i++)
{
_buffer[_position++] = BinaryTypeCode.Float32;
Unsafe.WriteUnaligned(ref _buffer[_position], array[i]);
_position += 4;
}
}
public void WriteGuidArrayBulk(Guid[] array)
{
EnsureCapacity(array.Length * 17);
for (var i = 0; i < array.Length; i++)
{
_buffer[_position++] = BinaryTypeCode.Guid;
array[i].TryWriteBytes(_buffer.AsSpan(_position, 16));
_position += 16;
}
}
public void WriteInt32ArrayOptimized(int[] array)
{
for (var i = 0; i < array.Length; i++)
{
var value = array[i];
if (BinaryTypeCode.TryEncodeTinyInt(value, out var tiny))
{
WriteByte(tiny);
}
else
{
WriteByte(BinaryTypeCode.Int32);
WriteVarInt(value);
}
}
}
public void WriteLongArrayOptimized(long[] array)
{
for (var i = 0; i < array.Length; i++)
{
var value = array[i];
if (value >= int.MinValue && value <= int.MaxValue)
{
var intValue = (int)value;
if (BinaryTypeCode.TryEncodeTinyInt(intValue, out var tiny))
{
WriteByte(tiny);
}
else
{
WriteByte(BinaryTypeCode.Int32);
WriteVarInt(intValue);
}
}
else
{
WriteByte(BinaryTypeCode.Int64);
WriteVarLong(value);
}
}
}
public void WriteBytesSimd(ReadOnlySpan<byte> source)
{
EnsureCapacity(source.Length);
var destination = _buffer.AsSpan(_position, source.Length);
if (Vector.IsHardwareAccelerated && source.Length >= Vector<byte>.Count * 2)
{
var vectorSize = Vector<byte>.Count;
var i = 0;
var length = source.Length;
var vectorCount = length / vectorSize;
for (var v = 0; v < vectorCount; v++)
{
var vec = new Vector<byte>(source.Slice(i, vectorSize));
vec.CopyTo(destination.Slice(i, vectorSize));
i += vectorSize;
}
if (i < length)
source.Slice(i).CopyTo(destination.Slice(i));
}
else
{
source.CopyTo(destination);
}
_position += source.Length;
}
#endregion
#region String Interning
/// <summary>
@ -245,7 +593,7 @@ public static partial class AcBinarySerializer
// 1st occurrence: store FirstIndex for validation, CacheIndex = -1 (not cached yet)
ref var newEntry = ref _stringInternMap.GetValueRef(slotIndex);
newEntry.FirstIndex = _nextFirstIndex++;
newEntry.FirstIndex = NextFirstIndex++;
newEntry.CacheIndex = -1;
}
@ -284,17 +632,17 @@ public static partial class AcBinarySerializer
/// Ismételt: [propNameHash (4b)]
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteInlineMetadata(BinarySerializeTypeMetadata metadata, TOutput output, bool isFirstOccurrence)
public void WriteInlineMetadata(BinarySerializeTypeMetadata metadata, bool isFirstOccurrence)
{
output.WriteRaw(metadata.PropNameHash);
WriteRaw(metadata.PropNameHash);
if (isFirstOccurrence)
{
var hashes = metadata.MetadataPropertyHashes;
output.WriteVarUInt((uint)hashes.Length);
WriteVarUInt((uint)hashes.Length);
for (var i = 0; i < hashes.Length; i++)
{
output.WriteRaw(hashes[i]);
WriteRaw(hashes[i]);
}
}
}
@ -370,12 +718,12 @@ public static partial class AcBinarySerializer
if (HasCaching)
flags |= BinaryTypeCode.HeaderFlag_HasCacheCount;
Output.WriteByte(AcBinarySerializerOptions.FormatVersion);
Output.WriteByte(flags);
WriteByte(AcBinarySerializerOptions.FormatVersion);
WriteByte(flags);
if (HasCaching)
{
Output.WriteVarUInt((uint)GetCacheCount());
WriteVarUInt((uint)GetCacheCount());
}
}

View File

@ -16,7 +16,7 @@ public static partial class AcBinarySerializer
/// CacheIndex is assigned immediately on 2nd occurrence (no post-processing needed).
/// </summary>
private static void ScanForDuplicates<TOutput>(object value, Type type, BinarySerializationContext<TOutput> context)
where TOutput : BinaryOutputBase
where TOutput : struct, IBinaryOutputBase
{
if (!context.HasCaching)
return;
@ -26,7 +26,7 @@ public static partial class AcBinarySerializer
}
private static void ScanValue<TOutput>(object? value, TypeMetadataWrapper<BinarySerializeTypeMetadata> wrapper, BinarySerializationContext<TOutput> context, int depth)
where TOutput : BinaryOutputBase
where TOutput : struct, IBinaryOutputBase
{
if (value == null || depth > context.MaxDepth)
return;
@ -131,7 +131,7 @@ public static partial class AcBinarySerializer
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void ScanItem<TOutput>(object item, BinarySerializationContext<TOutput> context, int depth)
where TOutput : BinaryOutputBase
where TOutput : struct, IBinaryOutputBase
{
// String fast path — avoid GetWrapper entirely
if (item is string str)

File diff suppressed because it is too large Load Diff

View File

@ -1,428 +1,111 @@
using System;
using System.Buffers;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
namespace AyCode.Core.Serializers.Binaries;
/// <summary>
/// High-performance binary output backed by a byte[] from ArrayPool.
/// Matches the exact performance characteristics of the original BinarySerializationContext buffer code:
/// direct _buffer[_position++] indexing, Unsafe.WriteUnaligned, SIMD bulk copy.
/// Only handles buffer lifecycle (Initialize, Grow, result extraction).
/// All write methods live in BinarySerializationContext for zero virtual dispatch.
///
/// This is the fastest output path — use when the result is needed as byte[]/Span.
/// </summary>
public sealed class ArrayBinaryOutput : BinaryOutputBase, IDisposable
public struct ArrayBinaryOutput : IBinaryOutputBase, IDisposable
{
private const int MinBufferSize = 256;
private static readonly Encoding Utf8NoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
private byte[] _buffer;
private int _position;
private byte[] _rentedBuffer;
public ArrayBinaryOutput(int initialCapacity = 4096)
{
_buffer = ArrayPool<byte>.Shared.Rent(Math.Max(initialCapacity, MinBufferSize));
_rentedBuffer = ArrayPool<byte>.Shared.Rent(Math.Max(initialCapacity, MinBufferSize));
}
/// <inheritdoc />
public override int Position
{
/// <summary>
/// Provides the initial buffer. Position starts at 0, bufferEnd = buffer.Length.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _position;
}
#region Abstract Overrides Core Primitives
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override void WriteByte(byte value)
public void Initialize(out byte[] buffer, out int position, out int bufferEnd)
{
if (_position >= _buffer.Length)
GrowBuffer(_position + 1);
_buffer[_position++] = value;
}
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override void WriteTwoBytes(byte b1, byte b2)
{
EnsureCapacity(2);
_buffer[_position++] = b1;
_buffer[_position++] = b2;
}
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override void WriteBytes(ReadOnlySpan<byte> data)
{
EnsureCapacity(data.Length);
data.CopyTo(_buffer.AsSpan(_position));
_position += data.Length;
}
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override void WriteRaw<T>(T value)
{
var size = Unsafe.SizeOf<T>();
EnsureCapacity(size);
Unsafe.WriteUnaligned(ref _buffer[_position], value);
_position += size;
}
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override void EnsureCapacity(int additionalBytes)
{
var required = _position + additionalBytes;
if (required <= _buffer.Length)
return;
GrowBuffer(required);
buffer = _rentedBuffer;
position = 0;
bufferEnd = _rentedBuffer.Length;
}
/// <summary>
/// Grows the buffer: rents a bigger one from ArrayPool, copies old data, returns old to pool.
/// Position is unchanged (data was copied to the same offset in the new buffer).
/// </summary>
[MethodImpl(MethodImplOptions.NoInlining)]
private void GrowBuffer(int required)
public void Grow(ref byte[] buffer, ref int position, ref int bufferEnd, int needed)
{
var newSize = Math.Max(_buffer.Length * 2, required);
var required = position + needed;
var newSize = Math.Max(buffer.Length * 2, required);
var newBuffer = ArrayPool<byte>.Shared.Rent(newSize);
_buffer.AsSpan(0, _position).CopyTo(newBuffer);
ArrayPool<byte>.Shared.Return(_buffer);
_buffer = newBuffer;
buffer.AsSpan(0, position).CopyTo(newBuffer);
ArrayPool<byte>.Shared.Return(buffer);
buffer = newBuffer;
bufferEnd = newBuffer.Length;
_rentedBuffer = newBuffer;
}
#endregion
#region Optimized Overrides Specialized Types (direct buffer access)
/// <summary>
/// Optimized: single EnsureCapacity + direct Unsafe.WriteUnaligned + indexer.
/// For ArrayBinaryOutput, position IS the total (single contiguous buffer).
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override void WriteTypeCodeAndRaw<T>(byte typeCode, T value)
{
var size = 1 + Unsafe.SizeOf<T>();
EnsureCapacity(size);
_buffer[_position++] = typeCode;
Unsafe.WriteUnaligned(ref _buffer[_position], value);
_position += Unsafe.SizeOf<T>();
}
public int GetTotalPosition(int currentPosition) => currentPosition;
/// <summary>
/// Optimized: VarUInt with direct _buffer[_position++] access.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override void WriteVarUInt(uint value)
{
if (value < 0x80)
{
if (_position >= _buffer.Length)
GrowBuffer(_position + 1);
_buffer[_position++] = (byte)value;
return;
}
EnsureCapacity(5);
while (value >= 0x80)
{
_buffer[_position++] = (byte)(value | 0x80);
value >>= 7;
}
_buffer[_position++] = (byte)value;
}
/// <summary>
/// Optimized: VarInt with direct buffer access.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override void WriteVarInt(int value)
{
var encoded = (uint)((value << 1) ^ (value >> 31));
WriteVarUInt(encoded);
}
/// <summary>
/// Optimized: VarULong with direct buffer access.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override void WriteVarULong(ulong value)
{
if (value < 0x80)
{
if (_position >= _buffer.Length)
GrowBuffer(_position + 1);
_buffer[_position++] = (byte)value;
return;
}
EnsureCapacity(10);
while (value >= 0x80)
{
_buffer[_position++] = (byte)(value | 0x80);
value >>= 7;
}
_buffer[_position++] = (byte)value;
}
/// <summary>
/// Optimized: VarLong with direct buffer access.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override void WriteVarLong(long value)
{
var encoded = (ulong)((value << 1) ^ (value >> 63));
WriteVarULong(encoded);
}
/// <summary>
/// Optimized: direct Unsafe.WriteUnaligned for decimal bits.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override void WriteDecimalBits(decimal value)
{
EnsureCapacity(16);
Span<int> bits = stackalloc int[4];
decimal.TryGetBits(value, bits, out _);
MemoryMarshal.AsBytes(bits).CopyTo(_buffer.AsSpan(_position, 16));
_position += 16;
}
/// <summary>
/// Optimized: direct Unsafe.WriteUnaligned + indexer for DateTime.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override void WriteDateTimeBits(DateTime value)
{
EnsureCapacity(9);
Unsafe.WriteUnaligned(ref _buffer[_position], value.Ticks);
_buffer[_position + 8] = (byte)value.Kind;
_position += 9;
}
/// <summary>
/// Optimized: direct TryWriteBytes into buffer.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override void WriteGuidBits(Guid value)
{
EnsureCapacity(16);
value.TryWriteBytes(_buffer.AsSpan(_position, 16));
_position += 16;
}
/// <summary>
/// Optimized: direct Unsafe.WriteUnaligned for DateTimeOffset.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override void WriteDateTimeOffsetBits(DateTimeOffset value)
{
EnsureCapacity(10);
Unsafe.WriteUnaligned(ref _buffer[_position], value.UtcTicks);
Unsafe.WriteUnaligned(ref _buffer[_position + 8], (short)value.Offset.TotalMinutes);
_position += 10;
}
/// <summary>
/// Optimized: direct ASCII fast path into _buffer.
/// </summary>
public override void WriteStringUtf8(string value)
{
if (Ascii.IsValid(value))
{
WriteVarUInt((uint)value.Length);
EnsureCapacity(value.Length);
Ascii.FromUtf16(value.AsSpan(), _buffer.AsSpan(_position, value.Length), out _);
_position += value.Length;
return;
}
var byteCount = Utf8NoBom.GetByteCount(value);
WriteVarUInt((uint)byteCount);
EnsureCapacity(byteCount);
Utf8NoBom.GetBytes(value.AsSpan(), _buffer.AsSpan(_position, byteCount));
_position += byteCount;
}
/// <summary>
/// Optimized: FixStr with direct buffer write.
/// </summary>
public override void WriteFixStr(string value)
{
var length = value.Length;
EnsureCapacity(1 + length);
_buffer[_position++] = BinaryTypeCode.EncodeFixStr(length);
Ascii.FromUtf16(value.AsSpan(), _buffer.AsSpan(_position, length), out _);
_position += length;
}
/// <summary>
/// Optimized: FixStrDirect with SIMD try into buffer.
/// </summary>
public override void WriteFixStrDirect(string value)
{
var length = value.Length;
EnsureCapacity(1 + length);
var destSpan = _buffer.AsSpan(_position + 1, length);
var status = Ascii.FromUtf16(value.AsSpan(), destSpan, out var bytesWritten);
if (status == System.Buffers.OperationStatus.Done && bytesWritten == length)
{
_buffer[_position] = BinaryTypeCode.EncodeFixStr(length);
_position += 1 + length;
}
else
{
_buffer[_position++] = BinaryTypeCode.String;
WriteStringUtf8Internal(value);
}
}
/// <summary>
/// Optimized: FixStrBytes with direct buffer copy.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override void WriteFixStrBytes(ReadOnlySpan<byte> utf8Bytes)
{
var length = utf8Bytes.Length;
EnsureCapacity(1 + length);
_buffer[_position++] = BinaryTypeCode.EncodeFixStr(length);
utf8Bytes.CopyTo(_buffer.AsSpan(_position, length));
_position += length;
}
private void WriteStringUtf8Internal(string value)
{
var byteCount = Utf8NoBom.GetByteCount(value);
WriteVarUInt((uint)byteCount);
EnsureCapacity(byteCount);
Utf8NoBom.GetBytes(value.AsSpan(), _buffer.AsSpan(_position, byteCount));
_position += byteCount;
}
#endregion
#region Optimized Overrides Bulk Arrays (direct buffer, batched capacity)
/// <inheritdoc />
public override void WriteDoubleArrayBulk(double[] array)
{
EnsureCapacity(array.Length * 9);
for (var i = 0; i < array.Length; i++)
{
_buffer[_position++] = BinaryTypeCode.Float64;
Unsafe.WriteUnaligned(ref _buffer[_position], array[i]);
_position += 8;
}
}
/// <inheritdoc />
public override void WriteFloatArrayBulk(float[] array)
{
EnsureCapacity(array.Length * 5);
for (var i = 0; i < array.Length; i++)
{
_buffer[_position++] = BinaryTypeCode.Float32;
Unsafe.WriteUnaligned(ref _buffer[_position], array[i]);
_position += 4;
}
}
/// <inheritdoc />
public override void WriteGuidArrayBulk(Guid[] array)
{
EnsureCapacity(array.Length * 17);
for (var i = 0; i < array.Length; i++)
{
_buffer[_position++] = BinaryTypeCode.Guid;
array[i].TryWriteBytes(_buffer.AsSpan(_position, 16));
_position += 16;
}
}
/// <inheritdoc />
public override void WriteBytesSimd(ReadOnlySpan<byte> source)
{
EnsureCapacity(source.Length);
var destination = _buffer.AsSpan(_position, source.Length);
if (Vector.IsHardwareAccelerated && source.Length >= Vector<byte>.Count * 2)
{
var vectorSize = Vector<byte>.Count;
var i = 0;
var length = source.Length;
var vectorCount = length / vectorSize;
for (var v = 0; v < vectorCount; v++)
{
var vec = new Vector<byte>(source.Slice(i, vectorSize));
vec.CopyTo(destination.Slice(i, vectorSize));
i += vectorSize;
}
if (i < length)
source.Slice(i).CopyTo(destination.Slice(i));
}
else
{
source.CopyTo(destination);
}
_position += source.Length;
}
#endregion
#region Output Methods
#region Result Extraction receive buffer/position from context
/// <summary>Returns the written data as a ReadOnlySpan without allocation.</summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ReadOnlySpan<byte> AsSpan() => _buffer.AsSpan(0, _position);
public ReadOnlySpan<byte> AsSpan(byte[] buffer, int position) => buffer.AsSpan(0, position);
/// <summary>Copies the written data to a new exactly-sized array.</summary>
public byte[] ToArray()
public byte[] ToArray(byte[] buffer, int position)
{
var result = GC.AllocateUninitializedArray<byte>(_position);
_buffer.AsSpan(0, _position).CopyTo(result);
var result = GC.AllocateUninitializedArray<byte>(position);
buffer.AsSpan(0, position).CopyTo(result);
return result;
}
/// <summary>Copies the written data to an IBufferWriter (single memcpy).</summary>
public void WriteTo(IBufferWriter<byte> writer)
public void WriteTo(IBufferWriter<byte> writer, byte[] buffer, int position)
{
var span = writer.GetSpan(_position);
_buffer.AsSpan(0, _position).CopyTo(span);
writer.Advance(_position);
var span = writer.GetSpan(position);
buffer.AsSpan(0, position).CopyTo(span);
writer.Advance(position);
}
/// <summary>Resets position for reuse without deallocation.</summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Reset() => _position = 0;
/// <summary>
/// Detaches the internal buffer as a BinarySerializationResult and allocates a fresh buffer.
/// The caller owns the returned result and must dispose it to return the buffer to the pool.
/// </summary>
public AcBinarySerializer.BinarySerializationResult DetachResult()
public AcBinarySerializer.BinarySerializationResult DetachResult(byte[] buffer, int position)
{
var resultBuffer = _buffer;
var resultLength = _position;
var resultBuffer = buffer;
var resultLength = position;
_buffer = ArrayPool<byte>.Shared.Rent(Math.Max(resultBuffer.Length / 2, MinBufferSize));
_position = 0;
_rentedBuffer = ArrayPool<byte>.Shared.Rent(Math.Max(resultBuffer.Length / 2, MinBufferSize));
return new AcBinarySerializer.BinarySerializationResult(resultBuffer, resultLength, pooled: true);
}
/// <summary>Resets for reuse — nothing to do, context handles position via Initialize.</summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Reset() { }
#endregion
#region IDisposable
public void Dispose()
{
if (_buffer != null)
if (_rentedBuffer != null)
{
ArrayPool<byte>.Shared.Return(_buffer);
_buffer = null!;
ArrayPool<byte>.Shared.Return(_rentedBuffer);
_rentedBuffer = null!;
}
}

View File

@ -9,12 +9,12 @@ namespace AyCode.Core.Serializers.Binaries;
/// 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
internal sealed class ArrayPooledBufferWriter : IBufferWriter<byte>, IDisposable
{
private byte[] _buffer;
private int _written;
public PooledBufferWriter(int initialCapacity)
public ArrayPooledBufferWriter(int initialCapacity)
{
_buffer = ArrayPool<byte>.Shared.Rent(Math.Max(initialCapacity, 256));
}

View File

@ -1,249 +1,42 @@
using System;
using System.Buffers;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
namespace AyCode.Core.Serializers.Binaries;
/// <summary>
/// Abstract base class for binary output implementations.
/// Provides common serialization logic (VarInt, strings, specialized types, bulk arrays)
/// built on top of a small set of abstract core primitives that derived classes implement.
/// Provides only the buffer management strategy (grow/flush) — all write methods live in
/// BinarySerializationContext which owns the _buffer/_position state for zero virtual dispatch.
///
/// Derived classes only need to implement the core buffer operations:
/// WriteByte, WriteTwoBytes, WriteBytes, WriteRaw, EnsureCapacity, Position.
///
/// All higher-level methods are virtual — derived classes can override any method
/// for backing-specific optimizations (e.g. batched GetSpan, direct buffer indexing).
/// Derived classes implement:
/// - Initialize: provide the initial buffer and position
/// - Grow: handle buffer exhaustion (ArrayPool.Rent+copy or IBufferWriter.Advance+GetMemory)
/// - GetTotalPosition: compute total bytes written (for Position property, cold path)
/// </summary>
public abstract class BinaryOutputBase : IBinaryOutput
public interface IBinaryOutputBase
{
private static readonly Encoding Utf8NoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
#region Abstract Core Primitives (derived must implement)
/// <inheritdoc />
public abstract void WriteByte(byte value);
/// <inheritdoc />
public abstract void WriteTwoBytes(byte b1, byte b2);
/// <inheritdoc />
public abstract void WriteBytes(ReadOnlySpan<byte> data);
/// <inheritdoc />
public abstract void WriteRaw<T>(T value) where T : unmanaged;
/// <summary>
/// Provides the initial buffer, starting position, and buffer end boundary.
/// Called once before serialization begins.
/// For ArrayBinaryOutput: Rent from ArrayPool, position = 0, bufferEnd = buffer.Length.
/// For BufferWriterBinaryOutput: GetMemory + TryGetArray, position = segment.Offset, bufferEnd = segment.Offset + segment.Count.
/// </summary>
public void Initialize(out byte[] buffer, out int position, out int bufferEnd);
/// <summary>
/// Ensure the backing storage can accept at least <paramref name="additionalBytes"/> more bytes
/// without reallocation. Called by higher-level methods to batch capacity checks.
/// Called when the context's buffer cannot hold 'needed' more bytes. Cold path only.
/// Must provide a new or grown buffer via the ref parameters.
/// For ArrayBinaryOutput: ArrayPool.Rent bigger buffer, copy old data, return old, position unchanged.
/// For BufferWriterBinaryOutput: Advance current chunk, GetMemory new chunk, update buffer/position/bufferEnd.
/// </summary>
protected abstract void EnsureCapacity(int additionalBytes);
[MethodImpl(MethodImplOptions.NoInlining)]
public void Grow(ref byte[] buffer, ref int position, ref int bufferEnd, int needed);
/// <inheritdoc />
public abstract int Position { get; }
#endregion
#region Abstract WriteTypeCodeAndRaw
/// <inheritdoc />
public abstract void WriteTypeCodeAndRaw<T>(byte typeCode, T value) where T : unmanaged;
#endregion
#region Abstract VarInt Encoding
/// <inheritdoc />
public abstract void WriteVarInt(int value);
/// <inheritdoc />
public abstract void WriteVarUInt(uint value);
/// <inheritdoc />
public abstract void WriteVarLong(long value);
/// <inheritdoc />
public abstract void WriteVarULong(ulong value);
#endregion
#region Virtual Specialized Types
/// <inheritdoc />
public virtual void WriteDecimalBits(decimal value)
{
Span<int> bits = stackalloc int[4];
decimal.TryGetBits(value, bits, out _);
WriteBytes(MemoryMarshal.AsBytes(bits));
}
/// <inheritdoc />
public abstract void WriteDateTimeBits(DateTime value);
/// <inheritdoc />
public virtual void WriteGuidBits(Guid value)
{
Span<byte> buf = stackalloc byte[16];
value.TryWriteBytes(buf);
WriteBytes(buf);
}
/// <inheritdoc />
public abstract void WriteDateTimeOffsetBits(DateTimeOffset value);
#endregion
#region Virtual String Writes
/// <inheritdoc />
public virtual void WriteStringUtf8(string value)
{
if (Ascii.IsValid(value))
{
WriteVarUInt((uint)value.Length);
Span<byte> buf = value.Length <= 256
? stackalloc byte[value.Length]
: new byte[value.Length];
Ascii.FromUtf16(value.AsSpan(), buf, out _);
WriteBytes(buf);
return;
}
var byteCount = Utf8NoBom.GetByteCount(value);
WriteVarUInt((uint)byteCount);
Span<byte> utf8Buf = byteCount <= 256
? stackalloc byte[byteCount]
: new byte[byteCount];
Utf8NoBom.GetBytes(value.AsSpan(), utf8Buf);
WriteBytes(utf8Buf);
}
/// <inheritdoc />
public virtual void WriteFixStr(string value)
{
var length = value.Length;
WriteByte(BinaryTypeCode.EncodeFixStr(length));
Span<byte> buf = length <= 256
? stackalloc byte[length]
: new byte[length];
Ascii.FromUtf16(value.AsSpan(), buf, out _);
WriteBytes(buf);
}
/// <inheritdoc />
public virtual void WriteFixStrDirect(string value)
{
var length = value.Length;
Span<byte> buf = length <= 256
? stackalloc byte[length]
: new byte[length];
var status = Ascii.FromUtf16(value.AsSpan(), buf, out var bytesWritten);
if (status == OperationStatus.Done && bytesWritten == length)
{
WriteByte(BinaryTypeCode.EncodeFixStr(length));
WriteBytes(buf.Slice(0, length));
}
else
{
WriteByte(BinaryTypeCode.String);
WriteStringUtf8Internal(value);
}
}
/// <inheritdoc />
public virtual void WriteFixStrBytes(ReadOnlySpan<byte> utf8Bytes)
{
WriteByte(BinaryTypeCode.EncodeFixStr(utf8Bytes.Length));
WriteBytes(utf8Bytes);
}
/// <inheritdoc />
public virtual void WritePreencodedPropertyName(ReadOnlySpan<byte> utf8Name)
{
WriteByte(BinaryTypeCode.String);
WriteVarUInt((uint)utf8Name.Length);
WriteBytes(utf8Name);
}
private void WriteStringUtf8Internal(string value)
{
var byteCount = Utf8NoBom.GetByteCount(value);
WriteVarUInt((uint)byteCount);
Span<byte> buf = byteCount <= 256
? stackalloc byte[byteCount]
: new byte[byteCount];
Utf8NoBom.GetBytes(value.AsSpan(), buf);
WriteBytes(buf);
}
#endregion
#region Bulk Array Writes
/// <inheritdoc />
public abstract void WriteDoubleArrayBulk(double[] array);
/// <inheritdoc />
public abstract void WriteFloatArrayBulk(float[] array);
/// <inheritdoc />
public abstract void WriteGuidArrayBulk(Guid[] array);
/// <inheritdoc />
public virtual void WriteInt32ArrayOptimized(int[] array)
{
for (var i = 0; i < array.Length; i++)
{
var value = array[i];
if (BinaryTypeCode.TryEncodeTinyInt(value, out var tiny))
{
WriteByte(tiny);
}
else
{
WriteByte(BinaryTypeCode.Int32);
WriteVarInt(value);
}
}
}
/// <inheritdoc />
public virtual void WriteLongArrayOptimized(long[] array)
{
for (var i = 0; i < array.Length; i++)
{
var value = array[i];
if (value >= int.MinValue && value <= int.MaxValue)
{
var intValue = (int)value;
if (BinaryTypeCode.TryEncodeTinyInt(intValue, out var tiny))
{
WriteByte(tiny);
}
else
{
WriteByte(BinaryTypeCode.Int32);
WriteVarInt(intValue);
}
}
else
{
WriteByte(BinaryTypeCode.Int64);
WriteVarLong(value);
}
}
}
/// <inheritdoc />
public virtual void WriteBytesSimd(ReadOnlySpan<byte> source)
{
WriteBytes(source);
}
#endregion
/// <summary>
/// Returns total bytes written so far, given the current position within the active buffer.
/// Cold path — called only when Position property is accessed (typically once per serialization).
/// For ArrayBinaryOutput: returns currentPosition directly (single contiguous buffer).
/// For BufferWriterBinaryOutput: returns committedBytes + (currentPosition - chunkStart).
/// </summary>
public int GetTotalPosition(int currentPosition);
}

View File

@ -2,348 +2,239 @@ using System;
using System.Buffers;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
namespace AyCode.Core.Serializers.Binaries;
/// <summary>
/// Binary output that writes directly to an IBufferWriter (e.g. SignalR pipe, network stream).
/// Uses a cached chunk pattern: acquires a large chunk once via GetMemory, extracts the backing
/// array, and writes into it with direct indexing (zero interface calls per write).
/// Only calls Advance + GetMemory when the chunk fills up.
///
/// Call <see cref="Flush"/> after all writes to commit any pending bytes to the underlying writer.
/// Two usage modes:
/// 1. Context mode: Initialize/Grow/Flush — all write methods live in BinarySerializationContext.
/// 2. Standalone mode: direct write methods (WriteByte, WriteVarUInt, etc.) for use outside
/// the serialization pipeline (e.g. AcBinaryHubProtocol frame headers).
///
/// Uses a cached chunk pattern: acquires a large chunk once via GetMemory, extracts the backing
/// array, and writes into it with direct indexing. Only calls Advance + GetMemory when the chunk fills up.
///
/// Call <see cref="Flush()"/> after all standalone writes to commit any pending bytes.
/// </summary>
public sealed class BufferWriterBinaryOutput : BinaryOutputBase
public struct BufferWriterBinaryOutput : IBinaryOutputBase
{
private static readonly Encoding Utf8NoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
private const int MinChunkRequest = 256;
private readonly IBufferWriter<byte> _writer;
private int _written;
private int _committedBytes; // total bytes Advanced to writer so far
private int _currentChunkStart; // _position value at start of current chunk
private bool _ownedBuffer; // true if current buffer is from ArrayPool (fallback path)
// Cached chunk state — avoids GetSpan/Advance per write
private byte[] _chunkArray; // backing array (from GetMemory or ArrayPool fallback)
private int _chunkOffset; // start offset within _chunkArray
private int _chunkPos; // bytes written into current chunk
private int _chunkLength; // usable length of current chunk
private bool _ownedBuffer; // true if _chunkArray is from ArrayPool (fallback path)
// Standalone mode buffer state (used by direct write methods)
private byte[] _buffer = null!;
private int _position;
private int _bufferEnd;
public BufferWriterBinaryOutput(IBufferWriter<byte> writer)
{
_writer = writer;
_chunkArray = null!;
RentChunk(MinChunkRequest);
// Initialize standalone buffer for direct write usage
_committedBytes = 0;
AcquireChunk(MinChunkRequest, out _buffer, out _position, out _bufferEnd);
_currentChunkStart = _position;
}
/// <inheritdoc />
public override int Position
{
/// <summary>
/// Provides the initial buffer from the IBufferWriter.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _written;
public void Initialize(out byte[] buffer, out int position, out int bufferEnd)
{
_committedBytes = 0;
AcquireChunk(MinChunkRequest, out buffer, out position, out bufferEnd);
_currentChunkStart = position;
}
/// <summary>
/// Called when the context's buffer is full. Commits current chunk to the IBufferWriter
/// and acquires a new chunk.
/// </summary>
[MethodImpl(MethodImplOptions.NoInlining)]
public void Grow(ref byte[] buffer, ref int position, ref int bufferEnd, int needed)
{
// Commit bytes written in current chunk
var bytesInChunk = position - _currentChunkStart;
if (bytesInChunk > 0)
{
if (_ownedBuffer)
{
FlushOwnedBuffer(buffer, bytesInChunk);
}
else
{
_writer.Advance(bytesInChunk);
}
_committedBytes += bytesInChunk;
}
// Acquire new chunk
AcquireChunk(Math.Max(needed, MinChunkRequest), out buffer, out position, out bufferEnd);
_currentChunkStart = position;
}
/// <summary>
/// Returns total bytes written: committed + pending in current chunk.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int GetTotalPosition(int currentPosition)
=> _committedBytes + (currentPosition - _currentChunkStart);
/// <summary>
/// Commits any pending bytes to the underlying IBufferWriter.
/// Must be called after all writes are complete.
/// Takes the context's current buffer state.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Flush()
public void Flush(byte[] buffer, int position)
{
if (_chunkPos > 0)
var bytesInChunk = position - _currentChunkStart;
if (bytesInChunk > 0)
{
if (_ownedBuffer)
FlushOwnedBuffer();
{
FlushOwnedBuffer(buffer, bytesInChunk);
}
else
_writer.Advance(_chunkPos);
_chunkPos = 0;
_chunkLength = 0;
{
_writer.Advance(bytesInChunk);
}
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
private void FlushOwnedBuffer()
private void FlushOwnedBuffer(byte[] buffer, int bytesInChunk)
{
// Copy from our owned array to the writer, then return to pool
var span = _writer.GetSpan(_chunkPos);
_chunkArray.AsSpan(_chunkOffset, _chunkPos).CopyTo(span);
_writer.Advance(_chunkPos);
ArrayPool<byte>.Shared.Return(_chunkArray);
var span = _writer.GetSpan(bytesInChunk);
buffer.AsSpan(_currentChunkStart, bytesInChunk).CopyTo(span);
_writer.Advance(bytesInChunk);
ArrayPool<byte>.Shared.Return(buffer);
_ownedBuffer = false;
}
[MethodImpl(MethodImplOptions.NoInlining)]
private void RentChunk(int minSize)
private void AcquireChunk(int requestSize, out byte[] buffer, out int position, out int bufferEnd)
{
// Commit whatever we wrote so far
if (_chunkPos > 0)
{
if (_ownedBuffer)
FlushOwnedBuffer();
else
_writer.Advance(_chunkPos);
}
// Use GetMemory so we can extract the backing array via TryGetArray
var requestSize = Math.Max(minSize, MinChunkRequest);
var memory = _writer.GetMemory(requestSize);
var actualRequest = Math.Max(requestSize, MinChunkRequest);
var memory = _writer.GetMemory(actualRequest);
if (MemoryMarshal.TryGetArray(memory, out ArraySegment<byte> segment) && segment.Array != null)
{
_chunkArray = segment.Array;
_chunkOffset = segment.Offset;
_chunkLength = segment.Count;
buffer = segment.Array;
position = segment.Offset;
bufferEnd = segment.Offset + segment.Count;
_ownedBuffer = false;
}
else
{
// Fallback for non-array-backed IBufferWriter (native memory).
// Rent our own buffer; FlushOwnedBuffer copies to writer on next RentChunk/Flush.
_chunkArray = ArrayPool<byte>.Shared.Rent(requestSize);
_chunkOffset = 0;
_chunkLength = _chunkArray.Length;
// Rent our own buffer; FlushOwnedBuffer copies to writer on next Grow/Flush.
var owned = ArrayPool<byte>.Shared.Rent(actualRequest);
buffer = owned;
position = 0;
bufferEnd = owned.Length;
_ownedBuffer = true;
}
_chunkPos = 0;
}
#region Abstract Overrides Core Primitives
#region Standalone Write Methods for direct usage outside serialization pipeline (e.g. AcBinaryHubProtocol)
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override void WriteByte(byte value)
/// <summary>
/// Total bytes written in standalone mode (committed + pending in current chunk).
/// </summary>
public int Position
{
if (_chunkPos >= _chunkLength)
RentChunk(1);
_chunkArray[_chunkOffset + _chunkPos++] = value;
_written++;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _committedBytes + (_position - _currentChunkStart);
}
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override void WriteTwoBytes(byte b1, byte b2)
private void StandaloneEnsureCapacity(int additionalBytes)
{
if (_chunkPos + 2 > _chunkLength)
RentChunk(2);
var off = _chunkOffset + _chunkPos;
_chunkArray[off] = b1;
_chunkArray[off + 1] = b2;
_chunkPos += 2;
_written += 2;
if (_position + additionalBytes > _bufferEnd)
Grow(ref _buffer, ref _position, ref _bufferEnd, additionalBytes);
}
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override void WriteBytes(ReadOnlySpan<byte> data)
public void WriteByte(byte value)
{
if (_chunkPos + data.Length > _chunkLength)
RentChunk(data.Length);
data.CopyTo(_chunkArray.AsSpan(_chunkOffset + _chunkPos, data.Length));
_chunkPos += data.Length;
_written += data.Length;
if (_position >= _bufferEnd)
Grow(ref _buffer, ref _position, ref _bufferEnd, 1);
_buffer[_position++] = value;
}
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override void WriteRaw<T>(T value)
public void WriteBytes(ReadOnlySpan<byte> data)
{
StandaloneEnsureCapacity(data.Length);
data.CopyTo(_buffer.AsSpan(_position));
_position += data.Length;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteRaw<T>(T value) where T : unmanaged
{
var size = Unsafe.SizeOf<T>();
if (_chunkPos + size > _chunkLength)
RentChunk(size);
Unsafe.WriteUnaligned(ref _chunkArray[_chunkOffset + _chunkPos], value);
_chunkPos += size;
_written += size;
StandaloneEnsureCapacity(size);
Unsafe.WriteUnaligned(ref _buffer[_position], value);
_position += size;
}
/// <summary>
/// Ensures the cached chunk has room for at least <paramref name="additionalBytes"/>.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override void EnsureCapacity(int additionalBytes)
{
if (_chunkPos + additionalBytes > _chunkLength)
RentChunk(additionalBytes);
}
#endregion
#region Optimized Overrides Batched Writes
/// <summary>
/// Optimized: single capacity check for type code + value, direct buffer write.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override void WriteTypeCodeAndRaw<T>(byte typeCode, T value)
{
var size = 1 + Unsafe.SizeOf<T>();
if (_chunkPos + size > _chunkLength)
RentChunk(size);
var off = _chunkOffset + _chunkPos;
_chunkArray[off] = typeCode;
Unsafe.WriteUnaligned(ref _chunkArray[off + 1], value);
_chunkPos += size;
_written += size;
}
/// <summary>
/// Optimized: VarUInt with direct cached buffer access.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override void WriteVarUInt(uint value)
public void WriteVarUInt(uint value)
{
if (value < 0x80)
{
if (_chunkPos >= _chunkLength)
RentChunk(1);
_chunkArray[_chunkOffset + _chunkPos++] = (byte)value;
_written++;
if (_position >= _bufferEnd)
Grow(ref _buffer, ref _position, ref _bufferEnd, 1);
_buffer[_position++] = (byte)value;
return;
}
StandaloneEnsureCapacity(5);
while (value >= 0x80)
{
_buffer[_position++] = (byte)(value | 0x80);
value >>= 7;
}
_buffer[_position++] = (byte)value;
}
public void WriteStringUtf8(string value)
{
if (Ascii.IsValid(value))
{
WriteVarUInt((uint)value.Length);
StandaloneEnsureCapacity(value.Length);
Ascii.FromUtf16(value.AsSpan(), _buffer.AsSpan(_position, value.Length), out _);
_position += value.Length;
return;
}
if (_chunkPos + 5 > _chunkLength)
RentChunk(5);
var off = _chunkOffset + _chunkPos;
while (value >= 0x80)
{
_chunkArray[off++] = (byte)(value | 0x80);
value >>= 7;
}
_chunkArray[off++] = (byte)value;
var bytesWritten = off - _chunkOffset - _chunkPos;
_chunkPos += bytesWritten;
_written += bytesWritten;
var byteCount = Utf8NoBom.GetByteCount(value);
WriteVarUInt((uint)byteCount);
StandaloneEnsureCapacity(byteCount);
Utf8NoBom.GetBytes(value.AsSpan(), _buffer.AsSpan(_position, byteCount));
_position += byteCount;
}
/// <summary>
/// Optimized: ZigZag + batched VarUInt.
/// Commits any pending bytes in standalone mode to the underlying IBufferWriter.
/// Call after all standalone writes are complete.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override void WriteVarInt(int value)
public void Flush()
{
var encoded = (uint)((value << 1) ^ (value >> 31));
WriteVarUInt(encoded);
}
/// <summary>
/// Optimized: VarULong with direct cached buffer access.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override void WriteVarULong(ulong value)
{
if (value < 0x80)
{
if (_chunkPos >= _chunkLength)
RentChunk(1);
_chunkArray[_chunkOffset + _chunkPos++] = (byte)value;
_written++;
return;
}
if (_chunkPos + 10 > _chunkLength)
RentChunk(10);
var off = _chunkOffset + _chunkPos;
while (value >= 0x80)
{
_chunkArray[off++] = (byte)(value | 0x80);
value >>= 7;
}
_chunkArray[off++] = (byte)value;
var bytesWritten = off - _chunkOffset - _chunkPos;
_chunkPos += bytesWritten;
_written += bytesWritten;
}
/// <summary>
/// Optimized: ZigZag + batched VarULong.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override void WriteVarLong(long value)
{
var encoded = (ulong)((value << 1) ^ (value >> 63));
WriteVarULong(encoded);
}
/// <summary>
/// Optimized: single capacity check for DateTime (9 bytes), direct buffer write.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override void WriteDateTimeBits(DateTime value)
{
if (_chunkPos + 9 > _chunkLength)
RentChunk(9);
var off = _chunkOffset + _chunkPos;
Unsafe.WriteUnaligned(ref _chunkArray[off], value.Ticks);
_chunkArray[off + 8] = (byte)value.Kind;
_chunkPos += 9;
_written += 9;
}
/// <summary>
/// Optimized: single capacity check for DateTimeOffset (10 bytes), direct buffer write.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override void WriteDateTimeOffsetBits(DateTimeOffset value)
{
if (_chunkPos + 10 > _chunkLength)
RentChunk(10);
var off = _chunkOffset + _chunkPos;
Unsafe.WriteUnaligned(ref _chunkArray[off], value.UtcTicks);
Unsafe.WriteUnaligned(ref _chunkArray[off + 8], (short)value.Offset.TotalMinutes);
_chunkPos += 10;
_written += 10;
}
#endregion
#region Optimized Overrides Bulk Arrays (single capacity check + tight loop)
/// <inheritdoc />
public override void WriteDoubleArrayBulk(double[] array)
{
var totalSize = array.Length * 9;
if (_chunkPos + totalSize > _chunkLength)
RentChunk(totalSize);
var off = _chunkOffset + _chunkPos;
for (var i = 0; i < array.Length; i++)
{
_chunkArray[off++] = BinaryTypeCode.Float64;
Unsafe.WriteUnaligned(ref _chunkArray[off], array[i]);
off += 8;
}
_chunkPos += totalSize;
_written += totalSize;
}
/// <inheritdoc />
public override void WriteFloatArrayBulk(float[] array)
{
var totalSize = array.Length * 5;
if (_chunkPos + totalSize > _chunkLength)
RentChunk(totalSize);
var off = _chunkOffset + _chunkPos;
for (var i = 0; i < array.Length; i++)
{
_chunkArray[off++] = BinaryTypeCode.Float32;
Unsafe.WriteUnaligned(ref _chunkArray[off], array[i]);
off += 4;
}
_chunkPos += totalSize;
_written += totalSize;
}
/// <inheritdoc />
public override void WriteGuidArrayBulk(Guid[] array)
{
var totalSize = array.Length * 17;
if (_chunkPos + totalSize > _chunkLength)
RentChunk(totalSize);
var off = _chunkOffset + _chunkPos;
for (var i = 0; i < array.Length; i++)
{
_chunkArray[off++] = BinaryTypeCode.Guid;
array[i].TryWriteBytes(_chunkArray.AsSpan(off, 16));
off += 16;
}
_chunkPos += totalSize;
_written += totalSize;
Flush(_buffer, _position);
}
#endregion

View File

@ -1,111 +0,0 @@
using System;
using System.Runtime.CompilerServices;
namespace AyCode.Core.Serializers.Binaries;
/// <summary>
/// Abstraction for binary serialization output.
/// Implementations can write to byte[] (ArrayBinaryOutput) or IBufferWriter (BufferWriterBinaryOutput).
/// Custom implementations can be provided for specialized output targets.
/// </summary>
public interface IBinaryOutput
{
#region Core Writes
/// <summary>Write a single byte.</summary>
void WriteByte(byte value);
/// <summary>Write two bytes efficiently.</summary>
void WriteTwoBytes(byte b1, byte b2);
/// <summary>Write a span of bytes.</summary>
void WriteBytes(ReadOnlySpan<byte> data);
/// <summary>Write an unmanaged value directly (no encoding).</summary>
void WriteRaw<T>(T value) where T : unmanaged;
/// <summary>Write a type code byte followed by an unmanaged value. Batches capacity check.</summary>
void WriteTypeCodeAndRaw<T>(byte typeCode, T value) where T : unmanaged;
#endregion
#region VarInt Encoding
/// <summary>Write a ZigZag-encoded variable-length int32.</summary>
void WriteVarInt(int value);
/// <summary>Write a variable-length uint32.</summary>
void WriteVarUInt(uint value);
/// <summary>Write a ZigZag-encoded variable-length int64.</summary>
void WriteVarLong(long value);
/// <summary>Write a variable-length uint64.</summary>
void WriteVarULong(ulong value);
#endregion
#region Specialized Types
/// <summary>Write decimal as 16 raw bytes (4 x int32 bits).</summary>
void WriteDecimalBits(decimal value);
/// <summary>Write DateTime as 8 bytes ticks + 1 byte kind.</summary>
void WriteDateTimeBits(DateTime value);
/// <summary>Write Guid as 16 raw bytes.</summary>
void WriteGuidBits(Guid value);
/// <summary>Write DateTimeOffset as 8 bytes UTC ticks + 2 bytes offset minutes.</summary>
void WriteDateTimeOffsetBits(DateTimeOffset value);
#endregion
#region String Writes
/// <summary>Write UTF8 string with VarUInt length prefix. Fast path for ASCII.</summary>
void WriteStringUtf8(string value);
/// <summary>Write short ASCII string using FixStr encoding (type+length in single byte).</summary>
void WriteFixStr(string value);
/// <summary>Write FixStr with SIMD ASCII try, falls back to standard UTF8.</summary>
void WriteFixStrDirect(string value);
/// <summary>Write pre-encoded UTF8 bytes using FixStr encoding.</summary>
void WriteFixStrBytes(ReadOnlySpan<byte> utf8Bytes);
/// <summary>Write pre-encoded property name with String type code + VarUInt length + bytes.</summary>
void WritePreencodedPropertyName(ReadOnlySpan<byte> utf8Name);
#endregion
#region Bulk Array Writes
/// <summary>Write double[] with per-element type codes. Override for optimized bulk write.</summary>
void WriteDoubleArrayBulk(double[] array);
/// <summary>Write float[] with per-element type codes. Override for optimized bulk write.</summary>
void WriteFloatArrayBulk(float[] array);
/// <summary>Write Guid[] with per-element type codes. Override for optimized bulk write.</summary>
void WriteGuidArrayBulk(Guid[] array);
/// <summary>Write int[] with TinyInt optimization per element.</summary>
void WriteInt32ArrayOptimized(int[] array);
/// <summary>Write long[] with TinyInt/Int32 downcast optimization per element.</summary>
void WriteLongArrayOptimized(long[] array);
/// <summary>Write bytes using SIMD when available, standard copy otherwise.</summary>
void WriteBytesSimd(ReadOnlySpan<byte> source);
#endregion
#region Position
/// <summary>Current write position (total bytes written so far).</summary>
int Position { get; }
#endregion
}