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;
|
_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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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}"/>.
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue