From 55e53c248f9731dac17918e4a4727458ed8c874a Mon Sep 17 00:00:00 2001 From: Loretta Date: Wed, 8 Apr 2026 09:50:46 +0200 Subject: [PATCH] Improve string serialization and buffer preallocation - Add VarUIntSize and unsafe VarUInt writers for efficient buffer sizing and writing without redundant checks. - Update WriteStringUtf8 to preallocate for VarUInt and string body in one step, reducing reallocations and risk of overflow. - Change ArrayBinaryOutput default initial capacity to 65535. - Use BufferWriterChunkSize from options in AcBinarySerializer. - Fix typo in AcBinarySerializerOptions. - Set SignalR client log level to Warning by default. --- ...rySerializer.BinarySerializationContext.cs | 29 +++++++------ .../Binaries/AcBinarySerializer.cs | 2 +- .../Binaries/AcBinarySerializerOptions.cs | 2 +- .../Serializers/Binaries/ArrayBinaryOutput.cs | 2 +- .../Binaries/BufferWriterBinaryOutput.cs | 41 ++++++++++++++++--- .../SignalRs/AcSignalRClientBase.cs | 2 +- 6 files changed, 56 insertions(+), 22 deletions(-) diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs index 3497ee0..0d0c549 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs @@ -493,6 +493,16 @@ public static partial class AcBinarySerializer _buffer[_position++] = (byte)value; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int VarUIntSize(uint value) + { + if (value < 0x80) return 1; + if (value < 0x4000) return 2; + if (value < 0x200000) return 3; + if (value < 0x10000000) return 4; + return 5; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void WriteVarInt(int value) { @@ -651,28 +661,23 @@ public static partial class AcBinarySerializer 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 + // Pre-allocate VarUInt + ASCII body BEFORE savedPosition — if Grow happens, + // it fires here, before the save. savedPosition is always in the current chunk. + EnsureCapacity(VarUIntSize((uint)charLength) + charLength); var savedPosition = _position; - WriteVarUInt((uint)charLength); - EnsureCapacity(charLength); - + WriteVarUIntUnsafe((uint)charLength); if (Ascii.FromUtf16(value.AsSpan(), _buffer.AsSpan(_position, charLength), out _) == OperationStatus.Done) { _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 + // Non-ASCII fallback: safe rewind (no Grow happened since pre-allocate) _position = savedPosition; var byteCount = Utf8NoBom.GetByteCount(value); - - WriteVarUInt((uint)byteCount); - EnsureCapacity(byteCount); - + EnsureCapacity(VarUIntSize((uint)byteCount) + byteCount); + WriteVarUIntUnsafe((uint)byteCount); Utf8NoBom.GetBytes(value.AsSpan(), _buffer.AsSpan(_position, byteCount)); _position += byteCount; } diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs index 59f69c1..98a7c86 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs @@ -80,7 +80,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(4096); + context.Output = new ArrayBinaryOutput(options.BufferWriterChunkSize); context.OutputInitialized = true; context.Output.Initialize(out context._buffer, out context._position, out context._bufferEnd); diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs index 18f8a2e..dacc3db 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs @@ -128,7 +128,7 @@ public sealed class AcBinarySerializerOptions : AcSerializerOptions /// Initial capacity for serialization buffer. /// Default: 4096 bytes /// - public int InitialBufferCapacity { get; init; } = 4096; + public int InitialBufferCapacity { get; init; } = 4096;d /// /// Chunk size (in bytes) used by when writing to an . diff --git a/AyCode.Core/Serializers/Binaries/ArrayBinaryOutput.cs b/AyCode.Core/Serializers/Binaries/ArrayBinaryOutput.cs index db9684b..9a81058 100644 --- a/AyCode.Core/Serializers/Binaries/ArrayBinaryOutput.cs +++ b/AyCode.Core/Serializers/Binaries/ArrayBinaryOutput.cs @@ -19,7 +19,7 @@ public struct ArrayBinaryOutput : IBinaryOutputBase, IDisposable private readonly int _initialCapacity; private byte[] _rentedBuffer; - public ArrayBinaryOutput(int initialCapacity = 4096) + public ArrayBinaryOutput(int initialCapacity = 65535) { _initialCapacity = Math.Max(initialCapacity, DefaultMinBufferSize); _rentedBuffer = ArrayPool.Shared.Rent(_initialCapacity); diff --git a/AyCode.Core/Serializers/Binaries/BufferWriterBinaryOutput.cs b/AyCode.Core/Serializers/Binaries/BufferWriterBinaryOutput.cs index cbc44e7..14c9b34 100644 --- a/AyCode.Core/Serializers/Binaries/BufferWriterBinaryOutput.cs +++ b/AyCode.Core/Serializers/Binaries/BufferWriterBinaryOutput.cs @@ -206,6 +206,24 @@ public struct BufferWriterBinaryOutput : IBinaryOutputBase return; } StandaloneEnsureCapacity(5); + WriteVarUIntMultiByteUnsafe(value); + } + + /// Writes VarUInt without bounds check. Caller must ensure sufficient space. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void WriteVarUIntUnsafe(uint value) + { + if (value < 0x80) + { + _buffer[_position++] = (byte)value; + return; + } + WriteVarUIntMultiByteUnsafe(value); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void WriteVarUIntMultiByteUnsafe(uint value) + { while (value >= 0x80) { _buffer[_position++] = (byte)(value | 0x80); @@ -214,26 +232,37 @@ public struct BufferWriterBinaryOutput : IBinaryOutputBase _buffer[_position++] = (byte)value; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int VarUIntSize(uint value) + { + if (value < 0x80) return 1; + if (value < 0x4000) return 2; + if (value < 0x200000) return 3; + if (value < 0x10000000) return 4; + return 5; + } + public void WriteStringUtf8(string value) { var charLength = value.Length; - // Speculative ASCII fast path: single-pass Ascii.FromUtf16 + // Pre-allocate VarUInt + ASCII body BEFORE savedPosition — if Grow happens, + // it fires here, before the save. savedPosition is always in the current chunk. + StandaloneEnsureCapacity(VarUIntSize((uint)charLength) + charLength); var savedPosition = _position; - WriteVarUInt((uint)charLength); - StandaloneEnsureCapacity(charLength); + WriteVarUIntUnsafe((uint)charLength); if (Ascii.FromUtf16(value.AsSpan(), _buffer.AsSpan(_position, charLength), out _) == OperationStatus.Done) { _position += charLength; return; } - // Non-ASCII fallback: rewind VarUInt, encode with UTF-8 + // Non-ASCII fallback: safe rewind (no Grow happened since pre-allocate) _position = savedPosition; var byteCount = Utf8NoBom.GetByteCount(value); - WriteVarUInt((uint)byteCount); - StandaloneEnsureCapacity(byteCount); + StandaloneEnsureCapacity(VarUIntSize((uint)byteCount) + byteCount); + WriteVarUIntUnsafe((uint)byteCount); Utf8NoBom.GetBytes(value.AsSpan(), _buffer.AsSpan(_position, byteCount)); _position += byteCount; } diff --git a/AyCode.Services/SignalRs/AcSignalRClientBase.cs b/AyCode.Services/SignalRs/AcSignalRClientBase.cs index 1c2ce6a..ebd6f30 100644 --- a/AyCode.Services/SignalRs/AcSignalRClientBase.cs +++ b/AyCode.Services/SignalRs/AcSignalRClientBase.cs @@ -54,7 +54,7 @@ namespace AyCode.Services.SignalRs .ConfigureLogging(logging => { // alap minimális MS log level - logging.SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Debug); + logging.SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Warning); // regisztráljuk az AcLoggerProvider-t úgy, hogy visszaadja a meglévő Logger példányt logging.AddAcLogger(_ => Logger);