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; _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)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteVarInt(int value) public void WriteVarInt(int value)
{ {
@ -651,28 +661,23 @@ public static partial class AcBinarySerializer
var charLength = value.Length; var charLength = value.Length;
// Speculative ASCII fast path: assume byteCount == charLength // Pre-allocate VarUInt + ASCII body BEFORE savedPosition — if Grow happens,
// Single-pass Ascii.FromUtf16 (scan+copy combined) instead of // it fires here, before the save. savedPosition is always in the current chunk.
// Ascii.IsValid (scan) + Ascii.FromUtf16 (scan+copy) = double traversal EnsureCapacity(VarUIntSize((uint)charLength) + charLength);
var savedPosition = _position; var savedPosition = _position;
WriteVarUInt((uint)charLength); WriteVarUIntUnsafe((uint)charLength);
EnsureCapacity(charLength);
if (Ascii.FromUtf16(value.AsSpan(), _buffer.AsSpan(_position, charLength), out _) == OperationStatus.Done) if (Ascii.FromUtf16(value.AsSpan(), _buffer.AsSpan(_position, charLength), out _) == OperationStatus.Done)
{ {
_position += charLength; _position += charLength;
return; return;
} }
// Non-ASCII fallback: rewind VarUInt, encode with UTF-8 // Non-ASCII fallback: safe rewind (no Grow happened since pre-allocate)
// FromUtf16 fails fast on first non-ASCII char, so speculative cost is minimal
_position = savedPosition; _position = savedPosition;
var byteCount = Utf8NoBom.GetByteCount(value); var byteCount = Utf8NoBom.GetByteCount(value);
EnsureCapacity(VarUIntSize((uint)byteCount) + byteCount);
WriteVarUInt((uint)byteCount); WriteVarUIntUnsafe((uint)byteCount);
EnsureCapacity(byteCount);
Utf8NoBom.GetBytes(value.AsSpan(), _buffer.AsSpan(_position, byteCount)); Utf8NoBom.GetBytes(value.AsSpan(), _buffer.AsSpan(_position, byteCount));
_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) // 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(4096); context.Output = new ArrayBinaryOutput(options.BufferWriterChunkSize);
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

@ -128,7 +128,7 @@ public sealed class AcBinarySerializerOptions : AcSerializerOptions
/// Initial capacity for serialization buffer. /// Initial capacity for serialization buffer.
/// Default: 4096 bytes /// Default: 4096 bytes
/// </summary> /// </summary>
public int InitialBufferCapacity { get; init; } = 4096; public int InitialBufferCapacity { get; init; } = 4096;d
/// <summary> /// <summary>
/// Chunk size (in bytes) used by <see cref="BufferWriterBinaryOutput"/> when writing to an <see cref="System.Buffers.IBufferWriter{T}"/>. /// 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 readonly int _initialCapacity;
private byte[] _rentedBuffer; private byte[] _rentedBuffer;
public ArrayBinaryOutput(int initialCapacity = 4096) public ArrayBinaryOutput(int initialCapacity = 65535)
{ {
_initialCapacity = Math.Max(initialCapacity, DefaultMinBufferSize); _initialCapacity = Math.Max(initialCapacity, DefaultMinBufferSize);
_rentedBuffer = ArrayPool<byte>.Shared.Rent(_initialCapacity); _rentedBuffer = ArrayPool<byte>.Shared.Rent(_initialCapacity);

View File

@ -206,6 +206,24 @@ public struct BufferWriterBinaryOutput : IBinaryOutputBase
return; return;
} }
StandaloneEnsureCapacity(5); 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) while (value >= 0x80)
{ {
_buffer[_position++] = (byte)(value | 0x80); _buffer[_position++] = (byte)(value | 0x80);
@ -214,26 +232,37 @@ public struct BufferWriterBinaryOutput : IBinaryOutputBase
_buffer[_position++] = (byte)value; _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) public void WriteStringUtf8(string value)
{ {
var charLength = value.Length; 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; var savedPosition = _position;
WriteVarUInt((uint)charLength);
StandaloneEnsureCapacity(charLength);
WriteVarUIntUnsafe((uint)charLength);
if (Ascii.FromUtf16(value.AsSpan(), _buffer.AsSpan(_position, charLength), out _) == OperationStatus.Done) if (Ascii.FromUtf16(value.AsSpan(), _buffer.AsSpan(_position, charLength), out _) == OperationStatus.Done)
{ {
_position += charLength; _position += charLength;
return; return;
} }
// Non-ASCII fallback: rewind VarUInt, encode with UTF-8 // Non-ASCII fallback: safe rewind (no Grow happened since pre-allocate)
_position = savedPosition; _position = savedPosition;
var byteCount = Utf8NoBom.GetByteCount(value); var byteCount = Utf8NoBom.GetByteCount(value);
WriteVarUInt((uint)byteCount); StandaloneEnsureCapacity(VarUIntSize((uint)byteCount) + byteCount);
StandaloneEnsureCapacity(byteCount); WriteVarUIntUnsafe((uint)byteCount);
Utf8NoBom.GetBytes(value.AsSpan(), _buffer.AsSpan(_position, byteCount)); Utf8NoBom.GetBytes(value.AsSpan(), _buffer.AsSpan(_position, byteCount));
_position += byteCount; _position += byteCount;
} }

View File

@ -54,7 +54,7 @@ namespace AyCode.Services.SignalRs
.ConfigureLogging(logging => .ConfigureLogging(logging =>
{ {
// alap minimális MS log level // 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 // regisztráljuk az AcLoggerProvider-t úgy, hogy visszaadja a meglévő Logger példányt
logging.AddAcLogger(_ => Logger); logging.AddAcLogger(_ => Logger);