From 97e4315d12a1a079a02bf2e9126f34d4fc835dd4 Mon Sep 17 00:00:00 2001 From: Loretta Date: Wed, 11 Feb 2026 17:29:19 +0100 Subject: [PATCH] Replace Newtonsoft.Json benchmark with AcBinary BufferWriter Removed Newtonsoft.Json from benchmarks and codebase. Added AcBinaryBufferWriterBenchmark using ArrayBufferWriter and AcBinarySerializer's buffer writer API. Optimized WriteStringUtf8 for ASCII fast path. Improved ArrayBinaryOutput buffer reuse and memory management. Introduced Reset method to IBinaryOutputBase and implemented it in outputs. Streamlined serializer benchmarks to focus on AcBinary and System.Text.Json. --- AyCode.Core.Serializers.Console/Program.cs | 39 ++++++++++--------- ...rySerializer.BinarySerializationContext.cs | 25 +++++++++--- .../Binaries/AcBinarySerializer.cs | 2 +- .../Serializers/Binaries/ArrayBinaryOutput.cs | 30 +++++++++++--- .../Serializers/Binaries/BinaryOutputBase.cs | 8 ++++ .../Binaries/BufferWriterBinaryOutput.cs | 21 +++++++--- 6 files changed, 89 insertions(+), 36 deletions(-) diff --git a/AyCode.Core.Serializers.Console/Program.cs b/AyCode.Core.Serializers.Console/Program.cs index 38052b5..04309a4 100644 --- a/AyCode.Core.Serializers.Console/Program.cs +++ b/AyCode.Core.Serializers.Console/Program.cs @@ -5,7 +5,7 @@ using AyCode.Core.Tests.TestModels; using MessagePack; using MessagePack.Resolvers; using Microsoft.Extensions.Options; -using Newtonsoft.Json; +using System.Buffers; using System.Diagnostics; using System.Runtime.CompilerServices; using System.Text; @@ -40,7 +40,7 @@ public static class Program private const string SerializerAcBinaryFastMode = "AcBinary (FastMode)"; private const string SerializerAcBinaryNoIntern = "AcBinary (NoIntern)"; private const string SerializerAcJsonDefault = "AcJson (Default)"; - private const string SerializerNewtonsoftJson = "Newtonsoft.Json"; + private const string SerializerAcBinaryBufferWriter = "AcBinary (BufferWriter)"; private const string SerializerSystemTextJson = "System.Text.Json"; private static readonly UTF8Encoding Utf8NoBom = new(encoderShouldEmitUTF8Identifier: false); @@ -228,8 +228,8 @@ public static class Program // MessagePack new MessagePackBenchmark(testData.Order, SerializerMessagePack), - // Newtonsoft.Json - new NewtonsoftBenchmark(testData.Order, SerializerNewtonsoftJson), + // AcBinary BufferWriter + new AcBinaryBufferWriterBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryBufferWriter), // System.Text.Json new SystemTextJsonBenchmark(testData.Order, SerializerSystemTextJson) @@ -367,27 +367,23 @@ public static class Program public void Deserialize() => MessagePackSerializer.Deserialize(_serialized, _options); } - private sealed class NewtonsoftBenchmark : ISerializerBenchmark + private sealed class AcBinaryBufferWriterBenchmark : ISerializerBenchmark { private readonly TestOrder _order; - private readonly JsonSerializerSettings _settings; - private readonly string _serialized; - private readonly byte[] _serializedUtf8; + private readonly AcBinarySerializerOptions _options; + private readonly byte[] _serialized; + private ArrayBufferWriter _bufferWriter; public string Name { get; } - public int SerializedSize => _serializedUtf8.Length; + public int SerializedSize => _serialized.Length; - public NewtonsoftBenchmark(TestOrder order, string name) + public AcBinaryBufferWriterBenchmark(TestOrder order, AcBinarySerializerOptions options, string name) { _order = order; + _options = options; Name = name; - _settings = new JsonSerializerSettings - { - ReferenceLoopHandling = ReferenceLoopHandling.Ignore, - NullValueHandling = NullValueHandling.Ignore - }; - _serialized = JsonConvert.SerializeObject(order, _settings); - _serializedUtf8 = Utf8NoBom.GetBytes(_serialized); + _serialized = AcBinarySerializer.Serialize(order, options); + //_bufferWriter = new ArrayBufferWriter(); } public void Warmup(int iterations) @@ -400,10 +396,15 @@ public static class Program } [MethodImpl(MethodImplOptions.NoInlining)] - public void Serialize() => JsonConvert.SerializeObject(_order, _settings); + public void Serialize() + { + //_bufferWriter.ResetWrittenCount(); + _bufferWriter = new ArrayBufferWriter(); + AcBinarySerializer.Serialize(_order, _bufferWriter, _options); + } [MethodImpl(MethodImplOptions.NoInlining)] - public void Deserialize() => JsonConvert.DeserializeObject(_serialized, _settings); + public void Deserialize() => AcBinaryDeserializer.Deserialize(_serialized, _options); } private sealed class SystemTextJsonBenchmark : ISerializerBenchmark diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs index dba7277..e4e4b41 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs @@ -171,6 +171,12 @@ public static partial class AcBinarySerializer public override void Clear() { + // Reset output buffer (large buffers → return to pool, rent halved) + if (OutputInitialized) + { + Output.Reset(); + } + _stringInternMap?.Reset(); _nextCacheIndex = 0; NextFirstIndex = 0; @@ -364,15 +370,24 @@ public static partial class AcBinarySerializer public void WriteStringUtf8(string value) { - if (Ascii.IsValid(value)) + var charLength = value.Length; + + // Speculative ASCII fast path: assume byteCount == charLength + // Single-pass Ascii.FromUtf16 (scan+copy combined) instead of + // Ascii.IsValid (scan) + Ascii.FromUtf16 (scan+copy) = double traversal + var savedPosition = _position; + WriteVarUInt((uint)charLength); + EnsureCapacity(charLength); + + if (Ascii.FromUtf16(value.AsSpan(), _buffer.AsSpan(_position, charLength), out _) == OperationStatus.Done) { - WriteVarUInt((uint)value.Length); - EnsureCapacity(value.Length); - Ascii.FromUtf16(value.AsSpan(), _buffer.AsSpan(_position, value.Length), out _); - _position += value.Length; + _position += charLength; return; } + // Non-ASCII fallback: rewind VarUInt, encode with UTF-8 + // FromUtf16 fails fast on first non-ASCII char, so speculative cost is minimal + _position = savedPosition; var byteCount = Utf8NoBom.GetByteCount(value); WriteVarUInt((uint)byteCount); EnsureCapacity(byteCount); diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs index 8665456..9855587 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs @@ -76,7 +76,7 @@ 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.Output = new ArrayBinaryOutput(4096); context.OutputInitialized = true; context.Output.Initialize(out context._buffer, out context._position, out context._bufferEnd); diff --git a/AyCode.Core/Serializers/Binaries/ArrayBinaryOutput.cs b/AyCode.Core/Serializers/Binaries/ArrayBinaryOutput.cs index ef323a7..49382b0 100644 --- a/AyCode.Core/Serializers/Binaries/ArrayBinaryOutput.cs +++ b/AyCode.Core/Serializers/Binaries/ArrayBinaryOutput.cs @@ -13,13 +13,16 @@ namespace AyCode.Core.Serializers.Binaries; /// public struct ArrayBinaryOutput : IBinaryOutputBase, IDisposable { - private const int MinBufferSize = 256; + private const int DefaultMinBufferSize = 256; + private const int MaxKeepBufferSize = 32 * 1024; // 32KB — below this, keep for reuse + private readonly int _initialCapacity; private byte[] _rentedBuffer; public ArrayBinaryOutput(int initialCapacity = 4096) { - _rentedBuffer = ArrayPool.Shared.Rent(Math.Max(initialCapacity, MinBufferSize)); + _initialCapacity = Math.Max(initialCapacity, DefaultMinBufferSize); + _rentedBuffer = ArrayPool.Shared.Rent(_initialCapacity); } /// @@ -87,14 +90,29 @@ public struct ArrayBinaryOutput : IBinaryOutputBase, IDisposable var resultBuffer = buffer; var resultLength = position; - _rentedBuffer = ArrayPool.Shared.Rent(Math.Max(resultBuffer.Length / 2, MinBufferSize)); + //_rentedBuffer = ArrayPool.Shared.Rent(Math.Max(resultBuffer.Length / 2, _initialCapacity)); 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() { } + /// + /// Resets for reuse when context is returned to pool. + /// Small buffers (≤ MaxKeepBufferSize): keep as-is (faster than pool round-trip). + /// Large buffers: return to pool, rent a halved one to avoid memory bloat. + /// + public void Reset() + { + if (_rentedBuffer == null) return; + + // Small buffer: keep as-is + if (_rentedBuffer.Length <= MaxKeepBufferSize) return; + + // Large buffer: return to pool, rent half size + var nextCapacity = Math.Max(_rentedBuffer.Length / 2, _initialCapacity); + ArrayPool.Shared.Return(_rentedBuffer); + + _rentedBuffer = nextCapacity == _initialCapacity ? null : ArrayPool.Shared.Rent(nextCapacity); + } #endregion diff --git a/AyCode.Core/Serializers/Binaries/BinaryOutputBase.cs b/AyCode.Core/Serializers/Binaries/BinaryOutputBase.cs index f560626..58de305 100644 --- a/AyCode.Core/Serializers/Binaries/BinaryOutputBase.cs +++ b/AyCode.Core/Serializers/Binaries/BinaryOutputBase.cs @@ -39,4 +39,12 @@ public interface IBinaryOutputBase /// For BufferWriterBinaryOutput: returns committedBytes + (currentPosition - chunkStart). /// public int GetTotalPosition(int currentPosition); + + /// + /// Resets the output for reuse when the context is returned to the pool. + /// Small buffers: keep as-is (faster than pool round-trip). + /// Large buffers: return to pool, rent a smaller one (halved capacity). + /// For BufferWriterBinaryOutput: no-op (chunks are owned by IBufferWriter). + /// + public void Reset(); } diff --git a/AyCode.Core/Serializers/Binaries/BufferWriterBinaryOutput.cs b/AyCode.Core/Serializers/Binaries/BufferWriterBinaryOutput.cs index 90cde34..4d6b4ad 100644 --- a/AyCode.Core/Serializers/Binaries/BufferWriterBinaryOutput.cs +++ b/AyCode.Core/Serializers/Binaries/BufferWriterBinaryOutput.cs @@ -147,6 +147,11 @@ public struct BufferWriterBinaryOutput : IBinaryOutputBase } } + /// + /// No-op for BufferWriterBinaryOutput — chunks are owned by IBufferWriter, not us. + /// + public void Reset() { } + #region Standalone Write Methods — for direct usage outside serialization pipeline (e.g. AcBinaryHubProtocol) /// @@ -211,15 +216,21 @@ public struct BufferWriterBinaryOutput : IBinaryOutputBase public void WriteStringUtf8(string value) { - if (Ascii.IsValid(value)) + var charLength = value.Length; + + // Speculative ASCII fast path: single-pass Ascii.FromUtf16 + var savedPosition = _position; + WriteVarUInt((uint)charLength); + StandaloneEnsureCapacity(charLength); + + if (Ascii.FromUtf16(value.AsSpan(), _buffer.AsSpan(_position, charLength), out _) == OperationStatus.Done) { - WriteVarUInt((uint)value.Length); - StandaloneEnsureCapacity(value.Length); - Ascii.FromUtf16(value.AsSpan(), _buffer.AsSpan(_position, value.Length), out _); - _position += value.Length; + _position += charLength; return; } + // Non-ASCII fallback: rewind VarUInt, encode with UTF-8 + _position = savedPosition; var byteCount = Utf8NoBom.GetByteCount(value); WriteVarUInt((uint)byteCount); StandaloneEnsureCapacity(byteCount);