diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..074151b --- /dev/null +++ b/.github/copilot-instructions.md @@ -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. \ No newline at end of file diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs index c36ca8f..d703e2d 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs @@ -341,6 +341,16 @@ public static partial class AcBinarySerializer Output.Grow(ref _buffer, ref _position, ref _bufferEnd, additionalBytes); } + /// + /// Ensures the buffer has enough space for the specified number of bytes. + /// Called before property writes to avoid mid-object Grow() calls. + /// + [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; } + /// Writes a single byte without capacity check. Caller must ensure buffer space. + [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); + } + + /// Writes two bytes without capacity check. Caller must ensure buffer space. + [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 data) { EnsureCapacity(data.Length); + WriteBytesUnsafe(data); + } + + /// Writes a span of bytes without capacity check. Caller must ensure buffer space. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteBytesUnsafe(ReadOnlySpan 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 value) where T : unmanaged { - var size = Unsafe.SizeOf(); - EnsureCapacity(size); + EnsureCapacity(Unsafe.SizeOf()); + WriteRawUnsafe(value); + } + + /// Writes an unmanaged value without capacity check. Caller must ensure buffer space. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteRawUnsafe(T value) where T : unmanaged + { Unsafe.WriteUnaligned(ref _buffer[_position], value); - _position += size; + _position += Unsafe.SizeOf(); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void WriteTypeCodeAndRaw(byte typeCode, T value) where T : unmanaged { - var size = 1 + Unsafe.SizeOf(); - EnsureCapacity(size); + EnsureCapacity(1 + Unsafe.SizeOf()); + WriteTypeCodeAndRawUnsafe(typeCode, value); + } + + /// Writes a type code + unmanaged value without capacity check. Caller must ensure buffer space. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteTypeCodeAndRawUnsafe(byte typeCode, T value) where T : unmanaged + { _buffer[_position++] = typeCode; Unsafe.WriteUnaligned(ref _buffer[_position], value); _position += Unsafe.SizeOf(); @@ -400,6 +443,24 @@ public static partial class AcBinarySerializer return; } EnsureCapacity(5); + WriteVarUIntMultiByteUnsafe(value); + } + + /// Writes a VarUInt without capacity check. Caller must ensure at least 5 bytes of buffer space. + [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); } + /// Writes a zigzag-encoded VarInt without capacity check. Caller must ensure at least 5 bytes of buffer space. + [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); + } + + /// Writes a VarULong without capacity check. Caller must ensure at least 10 bytes of buffer space. + [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); } + /// Writes a zigzag-encoded VarLong without capacity check. Caller must ensure at least 10 bytes of buffer space. + [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); + } + + /// Writes 16-byte decimal bits without capacity check. Caller must ensure buffer space. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteDecimalBitsUnsafe(decimal value) + { Span 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); + } + + /// Writes 9-byte DateTime (Ticks + Kind) without capacity check. Caller must ensure buffer space. + [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); + } + + /// Writes 16-byte Guid without capacity check. Caller must ensure buffer space. + [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); + } + + /// Writes 10-byte DateTimeOffset (UtcTicks + Offset) without capacity check. Caller must ensure buffer space. + [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; diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializeTypeMetadata.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializeTypeMetadata.cs index 8b12614..62fc178 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializeTypeMetadata.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializeTypeMetadata.cs @@ -118,6 +118,14 @@ public static partial class AcBinarySerializer } + /// + /// 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(). + /// + public int MinWriteSize { get; } + public BinarySerializeTypeMetadata(Type type, Func 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 } } + /// + /// 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). + /// + [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 + }; + + /// + /// 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). + /// + private static int ComputePropertyMaxWriteSize(BinaryPropertyAccessor accessor) + => GetMarkeredMaxSize(accessor.AccessorType); + private static Type? FindGeneratedSerializerType(Type type) { var generatedTypeName = $"{type.FullName}_AcBinarySerializer";