diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 2fd71d1..f906261 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -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\")" ] } } diff --git a/.plan b/.plan new file mode 100644 index 0000000..b01ce8f --- /dev/null +++ b/.plan @@ -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`-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 — ú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) +4. WriteRaw(T) where T : unmanaged +5. WriteTypeCodeAndRaw(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) +18. WritePreencodedPropertyName(ReadOnlySpan) +19. WriteDoubleArrayBulk(double[]) +20. WriteFloatArrayBulk(float[]) +21. WriteGuidArrayBulk(Guid[]) +22. WriteInt32ArrayOptimized(int[]) +23. WriteLongArrayOptimized(long[]) +24. WriteBytesSimd(ReadOnlySpan) + +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 AsSpan(byte[] buffer, int position); + public byte[] ToArray(byte[] buffer, int position); + public BinarySerializationResult DetachResult(byte[] buffer, int position); + public void WriteTo(IBufferWriter writer, byte[] buffer, int position); +} +``` + +## 4. BufferWriterBinaryOutput (~350 → ~100 sor) + +```csharp +public sealed class BufferWriterBinaryOutput : BinaryOutputBase +{ + private readonly IBufferWriter _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(int, TOutput output)` → `WriteInt32(int, BinarySerializationContext context)` +- `WriteString(string, TOutput output, context)` → `WriteString(string, BinarySerializationContext 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 context` param (2 JIT copy elfogadható, per-array hívás) + +### Public API metódusok: +```csharp +// Serialize (byte[]): +context.Output.Initialize(out context._buffer, out context._position, out context._bufferEnd); +// ... serialize ... +return context.Output.ToArray(context._buffer, context._position); + +// Serialize (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) diff --git a/AyCode.Core/AyCode.Core.csproj b/AyCode.Core/AyCode.Core.csproj index 8cbe66a..3a6e6dc 100644 --- a/AyCode.Core/AyCode.Core.csproj +++ b/AyCode.Core/AyCode.Core.csproj @@ -1,15 +1,14 @@  + net10.0 - + diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs index 3a5cccd..dba7277 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs @@ -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 where TOutput : BinaryOutputBase + private static class BinarySerializationContextPool where TOutput : struct, IBinaryOutputBase { private static readonly ConcurrentQueue> Pool = new(); @@ -48,26 +51,46 @@ public static partial class AcBinarySerializer } /// - /// 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. /// internal sealed class BinarySerializationContext : SerializationContextBase, 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; /// - /// Strongly-typed output target for JIT devirtualization in the write pass. + /// Output target — handles only Grow (cold path) and finalization (AsSpan/ToArray/Flush). /// - public TOutput Output = default!; + public TOutput Output; + + /// + /// True if Output has been assigned (struct can't be null-checked). + /// + public bool OutputInitialized; + + #region Buffer State — owned by context for zero virtual dispatch + + /// Current writable buffer (from ArrayPool or IBufferWriter chunk). + internal byte[] _buffer = null!; + + /// Current write position within _buffer. + internal int _position; + + /// One past the last writable index in _buffer. Write must satisfy _position < _bufferEnd. + internal int _bufferEnd; + + #endregion private IdentityMap? _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. /// /// 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; } - /// - /// Next first occurrence index for scan pass. Direct access for performance. - /// - 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; } /// - /// Current output position (delegates to Output). + /// Current output position (total bytes written so far). + /// Cold path — uses virtual dispatch through Output.GetTotalPosition. /// 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 data) + { + EnsureCapacity(data.Length); + data.CopyTo(_buffer.AsSpan(_position)); + _position += data.Length; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteRaw(T value) where T : unmanaged + { + var size = Unsafe.SizeOf(); + EnsureCapacity(size); + Unsafe.WriteUnaligned(ref _buffer[_position], value); + _position += size; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteTypeCodeAndRaw(byte typeCode, T value) where T : unmanaged + { + var size = 1 + Unsafe.SizeOf(); + EnsureCapacity(size); + _buffer[_position++] = typeCode; + Unsafe.WriteUnaligned(ref _buffer[_position], value); + _position += Unsafe.SizeOf(); + } + + #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 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 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 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 source) + { + EnsureCapacity(source.Length); + var destination = _buffer.AsSpan(_position, source.Length); + + if (Vector.IsHardwareAccelerated && source.Length >= Vector.Count * 2) + { + var vectorSize = Vector.Count; + var i = 0; + var length = source.Length; + var vectorCount = length / vectorSize; + for (var v = 0; v < vectorCount; v++) + { + var vec = new Vector(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 /// @@ -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)] /// [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()); } } diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.ScanPass.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.ScanPass.cs index c2ff1ab..b12b69c 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.ScanPass.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.ScanPass.cs @@ -16,7 +16,7 @@ public static partial class AcBinarySerializer /// CacheIndex is assigned immediately on 2nd occurrence (no post-processing needed). /// private static void ScanForDuplicates(object value, Type type, BinarySerializationContext 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(object? value, TypeMetadataWrapper wrapper, BinarySerializationContext 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 /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void ScanItem(object item, BinarySerializationContext context, int depth) - where TOutput : BinaryOutputBase + where TOutput : struct, IBinaryOutputBase { // String fast path — avoid GetWrapper entirely if (item is string str) diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs index 8b5c485..8665456 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs @@ -25,7 +25,8 @@ namespace AyCode.Core.Serializers.Binaries; /// - Optimized buffer management with ArrayPool /// - Zero-allocation hot paths using Span and MemoryMarshal /// - Automatic Expression to AcExpressionNode conversion -/// - Generic TOutput for JIT devirtualization (ArrayBinaryOutput / BufferWriterBinaryOutput) +/// - Generic TOutput for output strategy selection (ArrayBinaryOutput / BufferWriterBinaryOutput) +/// - Buffer-in-context: _buffer/_position owned by context for zero virtual dispatch on hot path /// public static partial class AcBinarySerializer { @@ -76,6 +77,8 @@ public static partial class AcBinarySerializer // Create context without pooling (we need to set up callback) using var context = new BinarySerializationContext(analysisOptions); context.Output = new ArrayBinaryOutput(); + context.OutputInitialized = true; + context.Output.Initialize(out context._buffer, out context._position, out context._bufferEnd); // Set up tracking callbacks context.OnStringInterned = (propertyPath, stringValue) => @@ -255,10 +258,12 @@ public static partial class AcBinarySerializer } var context = BinarySerializationContextPool.Get(options); - if (context.Output == null) + if (!context.OutputInitialized) + { context.Output = new ArrayBinaryOutput(options.InitialBufferCapacity); - else - context.Output.Reset(); + context.OutputInitialized = true; + } + context.Output.Initialize(out context._buffer, out context._position, out context._bufferEnd); try { @@ -269,11 +274,11 @@ public static partial class AcBinarySerializer // Apply compression if enabled - compress directly from buffer span (1 allocation) if (options.UseCompression != Lz4CompressionMode.None) { - return Lz4.Compress(context.Output.AsSpan(), options.UseCompression); + return Lz4.Compress(context.Output.AsSpan(context._buffer, context._position), options.UseCompression); } // No compression - single allocation for result - return context.Output.ToArray(); + return context.Output.ToArray(context._buffer, context._position); } finally { @@ -313,9 +318,9 @@ public static partial class AcBinarySerializer runtimeType = typeof(AcExpressionNode); } - var output = new BufferWriterBinaryOutput(writer); var context = BinarySerializationContextPool.Get(options); - context.Output = output; + context.Output = new BufferWriterBinaryOutput(writer); + context.Output.Initialize(out context._buffer, out context._position, out context._bufferEnd); try { @@ -328,7 +333,7 @@ public static partial class AcBinarySerializer { // For compression with BufferWriter, we need to flush first then compress // This path is less common — compression typically uses byte[] path - output.Flush(); + context.Output.Flush(context._buffer, context._position); // Compression with IBufferWriter requires intermediate buffer // Fall back to ArrayBinaryOutput path for compression throw new NotSupportedException( @@ -336,11 +341,11 @@ public static partial class AcBinarySerializer "Use the byte[] overload or disable compression."); } - output.Flush(); + context.Output.Flush(context._buffer, context._position); } finally { - context.Output = null!; + context.Output = default; if (options.UseAsync) BinarySerializationContextPool.ReturnAsync(context); else BinarySerializationContextPool.Return(context); } @@ -357,10 +362,12 @@ public static partial class AcBinarySerializer var runtimeType = value.GetType(); var context = BinarySerializationContextPool.Get(options); - if (context.Output == null) + if (!context.OutputInitialized) + { context.Output = new ArrayBinaryOutput(options.InitialBufferCapacity); - else - context.Output.Reset(); + context.OutputInitialized = true; + } + context.Output.Initialize(out context._buffer, out context._position, out context._bufferEnd); try { @@ -391,10 +398,12 @@ public static partial class AcBinarySerializer var runtimeType = value.GetType(); var context = BinarySerializationContextPool.Get(options); - if (context.Output == null) + if (!context.OutputInitialized) + { context.Output = new ArrayBinaryOutput(options.InitialBufferCapacity); - else - context.Output.Reset(); + context.OutputInitialized = true; + } + context.Output.Initialize(out context._buffer, out context._position, out context._bufferEnd); try { @@ -405,11 +414,11 @@ public static partial class AcBinarySerializer // If compression enabled, compress directly from buffer span (1 allocation) if (options.UseCompression != Lz4CompressionMode.None) { - var compressed = Lz4.Compress(context.Output.AsSpan(), options.UseCompression); + var compressed = Lz4.Compress(context.Output.AsSpan(context._buffer, context._position), options.UseCompression); return BinarySerializationResult.FromImmutable(compressed); } - return context.Output.DetachResult(); + return context.Output.DetachResult(context._buffer, context._position); } finally { @@ -423,20 +432,19 @@ public static partial class AcBinarySerializer #region Value Writing private static void WriteValue(object? value, Type type, BinarySerializationContext context, int depth) - where TOutput : BinaryOutputBase + where TOutput : struct, IBinaryOutputBase { - var output = context.Output; if (value == null) { - output.WriteByte(BinaryTypeCode.Null); + context.WriteByte(BinaryTypeCode.Null); return; } // Try writing as primitive first - if (TryWritePrimitive(value, type, output, context)) + if (TryWritePrimitive(value, type, context)) return; - WriteValueNonPrimitive(value, type, output, context, depth); + WriteValueNonPrimitive(value, type, context, depth); } /// @@ -444,27 +452,27 @@ public static partial class AcBinarySerializer /// Skips null check and TryWritePrimitive — caller guarantees value is non-null and not a primitive type. /// Called from WritePropertyOrSkip default case (PropertyAccessorType.Object) and WriteValue fallback. /// - private static void WriteValueNonPrimitive(object value, Type type, TOutput output, BinarySerializationContext context, int depth) - where TOutput : BinaryOutputBase + private static void WriteValueNonPrimitive(object value, Type type, BinarySerializationContext context, int depth) + where TOutput : struct, IBinaryOutputBase { // Nullable where T is a value type: boxed value may be a primitive. // Only Nullable can be a value type in the Object accessor path. if (type.IsValueType) { - if (TryWritePrimitive(value, value.GetType(), output, context)) + if (TryWritePrimitive(value, value.GetType(), context)) return; } if (depth > context.MaxDepth) { - output.WriteByte(BinaryTypeCode.Null); + context.WriteByte(BinaryTypeCode.Null); return; } // Handle byte arrays specially (value-like, no reference tracking) if (value is byte[] byteArray) { - WriteByteArray(byteArray, output); + WriteByteArray(byteArray, context); return; } @@ -494,8 +502,8 @@ public static partial class AcBinarySerializer /// Avoids Nullable.GetUnderlyingType in hot path by using cached type info. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool TryWritePrimitive(object value, Type type, TOutput output, BinarySerializationContext context) - where TOutput : BinaryOutputBase + private static bool TryWritePrimitive(object value, Type type, BinarySerializationContext context) + where TOutput : struct, IBinaryOutputBase { // Fast path: check TypeCode first (handles most primitives) var typeCode = Type.GetTypeCode(type); @@ -503,73 +511,73 @@ public static partial class AcBinarySerializer switch (typeCode) { case TypeCode.Int32: - WriteInt32((int)value, output); + WriteInt32((int)value, context); return true; case TypeCode.Int64: - WriteInt64((long)value, output); + WriteInt64((long)value, context); return true; case TypeCode.Boolean: - output.WriteByte((bool)value ? BinaryTypeCode.True : BinaryTypeCode.False); + context.WriteByte((bool)value ? BinaryTypeCode.True : BinaryTypeCode.False); return true; case TypeCode.Double: - WriteFloat64Unsafe((double)value, output); + WriteFloat64Unsafe((double)value, context); return true; case TypeCode.String: - WriteString((string)value, output, context); + WriteString((string)value, context); return true; case TypeCode.Single: - WriteFloat32Unsafe((float)value, output); + WriteFloat32Unsafe((float)value, context); return true; case TypeCode.Decimal: - WriteDecimalUnsafe((decimal)value, output); + WriteDecimalUnsafe((decimal)value, context); return true; case TypeCode.DateTime: - WriteDateTimeUnsafe((DateTime)value, output); + WriteDateTimeUnsafe((DateTime)value, context); return true; case TypeCode.Byte: - output.WriteByte(BinaryTypeCode.UInt8); - output.WriteByte((byte)value); + context.WriteByte(BinaryTypeCode.UInt8); + context.WriteByte((byte)value); return true; case TypeCode.Int16: - WriteInt16Unsafe((short)value, output); + WriteInt16Unsafe((short)value, context); return true; case TypeCode.UInt16: - WriteUInt16Unsafe((ushort)value, output); + WriteUInt16Unsafe((ushort)value, context); return true; case TypeCode.UInt32: - WriteUInt32((uint)value, output); + WriteUInt32((uint)value, context); return true; case TypeCode.UInt64: - WriteUInt64((ulong)value, output); + WriteUInt64((ulong)value, context); return true; case TypeCode.SByte: - output.WriteByte(BinaryTypeCode.Int8); - output.WriteByte(unchecked((byte)(sbyte)value)); + context.WriteByte(BinaryTypeCode.Int8); + context.WriteByte(unchecked((byte)(sbyte)value)); return true; case TypeCode.Char: - WriteCharUnsafe((char)value, output); + WriteCharUnsafe((char)value, context); return true; } // Handle special types by reference comparison (faster than type equality) if (ReferenceEquals(type, GuidType)) { - WriteGuidUnsafe((Guid)value, output); + WriteGuidUnsafe((Guid)value, context); return true; } if (ReferenceEquals(type, DateTimeOffsetType)) { - WriteDateTimeOffsetUnsafe((DateTimeOffset)value, output); + WriteDateTimeOffsetUnsafe((DateTimeOffset)value, context); return true; } if (ReferenceEquals(type, TimeSpanType)) { - WriteTimeSpanUnsafe((TimeSpan)value, output); + WriteTimeSpanUnsafe((TimeSpan)value, context); return true; } if (type.IsEnum) { - WriteEnum(value, output); + WriteEnum(value, context); return true; } @@ -579,7 +587,7 @@ public static partial class AcBinarySerializer { // When boxed, nullable value types are unwrapped to their underlying type // So we can just call TryWritePrimitive with the actual runtime type - return TryWritePrimitive(value, value.GetType(), output, context); + return TryWritePrimitive(value, value.GetType(), context); } return false; @@ -590,158 +598,158 @@ public static partial class AcBinarySerializer #region Optimized Primitive Writers using MemoryMarshal [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void WriteInt32(int value, TOutput output) - where TOutput : BinaryOutputBase + private static void WriteInt32(int value, BinarySerializationContext context) + where TOutput : struct, IBinaryOutputBase { if (BinaryTypeCode.TryEncodeTinyInt(value, out var tiny)) { - output.WriteByte(tiny); + context.WriteByte(tiny); return; } - output.WriteByte(BinaryTypeCode.Int32); - output.WriteVarInt(value); + context.WriteByte(BinaryTypeCode.Int32); + context.WriteVarInt(value); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void WriteInt64(long value, TOutput output) - where TOutput : BinaryOutputBase + private static void WriteInt64(long value, BinarySerializationContext context) + where TOutput : struct, IBinaryOutputBase { if (value >= int.MinValue && value <= int.MaxValue) { - WriteInt32((int)value, output); + WriteInt32((int)value, context); return; } - output.WriteByte(BinaryTypeCode.Int64); - output.WriteVarLong(value); + context.WriteByte(BinaryTypeCode.Int64); + context.WriteVarLong(value); } /// /// Optimized float64 writer using batched write. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void WriteFloat64Unsafe(double value, TOutput output) - where TOutput : BinaryOutputBase + private static void WriteFloat64Unsafe(double value, BinarySerializationContext context) + where TOutput : struct, IBinaryOutputBase { - output.WriteTypeCodeAndRaw(BinaryTypeCode.Float64, value); + context.WriteTypeCodeAndRaw(BinaryTypeCode.Float64, value); } /// /// Optimized float32 writer using batched write. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void WriteFloat32Unsafe(float value, TOutput output) - where TOutput : BinaryOutputBase + private static void WriteFloat32Unsafe(float value, BinarySerializationContext context) + where TOutput : struct, IBinaryOutputBase { - output.WriteTypeCodeAndRaw(BinaryTypeCode.Float32, value); + context.WriteTypeCodeAndRaw(BinaryTypeCode.Float32, value); } /// /// Optimized decimal writer using direct memory copy of bits. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void WriteDecimalUnsafe(decimal value, TOutput output) - where TOutput : BinaryOutputBase + private static void WriteDecimalUnsafe(decimal value, BinarySerializationContext context) + where TOutput : struct, IBinaryOutputBase { - output.WriteByte(BinaryTypeCode.Decimal); - output.WriteDecimalBits(value); + context.WriteByte(BinaryTypeCode.Decimal); + context.WriteDecimalBits(value); } /// /// Optimized DateTime writer. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void WriteDateTimeUnsafe(DateTime value, TOutput output) - where TOutput : BinaryOutputBase + private static void WriteDateTimeUnsafe(DateTime value, BinarySerializationContext context) + where TOutput : struct, IBinaryOutputBase { - output.WriteByte(BinaryTypeCode.DateTime); - output.WriteDateTimeBits(value); + context.WriteByte(BinaryTypeCode.DateTime); + context.WriteDateTimeBits(value); } /// /// Optimized Guid writer using direct memory copy. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void WriteGuidUnsafe(Guid value, TOutput output) - where TOutput : BinaryOutputBase + private static void WriteGuidUnsafe(Guid value, BinarySerializationContext context) + where TOutput : struct, IBinaryOutputBase { - output.WriteByte(BinaryTypeCode.Guid); - output.WriteGuidBits(value); + context.WriteByte(BinaryTypeCode.Guid); + context.WriteGuidBits(value); } /// /// Optimized DateTimeOffset writer. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void WriteDateTimeOffsetUnsafe(DateTimeOffset value, TOutput output) - where TOutput : BinaryOutputBase + private static void WriteDateTimeOffsetUnsafe(DateTimeOffset value, BinarySerializationContext context) + where TOutput : struct, IBinaryOutputBase { - output.WriteByte(BinaryTypeCode.DateTimeOffset); - output.WriteDateTimeOffsetBits(value); + context.WriteByte(BinaryTypeCode.DateTimeOffset); + context.WriteDateTimeOffsetBits(value); } /// /// Optimized TimeSpan writer. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void WriteTimeSpanUnsafe(TimeSpan value, TOutput output) - where TOutput : BinaryOutputBase + private static void WriteTimeSpanUnsafe(TimeSpan value, BinarySerializationContext context) + where TOutput : struct, IBinaryOutputBase { - output.WriteTypeCodeAndRaw(BinaryTypeCode.TimeSpan, value.Ticks); + context.WriteTypeCodeAndRaw(BinaryTypeCode.TimeSpan, value.Ticks); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void WriteInt16Unsafe(short value, TOutput output) - where TOutput : BinaryOutputBase + private static void WriteInt16Unsafe(short value, BinarySerializationContext context) + where TOutput : struct, IBinaryOutputBase { - output.WriteTypeCodeAndRaw(BinaryTypeCode.Int16, value); + context.WriteTypeCodeAndRaw(BinaryTypeCode.Int16, value); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void WriteUInt16Unsafe(ushort value, TOutput output) - where TOutput : BinaryOutputBase + private static void WriteUInt16Unsafe(ushort value, BinarySerializationContext context) + where TOutput : struct, IBinaryOutputBase { - output.WriteTypeCodeAndRaw(BinaryTypeCode.UInt16, value); + context.WriteTypeCodeAndRaw(BinaryTypeCode.UInt16, value); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void WriteUInt32(uint value, TOutput output) - where TOutput : BinaryOutputBase + private static void WriteUInt32(uint value, BinarySerializationContext context) + where TOutput : struct, IBinaryOutputBase { - output.WriteByte(BinaryTypeCode.UInt32); - output.WriteVarUInt(value); + context.WriteByte(BinaryTypeCode.UInt32); + context.WriteVarUInt(value); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void WriteUInt64(ulong value, TOutput output) - where TOutput : BinaryOutputBase + private static void WriteUInt64(ulong value, BinarySerializationContext context) + where TOutput : struct, IBinaryOutputBase { - output.WriteByte(BinaryTypeCode.UInt64); - output.WriteVarULong(value); + context.WriteByte(BinaryTypeCode.UInt64); + context.WriteVarULong(value); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void WriteCharUnsafe(char value, TOutput output) - where TOutput : BinaryOutputBase + private static void WriteCharUnsafe(char value, BinarySerializationContext context) + where TOutput : struct, IBinaryOutputBase { - output.WriteByte(BinaryTypeCode.Char); - output.WriteRaw(value); + context.WriteByte(BinaryTypeCode.Char); + context.WriteRaw(value); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void WriteEnum(object value, TOutput output) - where TOutput : BinaryOutputBase + private static void WriteEnum(object value, BinarySerializationContext context) + where TOutput : struct, IBinaryOutputBase { // Use direct unboxing instead of Convert.ToInt32 to avoid NumberFormatInfo overhead var intValue = GetEnumAsInt32Fast(value); if (BinaryTypeCode.TryEncodeTinyInt(intValue, out var tiny)) { - output.WriteByte(BinaryTypeCode.Enum); - output.WriteByte(tiny); + context.WriteByte(BinaryTypeCode.Enum); + context.WriteByte(tiny); return; } - output.WriteByte(BinaryTypeCode.Enum); - output.WriteByte(BinaryTypeCode.Int32); - output.WriteVarInt(intValue); + context.WriteByte(BinaryTypeCode.Enum); + context.WriteByte(BinaryTypeCode.Int32); + context.WriteVarInt(intValue); } /// @@ -776,12 +784,12 @@ public static partial class AcBinarySerializer /// Marker-based interning: write String marker, rewrite to StringInternFirst at end if needed. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void WriteString(string value, TOutput output, BinarySerializationContext context) - where TOutput : BinaryOutputBase + private static void WriteString(string value, BinarySerializationContext context) + where TOutput : struct, IBinaryOutputBase { if (value.Length == 0) { - output.WriteByte(BinaryTypeCode.StringEmpty); + context.WriteByte(BinaryTypeCode.StringEmpty); return; } @@ -802,15 +810,15 @@ public static partial class AcBinarySerializer { // 1st serialize occurrence of a cached string - write StringInternFirst + cacheIndex + data interEntry.IsFirstWrite = false; - output.WriteByte(BinaryTypeCode.StringInternFirst); - output.WriteVarUInt((uint)interEntry.CacheIndex); - output.WriteStringUtf8(value); + context.WriteByte(BinaryTypeCode.StringInternFirst); + context.WriteVarUInt((uint)interEntry.CacheIndex); + context.WriteStringUtf8(value); } else { // 2+ serialize occurrence: write index reference - output.WriteByte(BinaryTypeCode.StringInterned); - output.WriteVarUInt((uint)interEntry.CacheIndex); + context.WriteByte(BinaryTypeCode.StringInterned); + context.WriteVarUInt((uint)interEntry.CacheIndex); } return; } @@ -820,8 +828,8 @@ public static partial class AcBinarySerializer context.OnStringInterned?.Invoke(context.CurrentPropertyPath, value); #endif // String not cached (single occurrence or not found) - write plain String - output.WriteByte(BinaryTypeCode.String); - output.WriteStringUtf8(value); + context.WriteByte(BinaryTypeCode.String); + context.WriteStringUtf8(value); return; } @@ -831,22 +839,22 @@ public static partial class AcBinarySerializer if (length <= BinaryTypeCode.FixStrMaxLength) { // For short strings, use direct ASCII copy (avoids double validation) - output.WriteFixStrDirect(value); + context.WriteFixStrDirect(value); return; } // Long strings - standard encoding - output.WriteByte(BinaryTypeCode.String); - output.WriteStringUtf8(value); + context.WriteByte(BinaryTypeCode.String); + context.WriteStringUtf8(value); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void WriteByteArray(byte[] value, TOutput output) - where TOutput : BinaryOutputBase + private static void WriteByteArray(byte[] value, BinarySerializationContext context) + where TOutput : struct, IBinaryOutputBase { - output.WriteByte(BinaryTypeCode.ByteArray); - output.WriteVarUInt((uint)value.Length); - output.WriteBytes(value); + context.WriteByte(BinaryTypeCode.ByteArray); + context.WriteVarUInt((uint)value.Length); + context.WriteBytes(value); } #endregion @@ -854,9 +862,8 @@ public static partial class AcBinarySerializer #region Complex Type Writers private static void WriteObject(object value, TypeMetadataWrapper wrapper, BinarySerializationContext context, int depth, bool isNested = false) - where TOutput : BinaryOutputBase + where TOutput : struct, IBinaryOutputBase { - var output = context.Output; var metadata = wrapper.Metadata; // Wire format: @@ -897,8 +904,8 @@ public static partial class AcBinarySerializer else { // 2+ occurrence → write ObjectRef - output.WriteByte(BinaryTypeCode.ObjectRef); - output.WriteVarUInt((uint)entry.CacheIndex); + context.WriteByte(BinaryTypeCode.ObjectRef); + context.WriteVarUInt((uint)entry.CacheIndex); return; } } @@ -921,8 +928,8 @@ public static partial class AcBinarySerializer } else { - output.WriteByte(BinaryTypeCode.ObjectRef); - output.WriteVarUInt((uint)entry.CacheIndex); + context.WriteByte(BinaryTypeCode.ObjectRef); + context.WriteVarUInt((uint)entry.CacheIndex); return; } } @@ -945,8 +952,8 @@ public static partial class AcBinarySerializer } else { - output.WriteByte(BinaryTypeCode.ObjectRef); - output.WriteVarUInt((uint)entry.CacheIndex); + context.WriteByte(BinaryTypeCode.ObjectRef); + context.WriteVarUInt((uint)entry.CacheIndex); return; } } @@ -963,25 +970,25 @@ public static partial class AcBinarySerializer { if (cachedObjectCacheIndex >= 0) { - output.WriteByte(BinaryTypeCode.ObjectWithMetadataRefFirst); - output.WriteVarUInt((uint)cachedObjectCacheIndex); + context.WriteByte(BinaryTypeCode.ObjectWithMetadataRefFirst); + context.WriteVarUInt((uint)cachedObjectCacheIndex); } else { - output.WriteByte(BinaryTypeCode.ObjectWithMetadata); + context.WriteByte(BinaryTypeCode.ObjectWithMetadata); } - context.WriteInlineMetadata(wrapper.Metadata, output, isFirstMetadataOccurrence); + context.WriteInlineMetadata(wrapper.Metadata, isFirstMetadataOccurrence); } else { if (cachedObjectCacheIndex >= 0) { - output.WriteByte(BinaryTypeCode.ObjectRefFirst); - output.WriteVarUInt((uint)cachedObjectCacheIndex); + context.WriteByte(BinaryTypeCode.ObjectRefFirst); + context.WriteVarUInt((uint)cachedObjectCacheIndex); } else { - output.WriteByte(BinaryTypeCode.Object); + context.WriteByte(BinaryTypeCode.Object); } } @@ -1002,15 +1009,15 @@ public static partial class AcBinarySerializer if (prop.ExpectedTypeCode.HasValue) { - WritePropertyMarkerless(value, prop, output); + WritePropertyMarkerless(value, prop, context); } else if (hasPropertyFilter && !context.ShouldSerializeProperty(value, prop)) { - output.WriteByte(BinaryTypeCode.PropertySkip); + context.WriteByte(BinaryTypeCode.PropertySkip); } else { - WritePropertyOrSkip(value, prop, output, context, nextDepth); + WritePropertyOrSkip(value, prop, context, nextDepth); } } } @@ -1023,11 +1030,11 @@ public static partial class AcBinarySerializer if (hasPropertyFilter && !context.ShouldSerializeProperty(value, prop)) { - output.WriteByte(BinaryTypeCode.PropertySkip); + context.WriteByte(BinaryTypeCode.PropertySkip); continue; } - WritePropertyOrSkip(value, prop, output, context, nextDepth); + WritePropertyOrSkip(value, prop, context, nextDepth); } } } @@ -1082,63 +1089,63 @@ public static partial class AcBinarySerializer /// Writes a property value using typed getters to avoid boxing. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void WritePropertyValue(object obj, BinaryPropertyAccessor prop, TOutput output, BinarySerializationContext context, int depth) - where TOutput : BinaryOutputBase + private static void WritePropertyValue(object obj, BinaryPropertyAccessor prop, BinarySerializationContext context, int depth) + where TOutput : struct, IBinaryOutputBase { switch (prop.AccessorType) { case PropertyAccessorType.Int32: - WriteInt32(prop.GetInt32(obj), output); + WriteInt32(prop.GetInt32(obj), context); return; case PropertyAccessorType.Int64: - WriteInt64(prop.GetInt64(obj), output); + WriteInt64(prop.GetInt64(obj), context); return; case PropertyAccessorType.Boolean: - output.WriteByte(prop.GetBoolean(obj) ? BinaryTypeCode.True : BinaryTypeCode.False); + context.WriteByte(prop.GetBoolean(obj) ? BinaryTypeCode.True : BinaryTypeCode.False); return; case PropertyAccessorType.Double: - WriteFloat64Unsafe(prop.GetDouble(obj), output); + WriteFloat64Unsafe(prop.GetDouble(obj), context); return; case PropertyAccessorType.Single: - WriteFloat32Unsafe(prop.GetSingle(obj), output); + WriteFloat32Unsafe(prop.GetSingle(obj), context); return; case PropertyAccessorType.Decimal: - WriteDecimalUnsafe(prop.GetDecimal(obj), output); + WriteDecimalUnsafe(prop.GetDecimal(obj), context); return; case PropertyAccessorType.DateTime: - WriteDateTimeUnsafe(prop.GetDateTime(obj), output); + WriteDateTimeUnsafe(prop.GetDateTime(obj), context); return; case PropertyAccessorType.Byte: - output.WriteByte(BinaryTypeCode.UInt8); - output.WriteByte(prop.GetByte(obj)); + context.WriteByte(BinaryTypeCode.UInt8); + context.WriteByte(prop.GetByte(obj)); return; case PropertyAccessorType.Int16: - WriteInt16Unsafe(prop.GetInt16(obj), output); + WriteInt16Unsafe(prop.GetInt16(obj), context); return; case PropertyAccessorType.UInt16: - WriteUInt16Unsafe(prop.GetUInt16(obj), output); + WriteUInt16Unsafe(prop.GetUInt16(obj), context); return; case PropertyAccessorType.UInt32: - WriteUInt32(prop.GetUInt32(obj), output); + WriteUInt32(prop.GetUInt32(obj), context); return; case PropertyAccessorType.UInt64: - WriteUInt64(prop.GetUInt64(obj), output); + WriteUInt64(prop.GetUInt64(obj), context); return; case PropertyAccessorType.Guid: - WriteGuidUnsafe(prop.GetGuid(obj), output); + WriteGuidUnsafe(prop.GetGuid(obj), context); return; case PropertyAccessorType.Enum: var enumValue = prop.GetEnumAsInt32(obj); if (BinaryTypeCode.TryEncodeTinyInt(enumValue, out var tiny)) { - output.WriteByte(BinaryTypeCode.Enum); - output.WriteByte(tiny); + context.WriteByte(BinaryTypeCode.Enum); + context.WriteByte(tiny); } else { - output.WriteByte(BinaryTypeCode.Enum); - output.WriteByte(BinaryTypeCode.Int32); - output.WriteVarInt(enumValue); + context.WriteByte(BinaryTypeCode.Enum); + context.WriteByte(BinaryTypeCode.Int32); + context.WriteVarInt(enumValue); } return; case PropertyAccessorType.String: @@ -1146,9 +1153,9 @@ public static partial class AcBinarySerializer // Fast path: typed getter, no boxing, no Type.GetTypeCode() call var strValue = prop.GetString(obj); if (strValue != null) - WriteString(strValue, output, context); + WriteString(strValue, context); else - output.WriteByte(BinaryTypeCode.Null); + context.WriteByte(BinaryTypeCode.Null); return; } default: @@ -1165,82 +1172,81 @@ public static partial class AcBinarySerializer /// Avoids double getter calls. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void WritePropertyOrSkip(object obj, BinaryPropertyAccessor prop, TOutput output, BinarySerializationContext context, int depth) - where TOutput : BinaryOutputBase + private static void WritePropertyOrSkip(object obj, BinaryPropertyAccessor prop, BinarySerializationContext context, int depth) + where TOutput : struct, IBinaryOutputBase { - //var output = context.Output; switch (prop.AccessorType) { case PropertyAccessorType.Int32: { int value = prop.GetInt32(obj); if (value == 0) - output.WriteByte(BinaryTypeCode.PropertySkip); + context.WriteByte(BinaryTypeCode.PropertySkip); else - WriteInt32(value, output); + WriteInt32(value, context); return; } case PropertyAccessorType.Int64: { long value = prop.GetInt64(obj); if (value == 0L) - output.WriteByte(BinaryTypeCode.PropertySkip); + context.WriteByte(BinaryTypeCode.PropertySkip); else - WriteInt64(value, output); + WriteInt64(value, context); return; } case PropertyAccessorType.Boolean: { bool value = prop.GetBoolean(obj); if (!value) - output.WriteByte(BinaryTypeCode.PropertySkip); + context.WriteByte(BinaryTypeCode.PropertySkip); else - output.WriteByte(BinaryTypeCode.True); + context.WriteByte(BinaryTypeCode.True); return; } case PropertyAccessorType.Double: { double value = prop.GetDouble(obj); if (value == 0.0) - output.WriteByte(BinaryTypeCode.PropertySkip); + context.WriteByte(BinaryTypeCode.PropertySkip); else - WriteFloat64Unsafe(value, output); + WriteFloat64Unsafe(value, context); return; } case PropertyAccessorType.Single: { float value = prop.GetSingle(obj); if (value == 0f) - output.WriteByte(BinaryTypeCode.PropertySkip); + context.WriteByte(BinaryTypeCode.PropertySkip); else - WriteFloat32Unsafe(value, output); + WriteFloat32Unsafe(value, context); return; } case PropertyAccessorType.Decimal: { decimal value = prop.GetDecimal(obj); if (value == 0m) - output.WriteByte(BinaryTypeCode.PropertySkip); + context.WriteByte(BinaryTypeCode.PropertySkip); else - WriteDecimalUnsafe(value, output); + WriteDecimalUnsafe(value, context); return; } case PropertyAccessorType.DateTime: { DateTime value = prop.GetDateTime(obj); // DateTime always written (no default skip) - WriteDateTimeUnsafe(value, output); + WriteDateTimeUnsafe(value, context); return; } case PropertyAccessorType.Byte: { byte value = prop.GetByte(obj); if (value == 0) - output.WriteByte(BinaryTypeCode.PropertySkip); + context.WriteByte(BinaryTypeCode.PropertySkip); else { - output.WriteByte(BinaryTypeCode.UInt8); - output.WriteByte(value); + context.WriteByte(BinaryTypeCode.UInt8); + context.WriteByte(value); } return; } @@ -1248,45 +1254,45 @@ public static partial class AcBinarySerializer { short value = prop.GetInt16(obj); if (value == 0) - output.WriteByte(BinaryTypeCode.PropertySkip); + context.WriteByte(BinaryTypeCode.PropertySkip); else - WriteInt16Unsafe(value, output); + WriteInt16Unsafe(value, context); return; } case PropertyAccessorType.UInt16: { ushort value = prop.GetUInt16(obj); if (value == 0) - output.WriteByte(BinaryTypeCode.PropertySkip); + context.WriteByte(BinaryTypeCode.PropertySkip); else - WriteUInt16Unsafe(value, output); + WriteUInt16Unsafe(value, context); return; } case PropertyAccessorType.UInt32: { uint value = prop.GetUInt32(obj); if (value == 0) - output.WriteByte(BinaryTypeCode.PropertySkip); + context.WriteByte(BinaryTypeCode.PropertySkip); else - WriteUInt32(value, output); + WriteUInt32(value, context); return; } case PropertyAccessorType.UInt64: { ulong value = prop.GetUInt64(obj); if (value == 0) - output.WriteByte(BinaryTypeCode.PropertySkip); + context.WriteByte(BinaryTypeCode.PropertySkip); else - WriteUInt64(value, output); + WriteUInt64(value, context); return; } case PropertyAccessorType.Guid: { Guid value = prop.GetGuid(obj); if (value == Guid.Empty) - output.WriteByte(BinaryTypeCode.PropertySkip); + context.WriteByte(BinaryTypeCode.PropertySkip); else - WriteGuidUnsafe(value, output); + WriteGuidUnsafe(value, context); return; } case PropertyAccessorType.Enum: @@ -1294,18 +1300,18 @@ public static partial class AcBinarySerializer int enumValue = prop.GetEnumAsInt32(obj); if (enumValue == 0) { - output.WriteByte(BinaryTypeCode.PropertySkip); + context.WriteByte(BinaryTypeCode.PropertySkip); } else if (BinaryTypeCode.TryEncodeTinyInt(enumValue, out var tiny)) { - output.WriteByte(BinaryTypeCode.Enum); - output.WriteByte(tiny); + context.WriteByte(BinaryTypeCode.Enum); + context.WriteByte(tiny); } else { - output.WriteByte(BinaryTypeCode.Enum); - output.WriteByte(BinaryTypeCode.Int32); - output.WriteVarInt(enumValue); + context.WriteByte(BinaryTypeCode.Enum); + context.WriteByte(BinaryTypeCode.Int32); + context.WriteVarInt(enumValue); } return; } @@ -1315,14 +1321,14 @@ public static partial class AcBinarySerializer string? value = prop.GetString(obj); if (string.IsNullOrEmpty(value)) { - output.WriteByte(value == null ? BinaryTypeCode.PropertySkip : BinaryTypeCode.StringEmpty); + context.WriteByte(value == null ? BinaryTypeCode.PropertySkip : BinaryTypeCode.StringEmpty); } else { #if DEBUG context.CurrentPropertyPath = $"{prop.DeclaringType.Name}.{prop.Name}"; #endif - WriteString(value, output, context); + WriteString(value, context); } return; } @@ -1336,14 +1342,14 @@ public static partial class AcBinarySerializer // Empty string, empty collections, etc. are valid values and must be written! if (value == null) { - output.WriteByte(BinaryTypeCode.PropertySkip); + context.WriteByte(BinaryTypeCode.PropertySkip); } else { #if DEBUG context.CurrentPropertyPath = $"{prop.DeclaringType.Name}.{prop.Name}"; #endif - WriteValueNonPrimitive(value, prop.PropertyType, output, context, depth); + WriteValueNonPrimitive(value, prop.PropertyType, context, depth); } return; } @@ -1356,46 +1362,46 @@ public static partial class AcBinarySerializer /// Only called for non-nullable value types with ExpectedTypeCode set. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void WritePropertyMarkerless(object obj, BinaryPropertyAccessor prop, TOutput output) - where TOutput : BinaryOutputBase + private static void WritePropertyMarkerless(object obj, BinaryPropertyAccessor prop, BinarySerializationContext context) + where TOutput : struct, IBinaryOutputBase { switch (prop.AccessorType) { case PropertyAccessorType.Int32: - output.WriteVarInt(prop.GetInt32(obj)); + context.WriteVarInt(prop.GetInt32(obj)); return; case PropertyAccessorType.Int64: - output.WriteVarLong(prop.GetInt64(obj)); + context.WriteVarLong(prop.GetInt64(obj)); return; case PropertyAccessorType.Double: - output.WriteRaw(prop.GetDouble(obj)); + context.WriteRaw(prop.GetDouble(obj)); return; case PropertyAccessorType.Single: - output.WriteRaw(prop.GetSingle(obj)); + context.WriteRaw(prop.GetSingle(obj)); return; case PropertyAccessorType.Decimal: - output.WriteDecimalBits(prop.GetDecimal(obj)); + context.WriteDecimalBits(prop.GetDecimal(obj)); return; case PropertyAccessorType.DateTime: - output.WriteDateTimeBits(prop.GetDateTime(obj)); + context.WriteDateTimeBits(prop.GetDateTime(obj)); return; case PropertyAccessorType.Guid: - output.WriteGuidBits(prop.GetGuid(obj)); + context.WriteGuidBits(prop.GetGuid(obj)); return; case PropertyAccessorType.Byte: - output.WriteByte(prop.GetByte(obj)); + context.WriteByte(prop.GetByte(obj)); return; case PropertyAccessorType.Int16: - output.WriteRaw(prop.GetInt16(obj)); + context.WriteRaw(prop.GetInt16(obj)); return; case PropertyAccessorType.UInt16: - output.WriteRaw(prop.GetUInt16(obj)); + context.WriteRaw(prop.GetUInt16(obj)); return; case PropertyAccessorType.UInt32: - output.WriteVarUInt(prop.GetUInt32(obj)); + context.WriteVarUInt(prop.GetUInt32(obj)); return; case PropertyAccessorType.UInt64: - output.WriteVarULong(prop.GetUInt64(obj)); + context.WriteVarULong(prop.GetUInt64(obj)); return; } } @@ -1408,10 +1414,9 @@ public static partial class AcBinarySerializer /// Optimized array writer with specialized paths for primitive arrays. /// private static void WriteArray(IEnumerable enumerable, TypeMetadataWrapper wrapper, BinarySerializationContext context, int depth) - where TOutput : BinaryOutputBase + where TOutput : struct, IBinaryOutputBase { - var output = context.Output; - output.WriteByte(BinaryTypeCode.Array); + context.WriteByte(BinaryTypeCode.Array); var nextDepth = depth + 1; // Use pre-computed metadata — no GetWrapper or GetCollectionElementType needed @@ -1421,7 +1426,7 @@ public static partial class AcBinarySerializer // Optimized path for primitive arrays if (elementType != null && metadata.SourceType.IsArray) { - if (TryWritePrimitiveArray(enumerable, elementType, output, context)) + if (TryWritePrimitiveArray(enumerable, elementType, context)) return; } @@ -1429,7 +1434,7 @@ public static partial class AcBinarySerializer if (enumerable is IList list) { var count = list.Count; - output.WriteVarUInt((uint)count); + context.WriteVarUInt((uint)count); for (var i = 0; i < count; i++) { var item = list[i]; @@ -1446,7 +1451,7 @@ public static partial class AcBinarySerializer items.Add(item); } - output.WriteVarUInt((uint)items.Count); + context.WriteVarUInt((uint)items.Count); foreach (var item in items) { var itemType = item?.GetType() ?? typeof(object); @@ -1456,96 +1461,94 @@ public static partial class AcBinarySerializer /// /// Specialized array writer for primitive arrays using bulk memory operations. - /// Non-generic to avoid JIT code duplication — virtual dispatch cost is negligible - /// because this is called once per array, not per element. /// - private static bool TryWritePrimitiveArray(IEnumerable enumerable, Type elementType, TOutput output, BinarySerializationContext context) - where TOutput : BinaryOutputBase + private static bool TryWritePrimitiveArray(IEnumerable enumerable, Type elementType, BinarySerializationContext context) + where TOutput : struct, IBinaryOutputBase { // String array needs context for interning — keep generic path if (ReferenceEquals(elementType, StringType) && enumerable is string[] stringArray) { - output.WriteVarUInt((uint)stringArray.Length); + context.WriteVarUInt((uint)stringArray.Length); for (var i = 0; i < stringArray.Length; i++) { var s = stringArray[i]; if (s == null) - output.WriteByte(BinaryTypeCode.Null); + context.WriteByte(BinaryTypeCode.Null); else - WriteString(s, output, context); + WriteString(s, context); } return true; } - // All other primitive arrays don't need context — dispatch through base class - return TryWritePrimitiveArrayCore(enumerable, elementType, output); + // All other primitive arrays — inline write through context (zero virtual dispatch) + return TryWritePrimitiveArrayCore(enumerable, elementType, context); } /// - /// Non-generic core for primitive array writes. Only compiled once by JIT (not per TOutput). - /// Virtual dispatch on BinaryOutputBase is negligible: one vtable lookup per array, not per element. + /// Core primitive array writes. Uses context write methods (zero virtual dispatch). /// - private static bool TryWritePrimitiveArrayCore(IEnumerable enumerable, Type elementType, BinaryOutputBase output) + private static bool TryWritePrimitiveArrayCore(IEnumerable enumerable, Type elementType, BinarySerializationContext context) + where TOutput : struct, IBinaryOutputBase { // Int32 array - very common case if (ReferenceEquals(elementType, IntType) && enumerable is int[] intArray) { - output.WriteVarUInt((uint)intArray.Length); - output.WriteInt32ArrayOptimized(intArray); + context.WriteVarUInt((uint)intArray.Length); + context.WriteInt32ArrayOptimized(intArray); return true; } // Double array - bulk write as raw bytes if (ReferenceEquals(elementType, DoubleType) && enumerable is double[] doubleArray) { - output.WriteVarUInt((uint)doubleArray.Length); - output.WriteDoubleArrayBulk(doubleArray); + context.WriteVarUInt((uint)doubleArray.Length); + context.WriteDoubleArrayBulk(doubleArray); return true; } // Long array if (ReferenceEquals(elementType, LongType) && enumerable is long[] longArray) { - output.WriteVarUInt((uint)longArray.Length); - output.WriteLongArrayOptimized(longArray); + context.WriteVarUInt((uint)longArray.Length); + context.WriteLongArrayOptimized(longArray); return true; } // Float array - bulk write as raw bytes if (ReferenceEquals(elementType, FloatType) && enumerable is float[] floatArray) { - output.WriteVarUInt((uint)floatArray.Length); - output.WriteFloatArrayBulk(floatArray); + context.WriteVarUInt((uint)floatArray.Length); + context.WriteFloatArrayBulk(floatArray); return true; } // Bool array - pack as bytes if (ReferenceEquals(elementType, BoolType) && enumerable is bool[] boolArray) { - output.WriteVarUInt((uint)boolArray.Length); + context.WriteVarUInt((uint)boolArray.Length); for (var i = 0; i < boolArray.Length; i++) { - output.WriteByte(boolArray[i] ? BinaryTypeCode.True : BinaryTypeCode.False); + context.WriteByte(boolArray[i] ? BinaryTypeCode.True : BinaryTypeCode.False); } - output.WriteVarUInt((uint)boolArray.Length); + context.WriteVarUInt((uint)boolArray.Length); return true; } // Guid array - bulk write if (ReferenceEquals(elementType, GuidType) && enumerable is Guid[] guidArray) { - output.WriteVarUInt((uint)guidArray.Length); - output.WriteGuidArrayBulk(guidArray); + context.WriteVarUInt((uint)guidArray.Length); + context.WriteGuidArrayBulk(guidArray); return true; } // Decimal array if (ReferenceEquals(elementType, DecimalType) && enumerable is decimal[] decimalArray) { - output.WriteVarUInt((uint)decimalArray.Length); + context.WriteVarUInt((uint)decimalArray.Length); for (var i = 0; i < decimalArray.Length; i++) { - WriteDecimalUnsafe(decimalArray[i], output); + WriteDecimalUnsafe(decimalArray[i], context); } return true; } @@ -1553,11 +1556,10 @@ public static partial class AcBinarySerializer // DateTime array if (ReferenceEquals(elementType, DateTimeType) && enumerable is DateTime[] dateTimeArray) { - output.WriteVarUInt((uint)dateTimeArray.Length); + context.WriteVarUInt((uint)dateTimeArray.Length); for (var i = 0; i < dateTimeArray.Length; i++) { - WriteDateTimeUnsafe(dateTimeArray[i], output); - + WriteDateTimeUnsafe(dateTimeArray[i], context); } return true; } @@ -1566,11 +1568,10 @@ public static partial class AcBinarySerializer } private static void WriteDictionary(IDictionary dictionary, BinarySerializationContext context, int depth) - where TOutput : BinaryOutputBase + where TOutput : struct, IBinaryOutputBase { - var output = context.Output; - output.WriteByte(BinaryTypeCode.Dictionary); - output.WriteVarUInt((uint)dictionary.Count); + context.WriteByte(BinaryTypeCode.Dictionary); + context.WriteVarUInt((uint)dictionary.Count); var nextDepth = depth + 1; foreach (DictionaryEntry entry in dictionary) diff --git a/AyCode.Core/Serializers/Binaries/ArrayBinaryOutput.cs b/AyCode.Core/Serializers/Binaries/ArrayBinaryOutput.cs index c8b5e80..ef323a7 100644 --- a/AyCode.Core/Serializers/Binaries/ArrayBinaryOutput.cs +++ b/AyCode.Core/Serializers/Binaries/ArrayBinaryOutput.cs @@ -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; /// /// 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. /// -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.Shared.Rent(Math.Max(initialCapacity, MinBufferSize)); + _rentedBuffer = ArrayPool.Shared.Rent(Math.Max(initialCapacity, MinBufferSize)); } - /// - public override int Position - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => _position; - } - - #region Abstract Overrides — Core Primitives - - /// + /// + /// Provides the initial buffer. Position starts at 0, bufferEnd = buffer.Length. + /// [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; - } - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override void WriteTwoBytes(byte b1, byte b2) - { - EnsureCapacity(2); - _buffer[_position++] = b1; - _buffer[_position++] = b2; - } - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override void WriteBytes(ReadOnlySpan data) - { - EnsureCapacity(data.Length); - data.CopyTo(_buffer.AsSpan(_position)); - _position += data.Length; - } - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override void WriteRaw(T value) - { - var size = Unsafe.SizeOf(); - EnsureCapacity(size); - Unsafe.WriteUnaligned(ref _buffer[_position], value); - _position += size; - } - - /// - [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; } + /// + /// 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). + /// [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.Shared.Rent(newSize); - _buffer.AsSpan(0, _position).CopyTo(newBuffer); - ArrayPool.Shared.Return(_buffer); - _buffer = newBuffer; + buffer.AsSpan(0, position).CopyTo(newBuffer); + ArrayPool.Shared.Return(buffer); + buffer = newBuffer; + bufferEnd = newBuffer.Length; + _rentedBuffer = newBuffer; } - #endregion - - #region Optimized Overrides — Specialized Types (direct buffer access) - /// - /// Optimized: single EnsureCapacity + direct Unsafe.WriteUnaligned + indexer. + /// For ArrayBinaryOutput, position IS the total (single contiguous buffer). /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override void WriteTypeCodeAndRaw(byte typeCode, T value) - { - var size = 1 + Unsafe.SizeOf(); - EnsureCapacity(size); - _buffer[_position++] = typeCode; - Unsafe.WriteUnaligned(ref _buffer[_position], value); - _position += Unsafe.SizeOf(); - } + public int GetTotalPosition(int currentPosition) => currentPosition; - /// - /// Optimized: VarUInt with direct _buffer[_position++] access. - /// - [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; - } - - /// - /// Optimized: VarInt with direct buffer access. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override void WriteVarInt(int value) - { - var encoded = (uint)((value << 1) ^ (value >> 31)); - WriteVarUInt(encoded); - } - - /// - /// Optimized: VarULong with direct buffer access. - /// - [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; - } - - /// - /// Optimized: VarLong with direct buffer access. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override void WriteVarLong(long value) - { - var encoded = (ulong)((value << 1) ^ (value >> 63)); - WriteVarULong(encoded); - } - - /// - /// Optimized: direct Unsafe.WriteUnaligned for decimal bits. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override void WriteDecimalBits(decimal value) - { - EnsureCapacity(16); - Span bits = stackalloc int[4]; - decimal.TryGetBits(value, bits, out _); - MemoryMarshal.AsBytes(bits).CopyTo(_buffer.AsSpan(_position, 16)); - _position += 16; - } - - /// - /// Optimized: direct Unsafe.WriteUnaligned + indexer for DateTime. - /// - [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; - } - - /// - /// Optimized: direct TryWriteBytes into buffer. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override void WriteGuidBits(Guid value) - { - EnsureCapacity(16); - value.TryWriteBytes(_buffer.AsSpan(_position, 16)); - _position += 16; - } - - /// - /// Optimized: direct Unsafe.WriteUnaligned for DateTimeOffset. - /// - [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; - } - - /// - /// Optimized: direct ASCII fast path into _buffer. - /// - 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; - } - - /// - /// Optimized: FixStr with direct buffer write. - /// - 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; - } - - /// - /// Optimized: FixStrDirect with SIMD try into buffer. - /// - 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); - } - } - - /// - /// Optimized: FixStrBytes with direct buffer copy. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override void WriteFixStrBytes(ReadOnlySpan 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) - - /// - 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; - } - } - - /// - 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; - } - } - - /// - 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; - } - } - - /// - public override void WriteBytesSimd(ReadOnlySpan source) - { - EnsureCapacity(source.Length); - var destination = _buffer.AsSpan(_position, source.Length); - - if (Vector.IsHardwareAccelerated && source.Length >= Vector.Count * 2) - { - var vectorSize = Vector.Count; - var i = 0; - var length = source.Length; - var vectorCount = length / vectorSize; - for (var v = 0; v < vectorCount; v++) - { - var vec = new Vector(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 /// Returns the written data as a ReadOnlySpan without allocation. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public ReadOnlySpan AsSpan() => _buffer.AsSpan(0, _position); + public ReadOnlySpan AsSpan(byte[] buffer, int position) => buffer.AsSpan(0, position); /// Copies the written data to a new exactly-sized array. - public byte[] ToArray() + public byte[] ToArray(byte[] buffer, int position) { - var result = GC.AllocateUninitializedArray(_position); - _buffer.AsSpan(0, _position).CopyTo(result); + var result = GC.AllocateUninitializedArray(position); + buffer.AsSpan(0, position).CopyTo(result); return result; } /// Copies the written data to an IBufferWriter (single memcpy). - public void WriteTo(IBufferWriter writer) + public void WriteTo(IBufferWriter 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); } - /// Resets position for reuse without deallocation. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Reset() => _position = 0; - /// /// 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. /// - 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.Shared.Rent(Math.Max(resultBuffer.Length / 2, MinBufferSize)); - _position = 0; + _rentedBuffer = ArrayPool.Shared.Rent(Math.Max(resultBuffer.Length / 2, MinBufferSize)); return new AcBinarySerializer.BinarySerializationResult(resultBuffer, resultLength, pooled: true); } + /// Resets for reuse — nothing to do, context handles position via Initialize. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Reset() { } + #endregion #region IDisposable public void Dispose() { - if (_buffer != null) + if (_rentedBuffer != null) { - ArrayPool.Shared.Return(_buffer); - _buffer = null!; + ArrayPool.Shared.Return(_rentedBuffer); + _rentedBuffer = null!; } } diff --git a/AyCode.Core/Serializers/Binaries/PooledBufferWriter.cs b/AyCode.Core/Serializers/Binaries/ArrayPooledBufferWriter.cs similarity index 96% rename from AyCode.Core/Serializers/Binaries/PooledBufferWriter.cs rename to AyCode.Core/Serializers/Binaries/ArrayPooledBufferWriter.cs index 144b352..64ac742 100644 --- a/AyCode.Core/Serializers/Binaries/PooledBufferWriter.cs +++ b/AyCode.Core/Serializers/Binaries/ArrayPooledBufferWriter.cs @@ -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. /// -internal sealed class PooledBufferWriter : IBufferWriter, IDisposable +internal sealed class ArrayPooledBufferWriter : IBufferWriter, IDisposable { private byte[] _buffer; private int _written; - public PooledBufferWriter(int initialCapacity) + public ArrayPooledBufferWriter(int initialCapacity) { _buffer = ArrayPool.Shared.Rent(Math.Max(initialCapacity, 256)); } diff --git a/AyCode.Core/Serializers/Binaries/BinaryOutputBase.cs b/AyCode.Core/Serializers/Binaries/BinaryOutputBase.cs index 829b220..f560626 100644 --- a/AyCode.Core/Serializers/Binaries/BinaryOutputBase.cs +++ b/AyCode.Core/Serializers/Binaries/BinaryOutputBase.cs @@ -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; /// /// 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) /// -public abstract class BinaryOutputBase : IBinaryOutput +public interface IBinaryOutputBase { - private static readonly Encoding Utf8NoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false); - - #region Abstract — Core Primitives (derived must implement) - - /// - public abstract void WriteByte(byte value); - - /// - public abstract void WriteTwoBytes(byte b1, byte b2); - - /// - public abstract void WriteBytes(ReadOnlySpan data); - - /// - public abstract void WriteRaw(T value) where T : unmanaged; + /// + /// 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. + /// + public void Initialize(out byte[] buffer, out int position, out int bufferEnd); /// - /// Ensure the backing storage can accept at least 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. /// - protected abstract void EnsureCapacity(int additionalBytes); + [MethodImpl(MethodImplOptions.NoInlining)] + public void Grow(ref byte[] buffer, ref int position, ref int bufferEnd, int needed); - /// - public abstract int Position { get; } - - #endregion - - #region Abstract — WriteTypeCodeAndRaw - - /// - public abstract void WriteTypeCodeAndRaw(byte typeCode, T value) where T : unmanaged; - - #endregion - - #region Abstract — VarInt Encoding - - /// - public abstract void WriteVarInt(int value); - - /// - public abstract void WriteVarUInt(uint value); - - /// - public abstract void WriteVarLong(long value); - - /// - public abstract void WriteVarULong(ulong value); - - #endregion - - #region Virtual — Specialized Types - - /// - public virtual void WriteDecimalBits(decimal value) - { - Span bits = stackalloc int[4]; - decimal.TryGetBits(value, bits, out _); - WriteBytes(MemoryMarshal.AsBytes(bits)); - } - - /// - public abstract void WriteDateTimeBits(DateTime value); - - /// - public virtual void WriteGuidBits(Guid value) - { - Span buf = stackalloc byte[16]; - value.TryWriteBytes(buf); - WriteBytes(buf); - } - - /// - public abstract void WriteDateTimeOffsetBits(DateTimeOffset value); - - #endregion - - #region Virtual — String Writes - - /// - public virtual void WriteStringUtf8(string value) - { - if (Ascii.IsValid(value)) - { - WriteVarUInt((uint)value.Length); - Span 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 utf8Buf = byteCount <= 256 - ? stackalloc byte[byteCount] - : new byte[byteCount]; - Utf8NoBom.GetBytes(value.AsSpan(), utf8Buf); - WriteBytes(utf8Buf); - } - - /// - public virtual void WriteFixStr(string value) - { - var length = value.Length; - WriteByte(BinaryTypeCode.EncodeFixStr(length)); - Span buf = length <= 256 - ? stackalloc byte[length] - : new byte[length]; - Ascii.FromUtf16(value.AsSpan(), buf, out _); - WriteBytes(buf); - } - - /// - public virtual void WriteFixStrDirect(string value) - { - var length = value.Length; - Span 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); - } - } - - /// - public virtual void WriteFixStrBytes(ReadOnlySpan utf8Bytes) - { - WriteByte(BinaryTypeCode.EncodeFixStr(utf8Bytes.Length)); - WriteBytes(utf8Bytes); - } - - /// - public virtual void WritePreencodedPropertyName(ReadOnlySpan utf8Name) - { - WriteByte(BinaryTypeCode.String); - WriteVarUInt((uint)utf8Name.Length); - WriteBytes(utf8Name); - } - - private void WriteStringUtf8Internal(string value) - { - var byteCount = Utf8NoBom.GetByteCount(value); - WriteVarUInt((uint)byteCount); - Span buf = byteCount <= 256 - ? stackalloc byte[byteCount] - : new byte[byteCount]; - Utf8NoBom.GetBytes(value.AsSpan(), buf); - WriteBytes(buf); - } - - #endregion - - #region Bulk Array Writes - - /// - public abstract void WriteDoubleArrayBulk(double[] array); - - /// - public abstract void WriteFloatArrayBulk(float[] array); - - /// - public abstract void WriteGuidArrayBulk(Guid[] array); - - /// - 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); - } - } - } - - /// - 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); - } - } - } - - /// - public virtual void WriteBytesSimd(ReadOnlySpan source) - { - WriteBytes(source); - } - - #endregion + /// + /// 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). + /// + public int GetTotalPosition(int currentPosition); } diff --git a/AyCode.Core/Serializers/Binaries/BufferWriterBinaryOutput.cs b/AyCode.Core/Serializers/Binaries/BufferWriterBinaryOutput.cs index 02c3703..90cde34 100644 --- a/AyCode.Core/Serializers/Binaries/BufferWriterBinaryOutput.cs +++ b/AyCode.Core/Serializers/Binaries/BufferWriterBinaryOutput.cs @@ -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; /// /// 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 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 after all standalone writes to commit any pending bytes. /// -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 _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 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; } - /// - public override int Position + /// + /// Provides the initial buffer from the IBufferWriter. + /// + [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; } + /// + /// Called when the context's buffer is full. Commits current chunk to the IBufferWriter + /// and acquires a new chunk. + /// + [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; + } + + /// + /// Returns total bytes written: committed + pending in current chunk. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int GetTotalPosition(int currentPosition) + => _committedBytes + (currentPosition - _currentChunkStart); + /// /// Commits any pending bytes to the underlying IBufferWriter. /// Must be called after all writes are complete. + /// Takes the context's current buffer state. /// [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.Shared.Return(_chunkArray); + var span = _writer.GetSpan(bytesInChunk); + buffer.AsSpan(_currentChunkStart, bytesInChunk).CopyTo(span); + _writer.Advance(bytesInChunk); + ArrayPool.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 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.Shared.Rent(requestSize); - _chunkOffset = 0; - _chunkLength = _chunkArray.Length; + // Rent our own buffer; FlushOwnedBuffer copies to writer on next Grow/Flush. + var owned = ArrayPool.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) - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override void WriteByte(byte value) + /// + /// Total bytes written in standalone mode (committed + pending in current chunk). + /// + public int Position { - if (_chunkPos >= _chunkLength) - RentChunk(1); - _chunkArray[_chunkOffset + _chunkPos++] = value; - _written++; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _committedBytes + (_position - _currentChunkStart); } - /// [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); } - /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override void WriteBytes(ReadOnlySpan 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; } - /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override void WriteRaw(T value) + public void WriteBytes(ReadOnlySpan data) + { + StandaloneEnsureCapacity(data.Length); + data.CopyTo(_buffer.AsSpan(_position)); + _position += data.Length; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteRaw(T value) where T : unmanaged { var size = Unsafe.SizeOf(); - 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; } - /// - /// Ensures the cached chunk has room for at least . - /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - protected override void EnsureCapacity(int additionalBytes) - { - if (_chunkPos + additionalBytes > _chunkLength) - RentChunk(additionalBytes); - } - - #endregion - - #region Optimized Overrides — Batched Writes - - /// - /// Optimized: single capacity check for type code + value, direct buffer write. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override void WriteTypeCodeAndRaw(byte typeCode, T value) - { - var size = 1 + Unsafe.SizeOf(); - if (_chunkPos + size > _chunkLength) - RentChunk(size); - var off = _chunkOffset + _chunkPos; - _chunkArray[off] = typeCode; - Unsafe.WriteUnaligned(ref _chunkArray[off + 1], value); - _chunkPos += size; - _written += size; - } - - /// - /// Optimized: VarUInt with direct cached buffer access. - /// - [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; } /// - /// Optimized: ZigZag + batched VarUInt. + /// Commits any pending bytes in standalone mode to the underlying IBufferWriter. + /// Call after all standalone writes are complete. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override void WriteVarInt(int value) + public void Flush() { - var encoded = (uint)((value << 1) ^ (value >> 31)); - WriteVarUInt(encoded); - } - - /// - /// Optimized: VarULong with direct cached buffer access. - /// - [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; - } - - /// - /// Optimized: ZigZag + batched VarULong. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override void WriteVarLong(long value) - { - var encoded = (ulong)((value << 1) ^ (value >> 63)); - WriteVarULong(encoded); - } - - /// - /// Optimized: single capacity check for DateTime (9 bytes), direct buffer write. - /// - [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; - } - - /// - /// Optimized: single capacity check for DateTimeOffset (10 bytes), direct buffer write. - /// - [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) - - /// - 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; - } - - /// - 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; - } - - /// - 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 diff --git a/AyCode.Core/Serializers/Binaries/IBinaryOutput.cs b/AyCode.Core/Serializers/Binaries/IBinaryOutput.cs deleted file mode 100644 index 711d7b5..0000000 --- a/AyCode.Core/Serializers/Binaries/IBinaryOutput.cs +++ /dev/null @@ -1,111 +0,0 @@ -using System; -using System.Runtime.CompilerServices; - -namespace AyCode.Core.Serializers.Binaries; - -/// -/// Abstraction for binary serialization output. -/// Implementations can write to byte[] (ArrayBinaryOutput) or IBufferWriter (BufferWriterBinaryOutput). -/// Custom implementations can be provided for specialized output targets. -/// -public interface IBinaryOutput -{ - #region Core Writes - - /// Write a single byte. - void WriteByte(byte value); - - /// Write two bytes efficiently. - void WriteTwoBytes(byte b1, byte b2); - - /// Write a span of bytes. - void WriteBytes(ReadOnlySpan data); - - /// Write an unmanaged value directly (no encoding). - void WriteRaw(T value) where T : unmanaged; - - /// Write a type code byte followed by an unmanaged value. Batches capacity check. - void WriteTypeCodeAndRaw(byte typeCode, T value) where T : unmanaged; - - #endregion - - #region VarInt Encoding - - /// Write a ZigZag-encoded variable-length int32. - void WriteVarInt(int value); - - /// Write a variable-length uint32. - void WriteVarUInt(uint value); - - /// Write a ZigZag-encoded variable-length int64. - void WriteVarLong(long value); - - /// Write a variable-length uint64. - void WriteVarULong(ulong value); - - #endregion - - #region Specialized Types - - /// Write decimal as 16 raw bytes (4 x int32 bits). - void WriteDecimalBits(decimal value); - - /// Write DateTime as 8 bytes ticks + 1 byte kind. - void WriteDateTimeBits(DateTime value); - - /// Write Guid as 16 raw bytes. - void WriteGuidBits(Guid value); - - /// Write DateTimeOffset as 8 bytes UTC ticks + 2 bytes offset minutes. - void WriteDateTimeOffsetBits(DateTimeOffset value); - - #endregion - - #region String Writes - - /// Write UTF8 string with VarUInt length prefix. Fast path for ASCII. - void WriteStringUtf8(string value); - - /// Write short ASCII string using FixStr encoding (type+length in single byte). - void WriteFixStr(string value); - - /// Write FixStr with SIMD ASCII try, falls back to standard UTF8. - void WriteFixStrDirect(string value); - - /// Write pre-encoded UTF8 bytes using FixStr encoding. - void WriteFixStrBytes(ReadOnlySpan utf8Bytes); - - /// Write pre-encoded property name with String type code + VarUInt length + bytes. - void WritePreencodedPropertyName(ReadOnlySpan utf8Name); - - #endregion - - #region Bulk Array Writes - - /// Write double[] with per-element type codes. Override for optimized bulk write. - void WriteDoubleArrayBulk(double[] array); - - /// Write float[] with per-element type codes. Override for optimized bulk write. - void WriteFloatArrayBulk(float[] array); - - /// Write Guid[] with per-element type codes. Override for optimized bulk write. - void WriteGuidArrayBulk(Guid[] array); - - /// Write int[] with TinyInt optimization per element. - void WriteInt32ArrayOptimized(int[] array); - - /// Write long[] with TinyInt/Int32 downcast optimization per element. - void WriteLongArrayOptimized(long[] array); - - /// Write bytes using SIMD when available, standard copy otherwise. - void WriteBytesSimd(ReadOnlySpan source); - - #endregion - - #region Position - - /// Current write position (total bytes written so far). - int Position { get; } - - #endregion -}