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);