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:
Loretta 2026-02-21 19:41:37 +01:00
parent 19b15554cf
commit 5ebcd03e87
3 changed files with 181 additions and 5 deletions

4
.github/copilot-instructions.md vendored Normal file
View File

@ -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.

View File

@ -341,6 +341,16 @@ public static partial class AcBinarySerializer
Output.Grow(ref _buffer, ref _position, ref _bufferEnd, additionalBytes); 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)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteByte(byte value) public void WriteByte(byte value)
{ {
@ -349,10 +359,24 @@ public static partial class AcBinarySerializer
_buffer[_position++] = value; _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)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteTwoBytes(byte b1, byte b2) public void WriteTwoBytes(byte b1, byte b2)
{ {
EnsureCapacity(2); 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++] = b1;
_buffer[_position++] = b2; _buffer[_position++] = b2;
} }
@ -361,6 +385,13 @@ public static partial class AcBinarySerializer
public void WriteBytes(ReadOnlySpan<byte> data) public void WriteBytes(ReadOnlySpan<byte> data)
{ {
EnsureCapacity(data.Length); 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)); data.CopyTo(_buffer.AsSpan(_position));
_position += data.Length; _position += data.Length;
} }
@ -368,17 +399,29 @@ public static partial class AcBinarySerializer
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteRaw<T>(T value) where T : unmanaged public void WriteRaw<T>(T value) where T : unmanaged
{ {
var size = Unsafe.SizeOf<T>(); EnsureCapacity(Unsafe.SizeOf<T>());
EnsureCapacity(size); 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); Unsafe.WriteUnaligned(ref _buffer[_position], value);
_position += size; _position += Unsafe.SizeOf<T>();
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteTypeCodeAndRaw<T>(byte typeCode, T value) where T : unmanaged public void WriteTypeCodeAndRaw<T>(byte typeCode, T value) where T : unmanaged
{ {
var size = 1 + Unsafe.SizeOf<T>(); EnsureCapacity(1 + Unsafe.SizeOf<T>());
EnsureCapacity(size); 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; _buffer[_position++] = typeCode;
Unsafe.WriteUnaligned(ref _buffer[_position], value); Unsafe.WriteUnaligned(ref _buffer[_position], value);
_position += Unsafe.SizeOf<T>(); _position += Unsafe.SizeOf<T>();
@ -400,6 +443,24 @@ public static partial class AcBinarySerializer
return; return;
} }
EnsureCapacity(5); 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) while (value >= 0x80)
{ {
_buffer[_position++] = (byte)(value | 0x80); _buffer[_position++] = (byte)(value | 0x80);
@ -416,6 +477,14 @@ public static partial class AcBinarySerializer
WriteVarUInt(encoded); 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)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteVarULong(ulong value) public void WriteVarULong(ulong value)
{ {
@ -428,6 +497,24 @@ public static partial class AcBinarySerializer
return; return;
} }
EnsureCapacity(10); 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) while (value >= 0x80)
{ {
_buffer[_position++] = (byte)(value | 0x80); _buffer[_position++] = (byte)(value | 0x80);
@ -444,6 +531,14 @@ public static partial class AcBinarySerializer
WriteVarULong(encoded); 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 #endregion
#region Specialized Types inline #region Specialized Types inline
@ -452,6 +547,13 @@ public static partial class AcBinarySerializer
public void WriteDecimalBits(decimal value) public void WriteDecimalBits(decimal value)
{ {
EnsureCapacity(16); 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]; Span<int> bits = stackalloc int[4];
decimal.TryGetBits(value, bits, out _); decimal.TryGetBits(value, bits, out _);
MemoryMarshal.AsBytes(bits).CopyTo(_buffer.AsSpan(_position, 16)); MemoryMarshal.AsBytes(bits).CopyTo(_buffer.AsSpan(_position, 16));
@ -462,6 +564,13 @@ public static partial class AcBinarySerializer
public void WriteDateTimeBits(DateTime value) public void WriteDateTimeBits(DateTime value)
{ {
EnsureCapacity(9); 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); Unsafe.WriteUnaligned(ref _buffer[_position], value.Ticks);
_buffer[_position + 8] = (byte)value.Kind; _buffer[_position + 8] = (byte)value.Kind;
_position += 9; _position += 9;
@ -471,6 +580,13 @@ public static partial class AcBinarySerializer
public void WriteGuidBits(Guid value) public void WriteGuidBits(Guid value)
{ {
EnsureCapacity(16); 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)); value.TryWriteBytes(_buffer.AsSpan(_position, 16));
_position += 16; _position += 16;
} }
@ -479,6 +595,13 @@ public static partial class AcBinarySerializer
public void WriteDateTimeOffsetBits(DateTimeOffset value) public void WriteDateTimeOffsetBits(DateTimeOffset value)
{ {
EnsureCapacity(10); 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], value.UtcTicks);
Unsafe.WriteUnaligned(ref _buffer[_position + 8], (short)value.Offset.TotalMinutes); Unsafe.WriteUnaligned(ref _buffer[_position + 8], (short)value.Offset.TotalMinutes);
_position += 10; _position += 10;

View File

@ -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) public BinarySerializeTypeMetadata(Type type, Func<PropertyInfo, bool> ignorePropertyFilter) : base(type,ignorePropertyFilter)
{ {
// Use pre-computed WritableProperties directly - no method call overhead! // Use pre-computed WritableProperties directly - no method call overhead!
@ -130,6 +138,7 @@ public static partial class AcBinarySerializer
Properties = new BinaryPropertyAccessor[orderedProperties.Length]; Properties = new BinaryPropertyAccessor[orderedProperties.Length];
var complexCount = 0; var complexCount = 0;
var minWriteSize = 0;
for (var i = 0; i < orderedProperties.Length; i++) for (var i = 0; i < orderedProperties.Length; i++)
{ {
@ -142,8 +151,13 @@ public static partial class AcBinarySerializer
{ {
accessor.ComplexPropertyIndex = complexCount++; accessor.ComplexPropertyIndex = complexCount++;
} }
// Accumulate min write size per property
minWriteSize += ComputePropertyMaxWriteSize(accessor);
} }
MinWriteSize = minWriteSize;
// Set scan optimization flags // Set scan optimization flags
ComplexPropertyCount = complexCount; ComplexPropertyCount = complexCount;
HasComplexProperties = complexCount > 0; 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) private static Type? FindGeneratedSerializerType(Type type)
{ {
var generatedTypeName = $"{type.FullName}_AcBinarySerializer"; var generatedTypeName = $"{type.FullName}_AcBinarySerializer";