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.
This commit is contained in:
Loretta 2026-04-08 09:50:46 +02:00
parent cfc18d9c8e
commit 55e53c248f
6 changed files with 56 additions and 22 deletions

View File

@ -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;
}

View File

@ -80,7 +80,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(4096);
context.Output = new ArrayBinaryOutput(options.BufferWriterChunkSize);
context.OutputInitialized = true;
context.Output.Initialize(out context._buffer, out context._position, out context._bufferEnd);

View File

@ -128,7 +128,7 @@ public sealed class AcBinarySerializerOptions : AcSerializerOptions
/// Initial capacity for serialization buffer.
/// Default: 4096 bytes
/// </summary>
public int InitialBufferCapacity { get; init; } = 4096;
public int InitialBufferCapacity { get; init; } = 4096;d
/// <summary>
/// Chunk size (in bytes) used by <see cref="BufferWriterBinaryOutput"/> when writing to an <see cref="System.Buffers.IBufferWriter{T}"/>.

View File

@ -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<byte>.Shared.Rent(_initialCapacity);

View File

@ -206,6 +206,24 @@ public struct BufferWriterBinaryOutput : IBinaryOutputBase
return;
}
StandaloneEnsureCapacity(5);
WriteVarUIntMultiByteUnsafe(value);
}
/// <summary>Writes VarUInt without bounds check. Caller must ensure sufficient space.</summary>
[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;
}

View File

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