Add unsafe write methods and MinWriteSize for serialization
Added capacity-check-free "unsafe" write methods to BinarySerializationContext for all primitive and specialized types, plus ReserveCapacity for bulk writes. Introduced MinWriteSize property in BinarySerializeTypeMetadata to precompute worst-case buffer requirements per type. Improved documentation and added Copilot instructions to discourage code removal as a solution.
This commit is contained in:
parent
19b15554cf
commit
5ebcd03e87
|
|
@ -0,0 +1,4 @@
|
|||
# Copilot Instructions
|
||||
|
||||
## Project Guidelines
|
||||
- Ne ajánlj visszalépést/eltávolítást megoldásként — keress megoldást a problémára.
|
||||
|
|
@ -341,6 +341,16 @@ public static partial class AcBinarySerializer
|
|||
Output.Grow(ref _buffer, ref _position, ref _bufferEnd, additionalBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures the buffer has enough space for the specified number of bytes.
|
||||
/// Called before property writes to avoid mid-object Grow() calls.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void ReserveCapacity(int bytes)
|
||||
{
|
||||
EnsureCapacity(bytes);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteByte(byte value)
|
||||
{
|
||||
|
|
@ -349,10 +359,24 @@ public static partial class AcBinarySerializer
|
|||
_buffer[_position++] = value;
|
||||
}
|
||||
|
||||
/// <summary>Writes a single byte without capacity check. Caller must ensure buffer space.</summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteByteUnsafe(byte value)
|
||||
{
|
||||
_buffer[_position++] = value;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteTwoBytes(byte b1, byte b2)
|
||||
{
|
||||
EnsureCapacity(2);
|
||||
WriteTwoBytesUnsafe(b1, b2);
|
||||
}
|
||||
|
||||
/// <summary>Writes two bytes without capacity check. Caller must ensure buffer space.</summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteTwoBytesUnsafe(byte b1, byte b2)
|
||||
{
|
||||
_buffer[_position++] = b1;
|
||||
_buffer[_position++] = b2;
|
||||
}
|
||||
|
|
@ -361,6 +385,13 @@ public static partial class AcBinarySerializer
|
|||
public void WriteBytes(ReadOnlySpan<byte> data)
|
||||
{
|
||||
EnsureCapacity(data.Length);
|
||||
WriteBytesUnsafe(data);
|
||||
}
|
||||
|
||||
/// <summary>Writes a span of bytes without capacity check. Caller must ensure buffer space.</summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteBytesUnsafe(ReadOnlySpan<byte> data)
|
||||
{
|
||||
data.CopyTo(_buffer.AsSpan(_position));
|
||||
_position += data.Length;
|
||||
}
|
||||
|
|
@ -368,17 +399,29 @@ public static partial class AcBinarySerializer
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteRaw<T>(T value) where T : unmanaged
|
||||
{
|
||||
var size = Unsafe.SizeOf<T>();
|
||||
EnsureCapacity(size);
|
||||
EnsureCapacity(Unsafe.SizeOf<T>());
|
||||
WriteRawUnsafe(value);
|
||||
}
|
||||
|
||||
/// <summary>Writes an unmanaged value without capacity check. Caller must ensure buffer space.</summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteRawUnsafe<T>(T value) where T : unmanaged
|
||||
{
|
||||
Unsafe.WriteUnaligned(ref _buffer[_position], value);
|
||||
_position += size;
|
||||
_position += Unsafe.SizeOf<T>();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteTypeCodeAndRaw<T>(byte typeCode, T value) where T : unmanaged
|
||||
{
|
||||
var size = 1 + Unsafe.SizeOf<T>();
|
||||
EnsureCapacity(size);
|
||||
EnsureCapacity(1 + Unsafe.SizeOf<T>());
|
||||
WriteTypeCodeAndRawUnsafe(typeCode, value);
|
||||
}
|
||||
|
||||
/// <summary>Writes a type code + unmanaged value without capacity check. Caller must ensure buffer space.</summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteTypeCodeAndRawUnsafe<T>(byte typeCode, T value) where T : unmanaged
|
||||
{
|
||||
_buffer[_position++] = typeCode;
|
||||
Unsafe.WriteUnaligned(ref _buffer[_position], value);
|
||||
_position += Unsafe.SizeOf<T>();
|
||||
|
|
@ -400,6 +443,24 @@ public static partial class AcBinarySerializer
|
|||
return;
|
||||
}
|
||||
EnsureCapacity(5);
|
||||
WriteVarUIntMultiByteUnsafe(value);
|
||||
}
|
||||
|
||||
/// <summary>Writes a VarUInt without capacity check. Caller must ensure at least 5 bytes of buffer space.</summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public 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);
|
||||
|
|
@ -416,6 +477,14 @@ public static partial class AcBinarySerializer
|
|||
WriteVarUInt(encoded);
|
||||
}
|
||||
|
||||
/// <summary>Writes a zigzag-encoded VarInt without capacity check. Caller must ensure at least 5 bytes of buffer space.</summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteVarIntUnsafe(int value)
|
||||
{
|
||||
var encoded = (uint)((value << 1) ^ (value >> 31));
|
||||
WriteVarUIntUnsafe(encoded);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteVarULong(ulong value)
|
||||
{
|
||||
|
|
@ -428,6 +497,24 @@ public static partial class AcBinarySerializer
|
|||
return;
|
||||
}
|
||||
EnsureCapacity(10);
|
||||
WriteVarULongMultiByteUnsafe(value);
|
||||
}
|
||||
|
||||
/// <summary>Writes a VarULong without capacity check. Caller must ensure at least 10 bytes of buffer space.</summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteVarULongUnsafe(ulong value)
|
||||
{
|
||||
if (value < 0x80)
|
||||
{
|
||||
_buffer[_position++] = (byte)value;
|
||||
return;
|
||||
}
|
||||
WriteVarULongMultiByteUnsafe(value);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private void WriteVarULongMultiByteUnsafe(ulong value)
|
||||
{
|
||||
while (value >= 0x80)
|
||||
{
|
||||
_buffer[_position++] = (byte)(value | 0x80);
|
||||
|
|
@ -444,6 +531,14 @@ public static partial class AcBinarySerializer
|
|||
WriteVarULong(encoded);
|
||||
}
|
||||
|
||||
/// <summary>Writes a zigzag-encoded VarLong without capacity check. Caller must ensure at least 10 bytes of buffer space.</summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteVarLongUnsafe(long value)
|
||||
{
|
||||
var encoded = (ulong)((value << 1) ^ (value >> 63));
|
||||
WriteVarULongUnsafe(encoded);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Specialized Types — inline
|
||||
|
|
@ -452,6 +547,13 @@ public static partial class AcBinarySerializer
|
|||
public void WriteDecimalBits(decimal value)
|
||||
{
|
||||
EnsureCapacity(16);
|
||||
WriteDecimalBitsUnsafe(value);
|
||||
}
|
||||
|
||||
/// <summary>Writes 16-byte decimal bits without capacity check. Caller must ensure buffer space.</summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteDecimalBitsUnsafe(decimal value)
|
||||
{
|
||||
Span<int> bits = stackalloc int[4];
|
||||
decimal.TryGetBits(value, bits, out _);
|
||||
MemoryMarshal.AsBytes(bits).CopyTo(_buffer.AsSpan(_position, 16));
|
||||
|
|
@ -462,6 +564,13 @@ public static partial class AcBinarySerializer
|
|||
public void WriteDateTimeBits(DateTime value)
|
||||
{
|
||||
EnsureCapacity(9);
|
||||
WriteDateTimeBitsUnsafe(value);
|
||||
}
|
||||
|
||||
/// <summary>Writes 9-byte DateTime (Ticks + Kind) without capacity check. Caller must ensure buffer space.</summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteDateTimeBitsUnsafe(DateTime value)
|
||||
{
|
||||
Unsafe.WriteUnaligned(ref _buffer[_position], value.Ticks);
|
||||
_buffer[_position + 8] = (byte)value.Kind;
|
||||
_position += 9;
|
||||
|
|
@ -471,6 +580,13 @@ public static partial class AcBinarySerializer
|
|||
public void WriteGuidBits(Guid value)
|
||||
{
|
||||
EnsureCapacity(16);
|
||||
WriteGuidBitsUnsafe(value);
|
||||
}
|
||||
|
||||
/// <summary>Writes 16-byte Guid without capacity check. Caller must ensure buffer space.</summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteGuidBitsUnsafe(Guid value)
|
||||
{
|
||||
value.TryWriteBytes(_buffer.AsSpan(_position, 16));
|
||||
_position += 16;
|
||||
}
|
||||
|
|
@ -479,6 +595,13 @@ public static partial class AcBinarySerializer
|
|||
public void WriteDateTimeOffsetBits(DateTimeOffset value)
|
||||
{
|
||||
EnsureCapacity(10);
|
||||
WriteDateTimeOffsetBitsUnsafe(value);
|
||||
}
|
||||
|
||||
/// <summary>Writes 10-byte DateTimeOffset (UtcTicks + Offset) without capacity check. Caller must ensure buffer space.</summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteDateTimeOffsetBitsUnsafe(DateTimeOffset value)
|
||||
{
|
||||
Unsafe.WriteUnaligned(ref _buffer[_position], value.UtcTicks);
|
||||
Unsafe.WriteUnaligned(ref _buffer[_position + 8], (short)value.Offset.TotalMinutes);
|
||||
_position += 10;
|
||||
|
|
|
|||
|
|
@ -118,6 +118,14 @@ public static partial class AcBinarySerializer
|
|||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Pre-computed minimum byte size for writing all properties of this type.
|
||||
/// Markerless properties contribute their max encoded size (e.g. VarInt=5, double=8).
|
||||
/// Markered properties contribute 1 byte (minimum marker: PropertySkip/Null/True/False).
|
||||
/// Used by EnsureCapacity before property writes to avoid mid-object buffer Grow().
|
||||
/// </summary>
|
||||
public int MinWriteSize { get; }
|
||||
|
||||
public BinarySerializeTypeMetadata(Type type, Func<PropertyInfo, bool> ignorePropertyFilter) : base(type,ignorePropertyFilter)
|
||||
{
|
||||
// Use pre-computed WritableProperties directly - no method call overhead!
|
||||
|
|
@ -130,6 +138,7 @@ public static partial class AcBinarySerializer
|
|||
|
||||
Properties = new BinaryPropertyAccessor[orderedProperties.Length];
|
||||
var complexCount = 0;
|
||||
var minWriteSize = 0;
|
||||
|
||||
for (var i = 0; i < orderedProperties.Length; i++)
|
||||
{
|
||||
|
|
@ -142,8 +151,13 @@ public static partial class AcBinarySerializer
|
|||
{
|
||||
accessor.ComplexPropertyIndex = complexCount++;
|
||||
}
|
||||
|
||||
// Accumulate min write size per property
|
||||
minWriteSize += ComputePropertyMaxWriteSize(accessor);
|
||||
}
|
||||
|
||||
MinWriteSize = minWriteSize;
|
||||
|
||||
// Set scan optimization flags
|
||||
ComplexPropertyCount = complexCount;
|
||||
HasComplexProperties = complexCount > 0;
|
||||
|
|
@ -161,6 +175,41 @@ public static partial class AcBinarySerializer
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the worst-case byte count for a markered property write (skip-or-write pattern).
|
||||
/// Most types: type_code(1) + markerless_max. Boolean/Enum are special.
|
||||
/// Used as the single source of truth for MinWriteSize — covers both UseMetadata=true
|
||||
/// (all properties markered) and UseMetadata=false (ExpectedTypeCode properties markerless,
|
||||
/// which is strictly smaller than markered).
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static int GetMarkeredMaxSize(PropertyAccessorType accessorType) => accessorType switch
|
||||
{
|
||||
PropertyAccessorType.Boolean => 1, // PropertySkip or True (single byte)
|
||||
PropertyAccessorType.Enum => 7, // Enum(1) + Int32(1) + VarInt(5)
|
||||
PropertyAccessorType.Int32 => 6, // marker(1) + VarInt(5)
|
||||
PropertyAccessorType.Int64 => 11, // marker(1) + VarLong(10)
|
||||
PropertyAccessorType.Double => 9, // marker(1) + 8
|
||||
PropertyAccessorType.Single => 5, // marker(1) + 4
|
||||
PropertyAccessorType.Decimal => 17, // marker(1) + 16
|
||||
PropertyAccessorType.DateTime => 10, // marker(1) + Ticks(8) + Kind(1)
|
||||
PropertyAccessorType.Guid => 17, // marker(1) + 16
|
||||
PropertyAccessorType.Byte => 2, // marker(1) + 1
|
||||
PropertyAccessorType.Int16 => 3, // marker(1) + 2
|
||||
PropertyAccessorType.UInt16 => 3, // marker(1) + 2
|
||||
PropertyAccessorType.UInt32 => 6, // marker(1) + VarUInt(5)
|
||||
PropertyAccessorType.UInt64 => 11, // marker(1) + VarULong(10)
|
||||
_ => 1 // String/Complex/Collection: safe writes
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Computes the maximum byte size a single property write can consume.
|
||||
/// Always uses markered max — covers both UseMetadata=true (all properties markered via
|
||||
/// WritePropertyOrSkipUnsafe) and UseMetadata=false (markerless, which is strictly ≤ markered).
|
||||
/// </summary>
|
||||
private static int ComputePropertyMaxWriteSize(BinaryPropertyAccessor accessor)
|
||||
=> GetMarkeredMaxSize(accessor.AccessorType);
|
||||
|
||||
private static Type? FindGeneratedSerializerType(Type type)
|
||||
{
|
||||
var generatedTypeName = $"{type.FullName}_AcBinarySerializer";
|
||||
|
|
|
|||
Loading…
Reference in New Issue