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.
This commit is contained in:
Loretta 2026-02-11 17:29:19 +01:00
parent 991e8f6038
commit 97e4315d12
6 changed files with 89 additions and 36 deletions

View File

@ -5,7 +5,7 @@ using AyCode.Core.Tests.TestModels;
using MessagePack; using MessagePack;
using MessagePack.Resolvers; using MessagePack.Resolvers;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Newtonsoft.Json; using System.Buffers;
using System.Diagnostics; using System.Diagnostics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Text; using System.Text;
@ -40,7 +40,7 @@ public static class Program
private const string SerializerAcBinaryFastMode = "AcBinary (FastMode)"; private const string SerializerAcBinaryFastMode = "AcBinary (FastMode)";
private const string SerializerAcBinaryNoIntern = "AcBinary (NoIntern)"; private const string SerializerAcBinaryNoIntern = "AcBinary (NoIntern)";
private const string SerializerAcJsonDefault = "AcJson (Default)"; 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 const string SerializerSystemTextJson = "System.Text.Json";
private static readonly UTF8Encoding Utf8NoBom = new(encoderShouldEmitUTF8Identifier: false); private static readonly UTF8Encoding Utf8NoBom = new(encoderShouldEmitUTF8Identifier: false);
@ -228,8 +228,8 @@ public static class Program
// MessagePack // MessagePack
new MessagePackBenchmark(testData.Order, SerializerMessagePack), new MessagePackBenchmark(testData.Order, SerializerMessagePack),
// Newtonsoft.Json // AcBinary BufferWriter
new NewtonsoftBenchmark(testData.Order, SerializerNewtonsoftJson), new AcBinaryBufferWriterBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryBufferWriter),
// System.Text.Json // System.Text.Json
new SystemTextJsonBenchmark(testData.Order, SerializerSystemTextJson) new SystemTextJsonBenchmark(testData.Order, SerializerSystemTextJson)
@ -367,27 +367,23 @@ public static class Program
public void Deserialize() => MessagePackSerializer.Deserialize<TestOrder>(_serialized, _options); public void Deserialize() => MessagePackSerializer.Deserialize<TestOrder>(_serialized, _options);
} }
private sealed class NewtonsoftBenchmark : ISerializerBenchmark private sealed class AcBinaryBufferWriterBenchmark : ISerializerBenchmark
{ {
private readonly TestOrder _order; private readonly TestOrder _order;
private readonly JsonSerializerSettings _settings; private readonly AcBinarySerializerOptions _options;
private readonly string _serialized; private readonly byte[] _serialized;
private readonly byte[] _serializedUtf8; private ArrayBufferWriter<byte> _bufferWriter;
public string Name { get; } 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; _order = order;
_options = options;
Name = name; Name = name;
_settings = new JsonSerializerSettings _serialized = AcBinarySerializer.Serialize(order, options);
{ //_bufferWriter = new ArrayBufferWriter<byte>();
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
NullValueHandling = NullValueHandling.Ignore
};
_serialized = JsonConvert.SerializeObject(order, _settings);
_serializedUtf8 = Utf8NoBom.GetBytes(_serialized);
} }
public void Warmup(int iterations) public void Warmup(int iterations)
@ -400,10 +396,15 @@ public static class Program
} }
[MethodImpl(MethodImplOptions.NoInlining)] [MethodImpl(MethodImplOptions.NoInlining)]
public void Serialize() => JsonConvert.SerializeObject(_order, _settings); public void Serialize()
{
//_bufferWriter.ResetWrittenCount();
_bufferWriter = new ArrayBufferWriter<byte>();
AcBinarySerializer.Serialize(_order, _bufferWriter, _options);
}
[MethodImpl(MethodImplOptions.NoInlining)] [MethodImpl(MethodImplOptions.NoInlining)]
public void Deserialize() => JsonConvert.DeserializeObject<TestOrder>(_serialized, _settings); public void Deserialize() => AcBinaryDeserializer.Deserialize<TestOrder>(_serialized, _options);
} }
private sealed class SystemTextJsonBenchmark : ISerializerBenchmark private sealed class SystemTextJsonBenchmark : ISerializerBenchmark

View File

@ -171,6 +171,12 @@ public static partial class AcBinarySerializer
public override void Clear() public override void Clear()
{ {
// Reset output buffer (large buffers → return to pool, rent halved)
if (OutputInitialized)
{
Output.Reset();
}
_stringInternMap?.Reset(); _stringInternMap?.Reset();
_nextCacheIndex = 0; _nextCacheIndex = 0;
NextFirstIndex = 0; NextFirstIndex = 0;
@ -364,15 +370,24 @@ public static partial class AcBinarySerializer
public void WriteStringUtf8(string value) 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); _position += charLength;
EnsureCapacity(value.Length);
Ascii.FromUtf16(value.AsSpan(), _buffer.AsSpan(_position, value.Length), out _);
_position += value.Length;
return; 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); var byteCount = Utf8NoBom.GetByteCount(value);
WriteVarUInt((uint)byteCount); WriteVarUInt((uint)byteCount);
EnsureCapacity(byteCount); EnsureCapacity(byteCount);

