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:
parent
270f1b8265
commit
991e8f6038
|
|
@ -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\")"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 < _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());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => _position;
|
||||
}
|
||||
|
||||
#region Abstract Overrides — Core Primitives
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>
|
||||
/// Provides the initial buffer. Position starts at 0, bufferEnd = buffer.Length.
|
||||
/// </summary>
|
||||
[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!;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
public void Initialize(out byte[] buffer, out int position, out int bufferEnd)
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => _written;
|
||||
_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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
Loading…
Reference in New Issue