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:
parent
991e8f6038
commit
97e4315d12
|
|
@ -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<TestOrder>(_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<byte> _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<byte>();
|
||||
}
|
||||
|
||||
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<byte>();
|
||||
AcBinarySerializer.Serialize(_order, _bufferWriter, _options);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
public void Deserialize() => JsonConvert.DeserializeObject<TestOrder>(_serialized, _settings);
|
||||
public void Deserialize() => AcBinaryDeserializer.Deserialize<TestOrder>(_serialized, _options);
|
||||
}
|
||||
|
||||
private sealed class SystemTextJsonBenchmark : ISerializerBenchmark
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ public static partial class AcBinarySerializer
|
|||
|
||||
// Create context without pooling (we need to set up callback)
|
||||
using var context = new BinarySerializationContext<ArrayBinaryOutput>(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);
|
||||
|
||||
|
|
|
|||
|
|
@ -13,13 +13,16 @@ namespace AyCode.Core.Serializers.Binaries;
|
|||
/// </summary>
|
||||
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<byte>.Shared.Rent(Math.Max(initialCapacity, MinBufferSize));
|
||||
_initialCapacity = Math.Max(initialCapacity, DefaultMinBufferSize);
|
||||
_rentedBuffer = ArrayPool<byte>.Shared.Rent(_initialCapacity);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -87,14 +90,29 @@ public struct ArrayBinaryOutput : IBinaryOutputBase, IDisposable
|
|||
var resultBuffer = buffer;
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>Resets for reuse — nothing to do, context handles position via Initialize.</summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Reset() { }
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </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
|
||||
|
||||
|
|
|
|||
|
|
@ -39,4 +39,12 @@ public interface IBinaryOutputBase
|
|||
/// For BufferWriterBinaryOutput: returns committedBytes + (currentPosition - chunkStart).
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in New Issue