View File

@ -76,7 +76,7 @@ public static partial class AcBinarySerializer
// Create context without pooling (we need to set up callback) // Create context without pooling (we need to set up callback)
using var context = new BinarySerializationContext<ArrayBinaryOutput>(analysisOptions); using var context = new BinarySerializationContext<ArrayBinaryOutput>(analysisOptions);
context.Output = new ArrayBinaryOutput(); context.Output = new ArrayBinaryOutput(4096);
context.OutputInitialized = true; context.OutputInitialized = true;
context.Output.Initialize(out context._buffer, out context._position, out context._bufferEnd); context.Output.Initialize(out context._buffer, out context._position, out context._bufferEnd);

View File

@ -13,13 +13,16 @@ namespace AyCode.Core.Serializers.Binaries;
/// </summary> /// </summary>
public struct ArrayBinaryOutput : IBinaryOutputBase, IDisposable 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; private byte[] _rentedBuffer;
public ArrayBinaryOutput(int initialCapacity = 4096) public ArrayBinaryOutput(int initialCapacity = 4096)
{ {
_rentedBuffer = ArrayPool<byte>.Shared.Rent(Math.Max(initialCapacity, MinBufferSize)); _initialCapacity = Math.Max(initialCapacity, DefaultMinBufferSize);
_rentedBuffer = ArrayPool<byte>.Shared.Rent(_initialCapacity);
} }
/// <summary> /// <summary>
@ -87,14 +90,29 @@ public struct ArrayBinaryOutput : IBinaryOutputBase, IDisposable
var resultBuffer = buffer; var resultBuffer = buffer;
var resultLength = position; var resultLength = position;
_rentedBuffer = ArrayPool<byte>.Shared.Rent(Math.Max(resultBuffer.Length / 2, MinBufferSize)); //_rentedBuffer = ArrayPool<byte>.Shared.Rent(Math.Max(resultBuffer.Length / 2, _initialCapacity));
return new AcBinarySerializer.BinarySerializationResult(resultBuffer, resultLength, pooled: true); return new AcBinarySerializer.BinarySerializationResult(resultBuffer, resultLength, pooled: true);
} }
/// <summary>Resets for reuse — nothing to do, context handles position via Initialize.</summary> /// <summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)] /// Resets for reuse when context is returned to pool.
public void Reset() { } /// Small buffers (≤ MaxKeepBufferSize): keep as-is (faster than pool round-trip).
/// Large buffers: return to pool, rent a halved one to avoid memory bloat.
/// </summary>
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<byte>.Shared.Return(_rentedBuffer);
_rentedBuffer = nextCapacity == _initialCapacity ? null : ArrayPool<byte>.Shared.Rent(nextCapacity);
}
#endregion #endregion

View File

@ -39,4 +39,12 @@ public interface IBinaryOutputBase
/// For BufferWriterBinaryOutput: returns committedBytes + (currentPosition - chunkStart). /// For BufferWriterBinaryOutput: returns committedBytes + (currentPosition - chunkStart).
/// </summary> /// </summary>
public int GetTotalPosition(int currentPosition); public int GetTotalPosition(int currentPosition);
/// <summary>
/// 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).
/// </summary>
public void Reset();
} }

View File

@ -147,6 +147,11 @@ public struct BufferWriterBinaryOutput : IBinaryOutputBase
} }
} }
/// <summary>
/// No-op for BufferWriterBinaryOutput — chunks are owned by IBufferWriter, not us.
/// </summary>
public void Reset() { }
#region Standalone Write Methods for direct usage outside serialization pipeline (e.g. AcBinaryHubProtocol) #region Standalone Write Methods for direct usage outside serialization pipeline (e.g. AcBinaryHubProtocol)
/// <summary> /// <summary>
@ -211,15 +216,21 @@ public struct BufferWriterBinaryOutput : IBinaryOutputBase
public void WriteStringUtf8(string value) 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); _position += charLength;
StandaloneEnsureCapacity(value.Length);
Ascii.FromUtf16(value.AsSpan(), _buffer.AsSpan(_position, value.Length), out _);
_position += value.Length;
return; return;
} }
// Non-ASCII fallback: rewind VarUInt, encode with UTF-8
_position = savedPosition;
var byteCount = Utf8NoBom.GetByteCount(value); var byteCount = Utf8NoBom.GetByteCount(value);
WriteVarUInt((uint)byteCount); WriteVarUInt((uint)byteCount);
StandaloneEnsureCapacity(byteCount); StandaloneEnsureCapacity(byteCount);