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:
parent
cfc18d9c8e
commit
55e53c248f
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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}"/>.
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in New Issue