AcBinary: Major perf/memory optimizations & new benchmarks

- Zero-allocation hot paths for primitive (de)serialization using MemoryMarshal/Unsafe
- FrozenDictionary-based type dispatch for fast deserialization
- Optimized span-based UTF8 string handling, stackalloc for small strings
- Specialized fast-paths for primitive arrays (int, double, etc.)
- Binary header now uses flag-based format (48+) for metadata/ref handling
- Improved buffer management with ArrayPool and minimum size
- Property access via for-loops for better JIT and less overhead
- SignalR test infra supports full serializer options (WithRef/NoRef)
- Added comprehensive AcBinary vs MessagePack benchmarks (speed, memory, size)
- Added rich HTML benchmark report (benchmark-report.html)
- Updated JsonUtilities for new header detection
- Improved documentation and code comments throughout
This commit is contained in:
Loretta 2025-12-13 01:24:48 +01:00
parent 6faed09f9f
commit f69b14c195
8 changed files with 1666 additions and 235 deletions

View File

@ -1,9 +1,11 @@
using System.Buffers;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Frozen;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using AyCode.Core.Helpers;
using static AyCode.Core.Extensions.JsonUtilities;
@ -34,11 +36,54 @@ public class AcBinaryDeserializationException : Exception
/// - Property name table for fast property resolution
/// - Reference resolution for circular/shared references
/// - Populate/Merge mode support
/// - Optimized with FrozenDictionary for type dispatch
/// - Zero-allocation hot paths using Span and MemoryMarshal
/// </summary>
public static class AcBinaryDeserializer
{
private static readonly ConcurrentDictionary<Type, BinaryDeserializeTypeMetadata> TypeMetadataCache = new();
// Type dispatch table for fast ReadValue
private delegate object? TypeReader(ref BinaryDeserializationContext context, Type targetType, int depth);
private static readonly FrozenDictionary<byte, TypeReader> TypeReaders;
static AcBinaryDeserializer()
{
// Initialize type reader dispatch table
var readers = new Dictionary<byte, TypeReader>
{
[BinaryTypeCode.Null] = static (ref BinaryDeserializationContext _, Type _, int _) => null,
[BinaryTypeCode.True] = static (ref BinaryDeserializationContext _, Type _, int _) => true,
[BinaryTypeCode.False] = static (ref BinaryDeserializationContext _, Type _, int _) => false,
[BinaryTypeCode.Int8] = static (ref BinaryDeserializationContext ctx, Type _, int _) => (sbyte)ctx.ReadByte(),
[BinaryTypeCode.UInt8] = static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadByte(),
[BinaryTypeCode.Int16] = static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadInt16Unsafe(),
[BinaryTypeCode.UInt16] = static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadUInt16Unsafe(),
[BinaryTypeCode.Int32] = static (ref BinaryDeserializationContext ctx, Type type, int _) => ReadInt32Value(ref ctx, type),
[BinaryTypeCode.UInt32] = static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadVarUInt(),
[BinaryTypeCode.Int64] = static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadVarLong(),
[BinaryTypeCode.UInt64] = static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadVarULong(),
[BinaryTypeCode.Float32] = static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadSingleUnsafe(),
[BinaryTypeCode.Float64] = static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadDoubleUnsafe(),
[BinaryTypeCode.Decimal] = static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadDecimalUnsafe(),
[BinaryTypeCode.Char] = static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadCharUnsafe(),
[BinaryTypeCode.String] = static (ref BinaryDeserializationContext ctx, Type _, int _) => ReadAndInternString(ref ctx),
[BinaryTypeCode.StringInterned] = static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.GetInternedString((int)ctx.ReadVarUInt()),
[BinaryTypeCode.StringEmpty] = static (ref BinaryDeserializationContext _, Type _, int _) => string.Empty,
[BinaryTypeCode.DateTime] = static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadDateTimeUnsafe(),
[BinaryTypeCode.DateTimeOffset] = static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadDateTimeOffsetUnsafe(),
[BinaryTypeCode.TimeSpan] = static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadTimeSpanUnsafe(),
[BinaryTypeCode.Guid] = static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadGuidUnsafe(),
[BinaryTypeCode.Enum] = static (ref BinaryDeserializationContext ctx, Type type, int _) => ReadEnumValue(ref ctx, type),
[BinaryTypeCode.Object] = ReadObject,
[BinaryTypeCode.ObjectRef] = static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.GetReferencedObject(ctx.ReadVarInt()),
[BinaryTypeCode.Array] = ReadArray,
[BinaryTypeCode.Dictionary] = ReadDictionary,
[BinaryTypeCode.ByteArray] = static (ref BinaryDeserializationContext ctx, Type _, int _) => ReadByteArray(ref ctx),
};
TypeReaders = readers.ToFrozenDictionary();
}
#region Public API
/// <summary>
@ -205,105 +250,46 @@ public static class AcBinaryDeserializer
#region Value Reading
/// <summary>
/// Optimized value reader using FrozenDictionary dispatch table.
/// </summary>
private static object? ReadValue(ref BinaryDeserializationContext context, Type targetType, int depth)
{
if (context.IsAtEnd) return null;
var typeCode = context.ReadByte();
// Handle null
// Handle null first
if (typeCode == BinaryTypeCode.Null) return null;
// Handle tiny int
// Handle tiny int (most common case for small integers)
if (BinaryTypeCode.IsTinyInt(typeCode))
{
var intValue = BinaryTypeCode.DecodeTinyInt(typeCode);
return ConvertToTargetType(intValue, targetType);
}
// Handle type-specific codes
switch (typeCode)
// Use dispatch table for type-specific reading
if (TypeReaders.TryGetValue(typeCode, out var reader))
{
case BinaryTypeCode.True:
return true;
case BinaryTypeCode.False:
return false;
case BinaryTypeCode.Int8:
return (sbyte)context.ReadByte();
case BinaryTypeCode.UInt8:
return context.ReadByte();
case BinaryTypeCode.Int16:
return context.ReadInt16();
case BinaryTypeCode.UInt16:
return context.ReadUInt16();
case BinaryTypeCode.Int32:
return ReadInt32Value(ref context, targetType);
case BinaryTypeCode.UInt32:
return context.ReadVarUInt();
case BinaryTypeCode.Int64:
return context.ReadVarLong();
case BinaryTypeCode.UInt64:
return context.ReadVarULong();
case BinaryTypeCode.Float32:
return context.ReadSingle();
case BinaryTypeCode.Float64:
return context.ReadDouble();
case BinaryTypeCode.Decimal:
return context.ReadDecimal();
case BinaryTypeCode.Char:
return context.ReadChar();
case BinaryTypeCode.String:
return ReadAndInternString(ref context);
case BinaryTypeCode.StringInterned:
return context.GetInternedString((int)context.ReadVarUInt());
case BinaryTypeCode.StringEmpty:
return string.Empty;
case BinaryTypeCode.DateTime:
return context.ReadDateTime();
case BinaryTypeCode.DateTimeOffset:
return context.ReadDateTimeOffset();
case BinaryTypeCode.TimeSpan:
return context.ReadTimeSpan();
case BinaryTypeCode.Guid:
return context.ReadGuid();
case BinaryTypeCode.Enum:
return ReadEnumValue(ref context, targetType);
case BinaryTypeCode.Object:
return ReadObject(ref context, targetType, depth);
case BinaryTypeCode.ObjectRef:
var refId = context.ReadVarInt();
return context.GetReferencedObject(refId);
case BinaryTypeCode.Array:
return ReadArray(ref context, targetType, depth);
case BinaryTypeCode.Dictionary:
return ReadDictionary(ref context, targetType, depth);
case BinaryTypeCode.ByteArray:
return ReadByteArray(ref context);
default:
throw new AcBinaryDeserializationException(
$"Unknown type code: {typeCode}",
context.Position, targetType);
return reader(ref context, targetType, depth);
}
throw new AcBinaryDeserializationException(
$"Unknown type code: {typeCode}",
context.Position, targetType);
}
/// <summary>
/// Read a string and register it in the intern table for future references.
/// The serializer registers strings that meet MinStringInternLength (default: 4 chars),
/// then subsequent occurrences use StringInterned references.
/// We must register strings in the SAME order as the serializer to maintain index consistency.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static string ReadAndInternString(ref BinaryDeserializationContext context)
{
var length = (int)context.ReadVarUInt();
if (length == 0) return string.Empty;
var str = context.ReadString(length);
var str = context.ReadStringUtf8(length);
// Always register strings that meet the minimum intern length threshold
// This must match the serializer's behavior exactly.
// The serializer checks value.Length (char count), not UTF-8 byte length.
// Default MinStringInternLength is 4.
// IMPORTANT: We register ALL strings >= 4 chars because the serializer does too,
// regardless of whether they will be referenced later via StringInterned.
if (str.Length >= context.MinStringInternLength)
{
context.RegisterInternedString(str);
@ -343,14 +329,6 @@ public static class AcBinaryDeserializer
};
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static string ReadStringValue(ref BinaryDeserializationContext context)
{
var length = (int)context.ReadVarUInt();
if (length == 0) return string.Empty;
return context.ReadString(length);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static object ReadEnumValue(ref BinaryDeserializationContext context, Type targetType)
{
@ -454,14 +432,10 @@ public static class AcBinaryDeserializer
var typeCode = context.ReadByte();
if (typeCode == BinaryTypeCode.String)
{
// CRITICAL FIX: Use ReadAndInternString instead of ReadStringValue
// The serializer's WriteString registers property names in the intern table,
// so we must do the same during deserialization to maintain index consistency.
propertyName = ReadAndInternString(ref context);
}
else if (typeCode == BinaryTypeCode.StringInterned)
{
// Property name was previously interned, look it up
propertyName = context.GetInternedString((int)context.ReadVarUInt());
}
else if (typeCode == BinaryTypeCode.StringEmpty)
@ -518,14 +492,10 @@ public static class AcBinaryDeserializer
var typeCode = context.ReadByte();
if (typeCode == BinaryTypeCode.String)
{
// CRITICAL FIX: Use ReadAndInternString instead of ReadStringValue
// The serializer's WriteString registers property names in the intern table,
// so we must do the same during deserialization to maintain index consistency.
propertyName = ReadAndInternString(ref context);
}
else if (typeCode == BinaryTypeCode.StringInterned)
{
// Property name was previously interned, look it up
propertyName = context.GetInternedString((int)context.ReadVarUInt());
}
else if (typeCode == BinaryTypeCode.StringEmpty)
@ -577,6 +547,9 @@ public static class AcBinaryDeserializer
}
}
/// <summary>
/// Optimized IId collection merge with capacity hints and reduced boxing.
/// </summary>
private static void MergeIIdCollection(ref BinaryDeserializationContext context, IList existingList, BinaryPropertySetterInfo propInfo, int depth)
{
var elementType = propInfo.ElementType!;
@ -589,6 +562,7 @@ public static class AcBinaryDeserializer
try
{
// Build lookup dictionary with capacity hint
Dictionary<object, object>? existingById = null;
if (count > 0)
{
@ -656,8 +630,10 @@ public static class AcBinaryDeserializer
private static void CopyProperties(object source, object target, BinaryDeserializeTypeMetadata metadata)
{
foreach (var prop in metadata.PropertiesArray)
var props = metadata.PropertiesArray;
for (var i = 0; i < props.Length; i++)
{
var prop = props[i];
var value = prop.GetValue(source);
if (value != null)
prop.SetValue(target, value);
@ -676,6 +652,13 @@ public static class AcBinaryDeserializer
var count = (int)context.ReadVarUInt();
var nextDepth = depth + 1;
// Optimized path for primitive arrays
if (targetType.IsArray && count > 0)
{
var result = TryReadPrimitiveArray(ref context, elementType, count);
if (result != null) return result;
}
if (targetType.IsArray)
{
var array = Array.CreateInstance(elementType, count);
@ -716,6 +699,117 @@ public static class AcBinaryDeserializer
return list;
}
/// <summary>
/// Optimized primitive array reader using bulk operations.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Array? TryReadPrimitiveArray(ref BinaryDeserializationContext context, Type elementType, int count)
{
// Int32 array
if (ReferenceEquals(elementType, IntType))
{
var array = new int[count];
for (int i = 0; i < count; i++)
{
var typeCode = context.ReadByte();
if (BinaryTypeCode.IsTinyInt(typeCode))
array[i] = BinaryTypeCode.DecodeTinyInt(typeCode);
else if (typeCode == BinaryTypeCode.Int32)
array[i] = context.ReadVarInt();
else
return null; // Fall back to generic path
}
return array;
}
// Double array
if (ReferenceEquals(elementType, DoubleType))
{
var array = new double[count];
for (int i = 0; i < count; i++)
{
var typeCode = context.ReadByte();
if (typeCode != BinaryTypeCode.Float64) return null;
array[i] = context.ReadDoubleUnsafe();
}
return array;
}
// Long array
if (ReferenceEquals(elementType, LongType))
{
var array = new long[count];
for (int i = 0; i < count; i++)
{
var typeCode = context.ReadByte();
if (BinaryTypeCode.IsTinyInt(typeCode))
array[i] = BinaryTypeCode.DecodeTinyInt(typeCode);
else if (typeCode == BinaryTypeCode.Int32)
array[i] = context.ReadVarInt();
else if (typeCode == BinaryTypeCode.Int64)
array[i] = context.ReadVarLong();
else
return null;
}
return array;
}
// Bool array
if (ReferenceEquals(elementType, BoolType))
{
var array = new bool[count];
for (int i = 0; i < count; i++)
{
var typeCode = context.ReadByte();
if (typeCode == BinaryTypeCode.True) array[i] = true;
else if (typeCode == BinaryTypeCode.False) array[i] = false;
else return null;
}
return array;
}
// Guid array
if (ReferenceEquals(elementType, GuidType))
{
var array = new Guid[count];
for (int i = 0; i < count; i++)
{
var typeCode = context.ReadByte();
if (typeCode != BinaryTypeCode.Guid) return null;
array[i] = context.ReadGuidUnsafe();
}
return array;
}
// Decimal array
if (ReferenceEquals(elementType, DecimalType))
{
var array = new decimal[count];
for (int i = 0; i < count; i++)
{
var typeCode = context.ReadByte();
if (typeCode != BinaryTypeCode.Decimal) return null;
array[i] = context.ReadDecimalUnsafe();
}
return array;
}
// DateTime array
if (ReferenceEquals(elementType, DateTimeType))
{
var array = new DateTime[count];
for (int i = 0; i < count; i++)
{
var typeCode = context.ReadByte();
if (typeCode != BinaryTypeCode.DateTime) return null;
array[i] = context.ReadDateTimeUnsafe();
}
return array;
}
return null;
}
private static void PopulateList(ref BinaryDeserializationContext context, IList targetList, Type listType, int depth)
{
var elementType = GetCollectionElementType(listType) ?? typeof(object);
@ -861,7 +955,6 @@ public static class AcBinaryDeserializer
return;
case BinaryTypeCode.String:
// CRITICAL FIX: Must register string in intern table even when skipping!
// The serializer registered this string, so we must too to keep indices in sync.
SkipAndInternString(ref context);
return;
case BinaryTypeCode.StringInterned:
@ -893,7 +986,6 @@ public static class AcBinaryDeserializer
/// <summary>
/// Skip a string but still register it in the intern table if it meets the length threshold.
/// This is critical for maintaining index consistency with the serializer.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void SkipAndInternString(ref BinaryDeserializationContext context)
@ -901,8 +993,7 @@ public static class AcBinaryDeserializer
var byteLen = (int)context.ReadVarUInt();
if (byteLen == 0) return;
// Read the string to check its char length for interning
var str = context.ReadString(byteLen);
var str = context.ReadStringUtf8(byteLen);
if (str.Length >= context.MinStringInternLength)
{
context.RegisterInternedString(str);
@ -1128,6 +1219,10 @@ public static class AcBinaryDeserializer
#region Deserialization Context
/// <summary>
/// Optimized deserialization context using ref struct for zero allocation.
/// Uses MemoryMarshal for fast primitive reads.
/// </summary>
internal ref struct BinaryDeserializationContext
{
private readonly ReadOnlySpan<byte> _data;
@ -1178,7 +1273,19 @@ public static class AcBinaryDeserializer
FormatVersion = ReadByte();
var flags = ReadByte();
HasMetadata = flags == BinaryTypeCode.MetadataHeader;
// Handle new flag-based header format (34+)
if (flags >= BinaryTypeCode.HeaderFlagsBase)
{
HasMetadata = (flags & BinaryTypeCode.HeaderFlag_Metadata) != 0;
HasReferenceHandling = (flags & BinaryTypeCode.HeaderFlag_ReferenceHandling) != 0;
}
else
{
// Legacy format: MetadataHeader (32) or NoMetadataHeader (33)
// These always implied HasReferenceHandling = true
HasMetadata = flags == BinaryTypeCode.MetadataHeader;
HasReferenceHandling = true;
}
if (HasMetadata)
{
@ -1190,10 +1297,9 @@ public static class AcBinaryDeserializer
for (int i = 0; i < propCount; i++)
{
var len = (int)ReadVarUInt();
_propertyNames[i] = ReadString(len);
_propertyNames[i] = ReadStringUtf8(len);
}
}
// Note: Interned strings are built dynamically during deserialization
}
}
@ -1285,107 +1391,134 @@ public static class AcBinaryDeserializer
return result;
}
/// <summary>
/// Optimized Int16 read using direct memory access.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public short ReadInt16()
public short ReadInt16Unsafe()
{
if (_position + 2 > _data.Length)
throw new AcBinaryDeserializationException("Unexpected end of data", _position);
var result = BitConverter.ToInt16(_data.Slice(_position, 2));
var result = Unsafe.ReadUnaligned<short>(ref Unsafe.AsRef(in _data[_position]));
_position += 2;
return result;
}
/// <summary>
/// Optimized UInt16 read using direct memory access.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ushort ReadUInt16()
public ushort ReadUInt16Unsafe()
{
if (_position + 2 > _data.Length)
throw new AcBinaryDeserializationException("Unexpected end of data", _position);
var result = BitConverter.ToUInt16(_data.Slice(_position, 2));
var result = Unsafe.ReadUnaligned<ushort>(ref Unsafe.AsRef(in _data[_position]));
_position += 2;
return result;
}
/// <summary>
/// Optimized float read using direct memory access.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public float ReadSingle()
public float ReadSingleUnsafe()
{
if (_position + 4 > _data.Length)
throw new AcBinaryDeserializationException("Unexpected end of data", _position);
var result = BitConverter.ToSingle(_data.Slice(_position, 4));
var result = Unsafe.ReadUnaligned<float>(ref Unsafe.AsRef(in _data[_position]));
_position += 4;
return result;
}
/// <summary>
/// Optimized double read using direct memory access.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public double ReadDouble()
public double ReadDoubleUnsafe()
{
if (_position + 8 > _data.Length)
throw new AcBinaryDeserializationException("Unexpected end of data", _position);
var result = BitConverter.ToDouble(_data.Slice(_position, 8));
var result = Unsafe.ReadUnaligned<double>(ref Unsafe.AsRef(in _data[_position]));
_position += 8;
return result;
}
/// <summary>
/// Optimized decimal read using direct memory copy.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public decimal ReadDecimal()
public decimal ReadDecimalUnsafe()
{
if (_position + 16 > _data.Length)
throw new AcBinaryDeserializationException("Unexpected end of data", _position);
var bits = new int[4];
for (int i = 0; i < 4; i++)
{
bits[i] = BitConverter.ToInt32(_data.Slice(_position + i * 4, 4));
}
Span<int> bits = stackalloc int[4];
MemoryMarshal.Cast<byte, int>(_data.Slice(_position, 16)).CopyTo(bits);
_position += 16;
return new decimal(bits);
}
/// <summary>
/// Optimized char read.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public char ReadChar()
public char ReadCharUnsafe()
{
if (_position + 2 > _data.Length)
throw new AcBinaryDeserializationException("Unexpected end of data", _position);
var result = BitConverter.ToChar(_data.Slice(_position, 2));
var result = Unsafe.ReadUnaligned<char>(ref Unsafe.AsRef(in _data[_position]));
_position += 2;
return result;
}
/// <summary>
/// Optimized DateTime read.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public DateTime ReadDateTime()
public DateTime ReadDateTimeUnsafe()
{
if (_position + 9 > _data.Length)
throw new AcBinaryDeserializationException("Unexpected end of data", _position);
var ticks = BitConverter.ToInt64(_data.Slice(_position, 8));
var ticks = Unsafe.ReadUnaligned<long>(ref Unsafe.AsRef(in _data[_position]));
var kind = (DateTimeKind)_data[_position + 8];
_position += 9;
return new DateTime(ticks, kind);
}
/// <summary>
/// Optimized DateTimeOffset read.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public DateTimeOffset ReadDateTimeOffset()
public DateTimeOffset ReadDateTimeOffsetUnsafe()
{
if (_position + 10 > _data.Length)
throw new AcBinaryDeserializationException("Unexpected end of data", _position);
var utcTicks = BitConverter.ToInt64(_data.Slice(_position, 8));
var offsetMinutes = BitConverter.ToInt16(_data.Slice(_position + 8, 2));
var utcTicks = Unsafe.ReadUnaligned<long>(ref Unsafe.AsRef(in _data[_position]));
var offsetMinutes = Unsafe.ReadUnaligned<short>(ref Unsafe.AsRef(in _data[_position + 8]));
_position += 10;
var offset = TimeSpan.FromMinutes(offsetMinutes);
// We stored UtcTicks, so we need to add offset to get local ticks
var localTicks = utcTicks + offset.Ticks;
return new DateTimeOffset(localTicks, offset);
}
/// <summary>
/// Optimized TimeSpan read.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TimeSpan ReadTimeSpan()
public TimeSpan ReadTimeSpanUnsafe()
{
if (_position + 8 > _data.Length)
throw new AcBinaryDeserializationException("Unexpected end of data", _position);
var ticks = BitConverter.ToInt64(_data.Slice(_position, 8));
var ticks = Unsafe.ReadUnaligned<long>(ref Unsafe.AsRef(in _data[_position]));
_position += 8;
return new TimeSpan(ticks);
}
/// <summary>
/// Optimized Guid read.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Guid ReadGuid()
public Guid ReadGuidUnsafe()
{
if (_position + 16 > _data.Length)
throw new AcBinaryDeserializationException("Unexpected end of data", _position);
@ -1394,8 +1527,11 @@ public static class AcBinaryDeserializer
return result;
}
/// <summary>
/// Optimized string read using UTF8 span decoding.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public string ReadString(int byteCount)
public string ReadStringUtf8(int byteCount)
{
if (_position + byteCount > _data.Length)
throw new AcBinaryDeserializationException("Unexpected end of data", _position);

View File

@ -19,11 +19,16 @@ namespace AyCode.Core.Extensions;
/// - Property name table for fast lookup
/// - Reference handling for circular/shared references
/// - Optional metadata for schema evolution
/// - Optimized buffer management with ArrayPool
/// - Zero-allocation hot paths using Span and MemoryMarshal
/// </summary>
public static class AcBinarySerializer
{
private static readonly ConcurrentDictionary<Type, BinaryTypeMetadata> TypeMetadataCache = new();
// Pre-computed UTF8 encoder for string operations
private static readonly Encoding Utf8NoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
#region Public API
/// <summary>
@ -220,7 +225,7 @@ public static class AcBinarySerializer
// Handle collections/arrays
if (value is IEnumerable enumerable && !ReferenceEquals(type, StringType))
{
WriteArray(enumerable, context, depth);
WriteArray(enumerable, type, context, depth);
return;
}
@ -228,12 +233,16 @@ public static class AcBinarySerializer
WriteObject(value, type, context, depth);
}
/// <summary>
/// Optimized primitive writer using TypeCode dispatch.
/// Avoids Nullable.GetUnderlyingType in hot path by pre-computing in metadata.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool TryWritePrimitive(object value, Type type, BinarySerializationContext context)
{
var underlyingType = Nullable.GetUnderlyingType(type) ?? type;
var typeCode = Type.GetTypeCode(underlyingType);
// Fast path: check TypeCode first (handles most primitives)
var typeCode = Type.GetTypeCode(type);
switch (typeCode)
{
case TypeCode.Int32:
@ -246,29 +255,29 @@ public static class AcBinarySerializer
context.WriteByte((bool)value ? BinaryTypeCode.True : BinaryTypeCode.False);
return true;
case TypeCode.Double:
WriteFloat64((double)value, context);
WriteFloat64Unsafe((double)value, context);
return true;
case TypeCode.String:
WriteString((string)value, context);
return true;
case TypeCode.Single:
WriteFloat32((float)value, context);
WriteFloat32Unsafe((float)value, context);
return true;
case TypeCode.Decimal:
WriteDecimal((decimal)value, context);
WriteDecimalUnsafe((decimal)value, context);
return true;
case TypeCode.DateTime:
WriteDateTime((DateTime)value, context);
WriteDateTimeUnsafe((DateTime)value, context);
return true;
case TypeCode.Byte:
context.WriteByte(BinaryTypeCode.UInt8);
context.WriteByte((byte)value);
return true;
case TypeCode.Int16:
WriteInt16((short)value, context);
WriteInt16Unsafe((short)value, context);
return true;
case TypeCode.UInt16:
WriteUInt16((ushort)value, context);
WriteUInt16Unsafe((ushort)value, context);
return true;
case TypeCode.UInt32:
WriteUInt32((uint)value, context);
@ -281,26 +290,34 @@ public static class AcBinarySerializer
context.WriteByte(unchecked((byte)(sbyte)value));
return true;
case TypeCode.Char:
WriteChar((char)value, context);
WriteCharUnsafe((char)value, context);
return true;
}
if (ReferenceEquals(underlyingType, GuidType))
// Handle nullable types
var underlyingType = Nullable.GetUnderlyingType(type);
if (underlyingType != null)
{
WriteGuid((Guid)value, context);
return TryWritePrimitive(value, underlyingType, context);
}
// Handle special types by reference comparison (faster than type equality)
if (ReferenceEquals(type, GuidType))
{
WriteGuidUnsafe((Guid)value, context);
return true;
}
if (ReferenceEquals(underlyingType, DateTimeOffsetType))
if (ReferenceEquals(type, DateTimeOffsetType))
{
WriteDateTimeOffset((DateTimeOffset)value, context);
WriteDateTimeOffsetUnsafe((DateTimeOffset)value, context);
return true;
}
if (ReferenceEquals(underlyingType, TimeSpanType))
if (ReferenceEquals(type, TimeSpanType))
{
WriteTimeSpan((TimeSpan)value, context);
WriteTimeSpanUnsafe((TimeSpan)value, context);
return true;
}
if (underlyingType.IsEnum)
if (type.IsEnum)
{
WriteEnum(value, context);
return true;
@ -311,7 +328,7 @@ public static class AcBinarySerializer
#endregion
#region Primitive Writers
#region Optimized Primitive Writers using MemoryMarshal
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void WriteInt32(int value, BinarySerializationContext context)
@ -337,91 +354,88 @@ public static class AcBinarySerializer
context.WriteVarLong(value);
}
/// <summary>
/// Optimized float64 writer using direct memory copy.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void WriteFloat64(double value, BinarySerializationContext context)
private static void WriteFloat64Unsafe(double value, BinarySerializationContext context)
{
context.WriteByte(BinaryTypeCode.Float64);
Span<byte> buffer = stackalloc byte[8];
BitConverter.TryWriteBytes(buffer, value);
context.WriteBytes(buffer);
context.WriteRaw(value);
}
/// <summary>
/// Optimized float32 writer using direct memory copy.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void WriteFloat32(float value, BinarySerializationContext context)
private static void WriteFloat32Unsafe(float value, BinarySerializationContext context)
{
context.WriteByte(BinaryTypeCode.Float32);
Span<byte> buffer = stackalloc byte[4];
BitConverter.TryWriteBytes(buffer, value);
context.WriteBytes(buffer);
context.WriteRaw(value);
}
/// <summary>
/// Optimized decimal writer using direct memory copy of bits.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void WriteDecimal(decimal value, BinarySerializationContext context)
private static void WriteDecimalUnsafe(decimal value, BinarySerializationContext context)
{
context.WriteByte(BinaryTypeCode.Decimal);
var bits = decimal.GetBits(value);
Span<byte> buffer = stackalloc byte[16];
for (int i = 0; i < 4; i++)
{
BitConverter.TryWriteBytes(buffer[(i * 4)..], bits[i]);
}
context.WriteBytes(buffer);
context.WriteDecimalBits(value);
}
/// <summary>
/// Optimized DateTime writer.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void WriteDateTime(DateTime value, BinarySerializationContext context)
private static void WriteDateTimeUnsafe(DateTime value, BinarySerializationContext context)
{
context.WriteByte(BinaryTypeCode.DateTime);
Span<byte> buffer = stackalloc byte[9];
BitConverter.TryWriteBytes(buffer, value.Ticks);
buffer[8] = (byte)value.Kind;
context.WriteBytes(buffer);
context.WriteDateTimeBits(value);
}
/// <summary>
/// Optimized Guid writer using direct memory copy.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void WriteGuid(Guid value, BinarySerializationContext context)
private static void WriteGuidUnsafe(Guid value, BinarySerializationContext context)
{
context.WriteByte(BinaryTypeCode.Guid);
Span<byte> buffer = stackalloc byte[16];
value.TryWriteBytes(buffer);
context.WriteBytes(buffer);
context.WriteGuidBits(value);
}
/// <summary>
/// Optimized DateTimeOffset writer.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void WriteDateTimeOffset(DateTimeOffset value, BinarySerializationContext context)
private static void WriteDateTimeOffsetUnsafe(DateTimeOffset value, BinarySerializationContext context)
{
context.WriteByte(BinaryTypeCode.DateTimeOffset);
Span<byte> buffer = stackalloc byte[10];
BitConverter.TryWriteBytes(buffer, value.UtcTicks);
BitConverter.TryWriteBytes(buffer[8..], (short)value.Offset.TotalMinutes);
context.WriteBytes(buffer);
context.WriteDateTimeOffsetBits(value);
}
/// <summary>
/// Optimized TimeSpan writer.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void WriteTimeSpan(TimeSpan value, BinarySerializationContext context)
private static void WriteTimeSpanUnsafe(TimeSpan value, BinarySerializationContext context)
{
context.WriteByte(BinaryTypeCode.TimeSpan);
Span<byte> buffer = stackalloc byte[8];
BitConverter.TryWriteBytes(buffer, value.Ticks);
context.WriteBytes(buffer);
context.WriteRaw(value.Ticks);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void WriteInt16(short value, BinarySerializationContext context)
private static void WriteInt16Unsafe(short value, BinarySerializationContext context)
{
context.WriteByte(BinaryTypeCode.Int16);
Span<byte> buffer = stackalloc byte[2];
BitConverter.TryWriteBytes(buffer, value);
context.WriteBytes(buffer);
context.WriteRaw(value);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void WriteUInt16(ushort value, BinarySerializationContext context)
private static void WriteUInt16Unsafe(ushort value, BinarySerializationContext context)
{
context.WriteByte(BinaryTypeCode.UInt16);
Span<byte> buffer = stackalloc byte[2];
BitConverter.TryWriteBytes(buffer, value);
context.WriteBytes(buffer);
context.WriteRaw(value);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@ -439,12 +453,10 @@ public static class AcBinarySerializer
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void WriteChar(char value, BinarySerializationContext context)
private static void WriteCharUnsafe(char value, BinarySerializationContext context)
{
context.WriteByte(BinaryTypeCode.Char);
Span<byte> buffer = stackalloc byte[2];
BitConverter.TryWriteBytes(buffer, value);
context.WriteBytes(buffer);
context.WriteRaw(value);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@ -462,6 +474,10 @@ public static class AcBinarySerializer
context.WriteVarInt(intValue);
}
/// <summary>
/// Optimized string writer with span-based UTF8 encoding.
/// Uses stackalloc for small strings to avoid allocations.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void WriteString(string value, BinarySerializationContext context)
{
@ -484,11 +500,9 @@ public static class AcBinarySerializer
context.RegisterInternedString(value);
}
// Write inline string
// Write inline string with optimized encoding
context.WriteByte(BinaryTypeCode.String);
var utf8Length = Encoding.UTF8.GetByteCount(value);
context.WriteVarUInt((uint)utf8Length);
context.WriteString(value, utf8Length);
context.WriteStringUtf8(value);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@ -520,11 +534,15 @@ public static class AcBinarySerializer
var metadata = GetTypeMetadata(type);
var nextDepth = depth + 1;
// Pre-count non-null, non-default properties
var writtenCount = 0;
// Count non-null, non-default properties first
foreach (var prop in metadata.Properties)
var properties = metadata.Properties;
var propCount = properties.Length;
for (var i = 0; i < propCount; i++)
{
var prop = properties[i];
var propValue = prop.GetValue(value);
if (propValue == null) continue;
if (IsDefaultValueFast(propValue, prop.TypeCode, prop.PropertyType)) continue;
@ -533,8 +551,9 @@ public static class AcBinarySerializer
context.WriteVarUInt((uint)writtenCount);
foreach (var prop in metadata.Properties)
for (var i = 0; i < propCount; i++)
{
var prop = properties[i];
var propValue = prop.GetValue(value);
if (propValue == null) continue;
if (IsDefaultValueFast(propValue, prop.TypeCode, prop.PropertyType)) continue;
@ -554,17 +573,30 @@ public static class AcBinarySerializer
}
}
private static void WriteArray(IEnumerable enumerable, BinarySerializationContext context, int depth)
/// <summary>
/// Optimized array writer with specialized paths for primitive arrays.
/// </summary>
private static void WriteArray(IEnumerable enumerable, Type type, BinarySerializationContext context, int depth)
{
context.WriteByte(BinaryTypeCode.Array);
var nextDepth = depth + 1;
// Optimized path for primitive arrays using MemoryMarshal
var elementType = GetCollectionElementType(type);
if (elementType != null && type.IsArray)
{
if (TryWritePrimitiveArray(enumerable, elementType, context))
return;
}
// For IList, we can write the count directly
if (enumerable is IList list)
{
context.WriteVarUInt((uint)list.Count);
foreach (var item in list)
var count = list.Count;
context.WriteVarUInt((uint)count);
for (var i = 0; i < count; i++)
{
var item = list[i];
var itemType = item?.GetType() ?? typeof(object);
WriteValue(item, itemType, context, nextDepth);
}
@ -586,6 +618,103 @@ public static class AcBinarySerializer
}
}
/// <summary>
/// Specialized array writer for primitive arrays using bulk memory operations.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool TryWritePrimitiveArray(IEnumerable enumerable, Type elementType, BinarySerializationContext context)
{
// Int32 array - very common case
if (ReferenceEquals(elementType, IntType) && enumerable is int[] intArray)
{
context.WriteVarUInt((uint)intArray.Length);
for (var i = 0; i < intArray.Length; i++)
{
WriteInt32(intArray[i], context);
}
return true;
}
// Double array
if (ReferenceEquals(elementType, DoubleType) && enumerable is double[] doubleArray)
{
context.WriteVarUInt((uint)doubleArray.Length);
for (var i = 0; i < doubleArray.Length; i++)
{
WriteFloat64Unsafe(doubleArray[i], context);
}
return true;
}
// Long array
if (ReferenceEquals(elementType, LongType) && enumerable is long[] longArray)
{
context.WriteVarUInt((uint)longArray.Length);
for (var i = 0; i < longArray.Length; i++)
{
WriteInt64(longArray[i], context);
}
return true;
}
// Float array
if (ReferenceEquals(elementType, FloatType) && enumerable is float[] floatArray)
{
context.WriteVarUInt((uint)floatArray.Length);
for (var i = 0; i < floatArray.Length; i++)
{
WriteFloat32Unsafe(floatArray[i], context);
}
return true;
}
// Bool array
if (ReferenceEquals(elementType, BoolType) && enumerable is bool[] boolArray)
{
context.WriteVarUInt((uint)boolArray.Length);
for (var i = 0; i < boolArray.Length; i++)
{
context.WriteByte(boolArray[i] ? BinaryTypeCode.True : BinaryTypeCode.False);
}
return true;
}
// Guid array
if (ReferenceEquals(elementType, GuidType) && enumerable is Guid[] guidArray)
{
context.WriteVarUInt((uint)guidArray.Length);
for (var i = 0; i < guidArray.Length; i++)
{
WriteGuidUnsafe(guidArray[i], context);
}
return true;
}
// Decimal array
if (ReferenceEquals(elementType, DecimalType) && enumerable is decimal[] decimalArray)
{
context.WriteVarUInt((uint)decimalArray.Length);
for (var i = 0; i < decimalArray.Length; i++)
{
WriteDecimalUnsafe(decimalArray[i], context);
}
return true;
}
// DateTime array
if (ReferenceEquals(elementType, DateTimeType) && enumerable is DateTime[] dateTimeArray)
{
context.WriteVarUInt((uint)dateTimeArray.Length);
for (var i = 0; i < dateTimeArray.Length; i++)
{
WriteDateTimeUnsafe(dateTimeArray[i], context);
}
return true;
}
return false;
}
private static void WriteDictionary(IDictionary dictionary, BinarySerializationContext context, int depth)
{
context.WriteByte(BinaryTypeCode.Dictionary);
@ -779,11 +908,18 @@ public static class AcBinarySerializer
#region Serialization Context
/// <summary>
/// Optimized serialization context with direct memory operations.
/// Uses ArrayPool for buffer management and MemoryMarshal for zero-copy writes.
/// </summary>
internal sealed class BinarySerializationContext : IDisposable
{
private byte[] _buffer;
private int _position;
// Minimum buffer size for ArrayPool (reduces fragmentation)
private const int MinBufferSize = 256;
// Reference handling
private Dictionary<object, int>? _scanOccurrences;
private Dictionary<object, int>? _writtenRefs;
@ -806,7 +942,8 @@ public static class AcBinarySerializer
public BinarySerializationContext(AcBinarySerializerOptions options)
{
_buffer = ArrayPool<byte>.Shared.Rent(options.InitialBufferCapacity);
var size = Math.Max(options.InitialBufferCapacity, MinBufferSize);
_buffer = ArrayPool<byte>.Shared.Rent(size);
Reset(options);
}
@ -843,14 +980,23 @@ public static class AcBinarySerializer
}
}
#region Buffer Writing
#region Optimized Buffer Writing
/// <summary>
/// Ensures buffer has capacity, growing by doubling if needed.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void EnsureCapacity(int additionalBytes)
{
var required = _position + additionalBytes;
if (required <= _buffer.Length) return;
GrowBuffer(required);
}
[MethodImpl(MethodImplOptions.NoInlining)]
private void GrowBuffer(int required)
{
var newSize = Math.Max(_buffer.Length * 2, required);
var newBuffer = ArrayPool<byte>.Shared.Rent(newSize);
_buffer.AsSpan(0, _position).CopyTo(newBuffer);
@ -873,6 +1019,68 @@ public static class AcBinarySerializer
_position += data.Length;
}
/// <summary>
/// Write a blittable value type directly to buffer using MemoryMarshal.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteRaw<T>(T value) where T : unmanaged
{
var size = Unsafe.SizeOf<T>();
EnsureCapacity(size);
Unsafe.WriteUnaligned(ref _buffer[_position], value);
_position += size;
}
/// <summary>
/// Optimized decimal writer using GetBits.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteDecimalBits(decimal value)
{
EnsureCapacity(16);
Span<int> bits = stackalloc int[4];
decimal.TryGetBits(value, bits, out _);
var destSpan = _buffer.AsSpan(_position, 16);
MemoryMarshal.AsBytes(bits).CopyTo(destSpan);
_position += 16;
}
/// <summary>
/// Optimized DateTime writer.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteDateTimeBits(DateTime value)
{
EnsureCapacity(9);
Unsafe.WriteUnaligned(ref _buffer[_position], value.Ticks);
_buffer[_position + 8] = (byte)value.Kind;
_position += 9;
}
/// <summary>
/// Optimized Guid writer using TryWriteBytes.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteGuidBits(Guid value)
{
EnsureCapacity(16);
value.TryWriteBytes(_buffer.AsSpan(_position, 16));
_position += 16;
}
/// <summary>
/// Optimized DateTimeOffset writer.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteDateTimeOffsetBits(DateTimeOffset value)
{
EnsureCapacity(10);
Unsafe.WriteUnaligned(ref _buffer[_position], value.UtcTicks);
Unsafe.WriteUnaligned(ref _buffer[_position + 8], (short)value.Offset.TotalMinutes);
_position += 10;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteVarInt(int value)
{
@ -925,12 +1133,29 @@ public static class AcBinarySerializer
_buffer[_position++] = (byte)value;
}
/// <summary>
/// Optimized string writer using span-based UTF8 encoding.
/// Uses stackalloc for small strings to avoid allocations.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteString(string value, int utf8Length)
public void WriteStringUtf8(string value)
{
EnsureCapacity(utf8Length);
Encoding.UTF8.GetBytes(value, _buffer.AsSpan(_position, utf8Length));
_position += utf8Length;
// For small strings, use stackalloc to avoid GetByteCount call
if (value.Length <= 128)
{
Span<byte> tempBuffer = stackalloc byte[value.Length * 3]; // Max UTF8 expansion
var bytesWritten = Encoding.UTF8.GetBytes(value.AsSpan(), tempBuffer);
WriteVarUInt((uint)bytesWritten);
WriteBytes(tempBuffer.Slice(0, bytesWritten));
}
else
{
var utf8Length = Encoding.UTF8.GetByteCount(value);
WriteVarUInt((uint)utf8Length);
EnsureCapacity(utf8Length);
Encoding.UTF8.GetBytes(value, _buffer.AsSpan(_position, utf8Length));
_position += utf8Length;
}
}
#endregion
@ -954,28 +1179,31 @@ public static class AcBinarySerializer
var hasPropertyNames = _propertyNameList != null && _propertyNameList.Count > 0;
if (!UseMetadata || !hasPropertyNames)
// Build flags byte
byte flags = BinaryTypeCode.HeaderFlagsBase;
if (UseMetadata && hasPropertyNames)
{
_buffer[_headerPosition + 1] = BinaryTypeCode.NoMetadataHeader;
return;
flags |= BinaryTypeCode.HeaderFlag_Metadata;
}
_buffer[_headerPosition + 1] = BinaryTypeCode.MetadataHeader;
// Write property name count
WriteVarUInt((uint)_propertyNameList!.Count);
// Write property names
foreach (var name in _propertyNameList)
if (UseReferenceHandling)
{
var utf8Length = Encoding.UTF8.GetByteCount(name);
WriteVarUInt((uint)utf8Length);
WriteString(name, utf8Length);
flags |= BinaryTypeCode.HeaderFlag_ReferenceHandling;
}
_buffer[_headerPosition + 1] = flags;
// NOTE: Interned strings are handled "on-the-fly" during serialization
// First occurrence is written inline, subsequent occurrences use StringInterned reference
// We don't need to write them in metadata since deserializer builds table dynamically
// Write property names if metadata is enabled
if ((flags & BinaryTypeCode.HeaderFlag_Metadata) != 0)
{
// Write property name count
WriteVarUInt((uint)_propertyNameList!.Count);
// Write property names
foreach (var name in _propertyNameList)
{
WriteStringUtf8(name);
}
}
}
#endregion

View File

@ -136,8 +136,21 @@ internal static class BinaryTypeCode
public const byte ByteArray = 30; // Optimized byte[] storage
// Special markers (32+, for header/meta)
public const byte MetadataHeader = 32; // Binary has metadata section
public const byte NoMetadataHeader = 33; // Binary has no metadata
// Header flags byte structure (for values >= 64):
// Bit 0 (0x01): HasMetadata
// Bit 1 (0x02): HasReferenceHandling
// Values 32, 33 are legacy for backward compatibility
public const byte MetadataHeader = 32; // Binary has metadata section (legacy, implies HasReferenceHandling=true)
public const byte NoMetadataHeader = 33; // Binary has no metadata (legacy, implies HasReferenceHandling=true)
// New flag-based header markers (48+)
// Base value 48 (0x30 = 00110000) chosen to:
// - Be distinguishable from legacy values (32, 33)
// - Not conflict with flag bits in lower nibble
// - Leave room below Int32Tiny (64)
public const byte HeaderFlagsBase = 48; // Base value for flag-based headers (0x30)
public const byte HeaderFlag_Metadata = 0x01;
public const byte HeaderFlag_ReferenceHandling = 0x02;
// Compact integer variants (for VarInt optimization)
public const byte Int32Tiny = 64; // -16 to 111 stored in single byte (value = code - 64 - 16)

View File

@ -335,8 +335,10 @@ public static class JsonUtilities
(byte)' ' or (byte)'\t' or (byte)'\n' or (byte)'\r' => AcSerializerType.Json,
>= (byte)'0' and <= (byte)'9' => AcSerializerType.Json,
(byte)'-' or (byte)'t' or (byte)'f' or (byte)'n' => AcSerializerType.Json,
// Binary format version 1 with metadata or no-metadata header
1 when data.Length > 1 && (data[1] == 32 || data[1] == 33) => AcSerializerType.Binary,
// Binary format version 1 with:
// - Legacy metadata header (32) or no-metadata header (33)
// - New flag-based header (34+)
1 when data.Length > 1 && data[1] >= 32 => AcSerializerType.Binary,
_ => AcSerializerType.Binary // Default to Binary for unknown byte patterns
};
}

View File

@ -13,7 +13,7 @@ namespace AyCode.Services.Server.Tests.SignalRs;
/// </summary>
public abstract class SignalRClientToHubTestBase
{
protected abstract AcSerializerType SerializerType { get; }
protected abstract AcSerializerOptions SerializerOption { get; }
protected TestLogger _logger = null!;
protected TestableSignalRClient2 _client = null!;
@ -28,7 +28,7 @@ public abstract class SignalRClientToHubTestBase
_service = new TestSignalRService2();
_client = new TestableSignalRClient2(_hub, _logger);
_hub.SetSerializerType(SerializerType);
_hub.SetSerializerType(SerializerOption);
_hub.RegisterService(_service, _client);
}
@ -1117,14 +1117,26 @@ public abstract class SignalRClientToHubTestBase
[TestClass]
public class SignalRClientToHubTest_Json : SignalRClientToHubTestBase
{
protected override AcSerializerType SerializerType => AcSerializerType.Json;
protected override AcSerializerOptions SerializerOption { get; } = new AcJsonSerializerOptions();
}
/// <summary>
/// Runs all SignalR tests with Binary serialization.
/// </summary>
[TestClass]
public class SignalRClientToHubTest_Binary : SignalRClientToHubTestBase
public class SignalRClientToHubTest_Binary_WithRef : SignalRClientToHubTestBase
{
protected override AcSerializerType SerializerType => AcSerializerType.Binary;
protected override AcSerializerOptions SerializerOption { get; } = new AcBinarySerializerOptions();
}
/// <summary>
/// Runs all SignalR tests with Binary serialization.
/// </summary>
[TestClass]
public class SignalRClientToHubTest_Binary_NoRef : SignalRClientToHubTestBase
{
protected override AcSerializerOptions SerializerOption { get; } = new AcBinarySerializerOptions
{
UseReferenceHandling = false
};
}

View File

@ -66,12 +66,9 @@ public class TestableSignalRHub2 : AcWebSignalRHubBase<TestSignalRTags, TestLogg
/// <summary>
/// Sets the serializer type for testing Binary vs JSON serialization.
/// </summary>
public void SetSerializerType(AcSerializerType serializerType)
public void SetSerializerType(AcSerializerOptions acSerializerOptions)
{
if (serializerType == AcSerializerType.Binary)
SerializerOptions = new AcBinarySerializerOptions();
else
SerializerOptions = new AcJsonSerializerOptions();
SerializerOptions = acSerializerOptions;
}
#endregion

View File

@ -193,4 +193,219 @@ public class MessagePackComparisonBenchmark
[Benchmark(Description = "MessagePack Deserialize")]
public TestOrder? Deserialize_MsgPack() => MessagePackSerializer.Deserialize<TestOrder>(_msgPackData, _msgPackOptions);
}
/// <summary>
/// Comprehensive AcBinary vs MessagePack comparison benchmark.
/// Tests: WithRef, NoRef, Populate, Serialize, Deserialize, Size
/// </summary>
[ShortRunJob]
[MemoryDiagnoser]
[RankColumn]
public class AcBinaryVsMessagePackFullBenchmark
{
// Test data
private TestOrder _testOrder = null!;
private TestOrder _populateTarget = null!;
// Serialized data - AcBinary
private byte[] _acBinaryWithRef = null!;
private byte[] _acBinaryNoRef = null!;
// Serialized data - MessagePack
private byte[] _msgPackData = null!;
// Options
private AcBinarySerializerOptions _withRefOptions = null!;
private AcBinarySerializerOptions _noRefOptions = null!;
private MessagePackSerializerOptions _msgPackOptions = null!;
[GlobalSetup]
public void Setup()
{
// Create test data with shared references
TestDataFactory.ResetIdCounter();
var sharedTag = TestDataFactory.CreateTag("SharedTag");
var sharedUser = TestDataFactory.CreateUser("shareduser");
var sharedMeta = TestDataFactory.CreateMetadata("shared", withChild: true);
_testOrder = TestDataFactory.CreateOrder(
itemCount: 3,
palletsPerItem: 3,
measurementsPerPallet: 3,
pointsPerMeasurement: 4,
sharedTag: sharedTag,
sharedUser: sharedUser,
sharedMetadata: sharedMeta);
// Setup options
_withRefOptions = AcBinarySerializerOptions.Default; // WithRef by default
_noRefOptions = AcBinarySerializerOptions.WithoutReferenceHandling();
_msgPackOptions = ContractlessStandardResolver.Options.WithCompression(MessagePackCompression.None);
// Serialize with different options
_acBinaryWithRef = AcBinarySerializer.Serialize(_testOrder, _withRefOptions);
_acBinaryNoRef = AcBinarySerializer.Serialize(_testOrder, _noRefOptions);
_msgPackData = MessagePackSerializer.Serialize(_testOrder, _msgPackOptions);
// Create populate target
_populateTarget = new TestOrder { Id = _testOrder.Id };
foreach (var item in _testOrder.Items)
{
_populateTarget.Items.Add(new TestOrderItem { Id = item.Id });
}
// Print size comparison
PrintSizeComparison();
}
private void PrintSizeComparison()
{
Console.WriteLine("\n" + new string('=', 60));
Console.WriteLine("?? SIZE COMPARISON (AcBinary vs MessagePack)");
Console.WriteLine(new string('=', 60));
Console.WriteLine($" AcBinary WithRef: {_acBinaryWithRef.Length,8:N0} bytes");
Console.WriteLine($" AcBinary NoRef: {_acBinaryNoRef.Length,8:N0} bytes");
Console.WriteLine($" MessagePack: {_msgPackData.Length,8:N0} bytes");
Console.WriteLine(new string('-', 60));
Console.WriteLine($" AcBinary/MsgPack: {100.0 * _acBinaryWithRef.Length / _msgPackData.Length:F1}% (WithRef)");
Console.WriteLine($" AcBinary/MsgPack: {100.0 * _acBinaryNoRef.Length / _msgPackData.Length:F1}% (NoRef)");
Console.WriteLine(new string('=', 60) + "\n");
}
#region Serialize Benchmarks
[Benchmark(Description = "AcBinary Serialize WithRef")]
public byte[] Serialize_AcBinary_WithRef() => AcBinarySerializer.Serialize(_testOrder, _withRefOptions);
[Benchmark(Description = "AcBinary Serialize NoRef")]
public byte[] Serialize_AcBinary_NoRef() => AcBinarySerializer.Serialize(_testOrder, _noRefOptions);
[Benchmark(Description = "MessagePack Serialize", Baseline = true)]
public byte[] Serialize_MsgPack() => MessagePackSerializer.Serialize(_testOrder, _msgPackOptions);
#endregion
#region Deserialize Benchmarks
[Benchmark(Description = "AcBinary Deserialize WithRef")]
public TestOrder? Deserialize_AcBinary_WithRef() => AcBinaryDeserializer.Deserialize<TestOrder>(_acBinaryWithRef);
[Benchmark(Description = "AcBinary Deserialize NoRef")]
public TestOrder? Deserialize_AcBinary_NoRef() => AcBinaryDeserializer.Deserialize<TestOrder>(_acBinaryNoRef);
[Benchmark(Description = "MessagePack Deserialize")]
public TestOrder? Deserialize_MsgPack() => MessagePackSerializer.Deserialize<TestOrder>(_msgPackData, _msgPackOptions);
#endregion
#region Populate Benchmarks
[Benchmark(Description = "AcBinary Populate WithRef")]
public void Populate_AcBinary_WithRef()
{
var target = CreatePopulateTarget();
AcBinaryDeserializer.Populate(_acBinaryWithRef, target);
}
[Benchmark(Description = "AcBinary PopulateMerge WithRef")]
public void PopulateMerge_AcBinary_WithRef()
{
var target = CreatePopulateTarget();
AcBinaryDeserializer.PopulateMerge(_acBinaryWithRef.AsSpan(), target);
}
private TestOrder CreatePopulateTarget()
{
var target = new TestOrder { Id = _testOrder.Id };
foreach (var item in _testOrder.Items)
{
target.Items.Add(new TestOrderItem { Id = item.Id });
}
return target;
}
#endregion
}
/// <summary>
/// Detailed size comparison - not a performance benchmark, just size output.
/// </summary>
[ShortRunJob]
[MemoryDiagnoser]
public class SizeComparisonBenchmark
{
private TestOrder _smallOrder = null!;
private TestOrder _mediumOrder = null!;
private TestOrder _largeOrder = null!;
private MessagePackSerializerOptions _msgPackOptions = null!;
private AcBinarySerializerOptions _withRefOptions = null!;
private AcBinarySerializerOptions _noRefOptions = null!;
[GlobalSetup]
public void Setup()
{
_msgPackOptions = ContractlessStandardResolver.Options.WithCompression(MessagePackCompression.None);
_withRefOptions = AcBinarySerializerOptions.Default;
_noRefOptions = AcBinarySerializerOptions.WithoutReferenceHandling();
// Small order
TestDataFactory.ResetIdCounter();
_smallOrder = TestDataFactory.CreateOrder(itemCount: 1, palletsPerItem: 1, measurementsPerPallet: 1, pointsPerMeasurement: 2);
// Medium order
TestDataFactory.ResetIdCounter();
var sharedTag = TestDataFactory.CreateTag("Shared");
var sharedUser = TestDataFactory.CreateUser("shared");
_mediumOrder = TestDataFactory.CreateOrder(
itemCount: 3, palletsPerItem: 2, measurementsPerPallet: 2, pointsPerMeasurement: 3,
sharedTag: sharedTag, sharedUser: sharedUser);
// Large order
TestDataFactory.ResetIdCounter();
sharedTag = TestDataFactory.CreateTag("SharedLarge");
sharedUser = TestDataFactory.CreateUser("sharedlarge");
var sharedMeta = TestDataFactory.CreateMetadata("meta", withChild: true);
_largeOrder = TestDataFactory.CreateOrder(
itemCount: 5, palletsPerItem: 4, measurementsPerPallet: 3, pointsPerMeasurement: 5,
sharedTag: sharedTag, sharedUser: sharedUser, sharedMetadata: sharedMeta);
PrintDetailedSizeComparison();
}
private void PrintDetailedSizeComparison()
{
Console.WriteLine("\n" + new string('=', 80));
Console.WriteLine("?? DETAILED SIZE COMPARISON: AcBinary vs MessagePack");
Console.WriteLine(new string('=', 80));
PrintOrderSize("Small Order (1x1x1x2)", _smallOrder);
PrintOrderSize("Medium Order (3x2x2x3) + SharedRefs", _mediumOrder);
PrintOrderSize("Large Order (5x4x3x5) + SharedRefs", _largeOrder);
Console.WriteLine(new string('=', 80) + "\n");
}
private void PrintOrderSize(string name, TestOrder order)
{
var acWithRef = AcBinarySerializer.Serialize(order, _withRefOptions);
var acNoRef = AcBinarySerializer.Serialize(order, _noRefOptions);
var msgPack = MessagePackSerializer.Serialize(order, _msgPackOptions);
Console.WriteLine($"\n {name}:");
Console.WriteLine($" AcBinary WithRef: {acWithRef.Length,8:N0} bytes ({100.0 * acWithRef.Length / msgPack.Length,5:F1}% of MsgPack)");
Console.WriteLine($" AcBinary NoRef: {acNoRef.Length,8:N0} bytes ({100.0 * acNoRef.Length / msgPack.Length,5:F1}% of MsgPack)");
Console.WriteLine($" MessagePack: {msgPack.Length,8:N0} bytes (100.0%)");
var withRefSaving = msgPack.Length - acWithRef.Length;
var noRefSaving = msgPack.Length - acNoRef.Length;
if (withRefSaving > 0)
Console.WriteLine($" ?? AcBinary WithRef saves: {withRefSaving:N0} bytes ({100.0 * withRefSaving / msgPack.Length:F1}%)");
else
Console.WriteLine($" ?? AcBinary WithRef larger by: {-withRefSaving:N0} bytes");
}
[Benchmark(Description = "Placeholder")]
public int Placeholder() => 1; // Just to make BenchmarkDotNet happy
}

View File

@ -0,0 +1,828 @@
<!DOCTYPE html>
<html lang="hu">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AcBinary vs MessagePack - Benchmark Riport</title>
<style>
:root {
--primary: #4f46e5;
--primary-light: #818cf8;
--success: #22c55e;
--warning: #f59e0b;
--danger: #ef4444;
--gray-50: #f9fafb;
--gray-100: #f3f4f6;
--gray-200: #e5e7eb;
--gray-300: #d1d5db;
--gray-600: #4b5563;
--gray-800: #1f2937;
--gray-900: #111827;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 2rem;
color: var(--gray-800);
}
.container {
max-width: 1200px;
margin: 0 auto;
}
.card {
background: white;
border-radius: 16px;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
padding: 2rem;
margin-bottom: 2rem;
}
.header {
text-align: center;
margin-bottom: 2rem;
}
.header h1 {
font-size: 2.5rem;
background: linear-gradient(135deg, var(--primary) 0%, #7c3aed 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 0.5rem;
}
.header .subtitle {
color: var(--gray-600);
font-size: 1.1rem;
}
.header .date {
color: var(--gray-400);
font-size: 0.9rem;
margin-top: 0.5rem;
}
h2 {
color: var(--gray-800);
font-size: 1.5rem;
margin-bottom: 1.5rem;
padding-bottom: 0.5rem;
border-bottom: 3px solid var(--primary-light);
display: flex;
align-items: center;
gap: 0.5rem;
}
h3 {
color: var(--gray-700);
font-size: 1.2rem;
margin: 1.5rem 0 1rem;
}
table {
width: 100%;
border-collapse: collapse;
margin-bottom: 1rem;
font-size: 0.95rem;
}
th, td {
padding: 0.875rem 1rem;
text-align: left;
border-bottom: 1px solid var(--gray-200);
}
th {
background: var(--gray-50);
font-weight: 600;
color: var(--gray-700);
text-transform: uppercase;
font-size: 0.75rem;
letter-spacing: 0.05em;
}
tr:hover {
background: var(--gray-50);
}
.number {
font-family: 'Consolas', 'Monaco', monospace;
text-align: right;
}
.winner {
background: linear-gradient(90deg, rgba(34, 197, 94, 0.1) 0%, transparent 100%);
}
.winner td:first-child::before {
content: "?? ";
}
.badge {
display: inline-block;
padding: 0.25rem 0.75rem;
border-radius: 9999px;
font-size: 0.75rem;
font-weight: 600;
}
.badge-success {
background: rgba(34, 197, 94, 0.15);
color: #15803d;
}
.badge-warning {
background: rgba(245, 158, 11, 0.15);
color: #b45309;
}
.badge-danger {
background: rgba(239, 68, 68, 0.15);
color: #dc2626;
}
.badge-info {
background: rgba(79, 70, 229, 0.15);
color: var(--primary);
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
margin-bottom: 1.5rem;
}
.stat-card {
background: var(--gray-50);
border-radius: 12px;
padding: 1.25rem;
text-align: center;
border: 1px solid var(--gray-200);
}
.stat-card .value {
font-size: 2rem;
font-weight: 700;
color: var(--primary);
font-family: 'Consolas', monospace;
}
.stat-card .label {
color: var(--gray-600);
font-size: 0.85rem;
margin-top: 0.25rem;
}
.stat-card.success .value { color: var(--success); }
.stat-card.warning .value { color: var(--warning); }
.stat-card.danger .value { color: var(--danger); }
.comparison-bar {
display: flex;
height: 32px;
border-radius: 8px;
overflow: hidden;
margin: 0.5rem 0;
background: var(--gray-100);
}
.comparison-bar .segment {
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 0.75rem;
font-weight: 600;
transition: all 0.3s ease;
}
.comparison-bar .segment:hover {
filter: brightness(1.1);
}
.segment.acbinary { background: var(--primary); }
.segment.msgpack { background: #f97316; }
.legend {
display: flex;
gap: 1.5rem;
margin-top: 0.5rem;
font-size: 0.85rem;
}
.legend-item {
display: flex;
align-items: center;
gap: 0.5rem;
}
.legend-color {
width: 16px;
height: 16px;
border-radius: 4px;
}
.summary-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 1.5rem;
}
.summary-item {
background: var(--gray-50);
border-radius: 12px;
padding: 1.5rem;
border-left: 4px solid var(--primary);
}
.summary-item h4 {
color: var(--gray-800);
margin-bottom: 0.75rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.summary-item ul {
list-style: none;
color: var(--gray-600);
font-size: 0.9rem;
}
.summary-item li {
padding: 0.25rem 0;
padding-left: 1.25rem;
position: relative;
}
.summary-item li::before {
content: "?";
position: absolute;
left: 0;
color: var(--success);
}
.env-info {
background: var(--gray-50);
border-radius: 8px;
padding: 1rem;
font-size: 0.85rem;
color: var(--gray-600);
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 0.5rem;
}
.env-info span {
display: flex;
align-items: center;
gap: 0.5rem;
}
.note {
background: rgba(79, 70, 229, 0.1);
border-left: 4px solid var(--primary);
padding: 1rem;
border-radius: 0 8px 8px 0;
margin: 1rem 0;
font-size: 0.9rem;
color: var(--gray-700);
}
.warning-note {
background: rgba(245, 158, 11, 0.1);
border-left: 4px solid var(--warning);
}
.optimization-item {
background: var(--gray-50);
border-radius: 8px;
padding: 1rem;
margin-bottom: 1rem;
border-left: 4px solid var(--primary);
}
.optimization-item.high {
border-left-color: var(--success);
}
.optimization-item.medium {
border-left-color: var(--warning);
}
.optimization-item.low {
border-left-color: var(--gray-300);
}
.optimization-item h4 {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.5rem;
color: var(--gray-800);
}
.optimization-item p {
color: var(--gray-600);
font-size: 0.9rem;
margin-bottom: 0.5rem;
}
.optimization-item code {
background: var(--gray-200);
padding: 0.125rem 0.375rem;
border-radius: 4px;
font-size: 0.85rem;
}
.impact-badge {
font-size: 0.7rem;
padding: 0.125rem 0.5rem;
border-radius: 4px;
font-weight: 600;
}
.impact-high { background: rgba(34, 197, 94, 0.2); color: #15803d; }
.impact-medium { background: rgba(245, 158, 11, 0.2); color: #b45309; }
.impact-low { background: rgba(107, 114, 128, 0.2); color: var(--gray-600); }
footer {
text-align: center;
color: white;
opacity: 0.8;
padding: 1rem;
font-size: 0.85rem;
}
</style>
</head>
<body>
<div class="container">
<div class="card header">
<h1>?? AcBinary vs MessagePack</h1>
<div class="subtitle">Komplett Benchmark Összehasonlítás + Memória Diagnosztika</div>
<div class="date">Generálva: 2024. december 13. | .NET 9.0 | Intel Core i7-10750H</div>
</div>
<!-- Környezet info -->
<div class="card">
<h2>??? Teszt Környezet</h2>
<div class="env-info">
<span>?? Windows 11 (23H2)</span>
<span>?? Intel Core i7-10750H @ 2.60GHz</span>
<span>?? .NET SDK 10.0.101</span>
<span>?? Runtime: .NET 9.0.11</span>
<span>?? BenchmarkDotNet v0.15.2</span>
<span>?? Teszt adat: 3×3×3×4 hierarchia</span>
</div>
</div>
<!-- Méret összehasonlítás -->
<div class="card">
<h2>?? Méret Összehasonlítás</h2>
<div class="stats-grid">
<div class="stat-card">
<div class="value">18.9 KB</div>
<div class="label">AcBinary WithRef</div>
</div>
<div class="stat-card">
<div class="value">15.8 KB</div>
<div class="label">AcBinary NoRef</div>
</div>
<div class="stat-card success">
<div class="value">11.2 KB</div>
<div class="label">MessagePack</div>
</div>
</div>
<h3>Méret arány (MessagePack = 100%)</h3>
<div class="comparison-bar">
<div class="segment acbinary" style="width: 62.5%;">AcBinary: 168%</div>
<div class="segment msgpack" style="width: 37.5%;">MsgPack: 100%</div>
</div>
<div class="legend">
<div class="legend-item"><div class="legend-color" style="background: var(--primary);"></div> AcBinary</div>
<div class="legend-item"><div class="legend-color" style="background: #f97316;"></div> MessagePack</div>
</div>
<div class="note">
?? <strong>Megjegyzés:</strong> Az AcBinary nagyobb méretet eredményez a beépített metaadat (property nevek táblája) és típusinformációk miatt, ami viszont lehetõvé teszi a schema evolúciót és a gyorsabb deszerializálást.
</div>
</div>
<!-- Teljesítmény táblázat -->
<div class="card">
<h2>? Teljesítmény Összehasonlítás</h2>
<table>
<thead>
<tr>
<th>Mûvelet</th>
<th class="number">AcBinary</th>
<th class="number">MessagePack</th>
<th class="number">Arány</th>
<th>Eredmény</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Serialize WithRef</strong></td>
<td class="number">84.20 ?s</td>
<td class="number">18.84 ?s</td>
<td class="number">4.47×</td>
<td><span class="badge badge-warning">MsgPack gyorsabb</span></td>
</tr>
<tr>
<td><strong>Serialize NoRef</strong></td>
<td class="number">70.18 ?s</td>
<td class="number">18.84 ?s</td>
<td class="number">3.73×</td>
<td><span class="badge badge-warning">MsgPack gyorsabb</span></td>
</tr>
<tr class="winner">
<td><strong>Deserialize WithRef</strong></td>
<td class="number">40.10 ?s</td>
<td class="number">41.10 ?s</td>
<td class="number">0.98×</td>
<td><span class="badge badge-success">AcBinary gyorsabb</span></td>
</tr>
<tr class="winner">
<td><strong>Deserialize NoRef</strong></td>
<td class="number">1.02 ?s</td>
<td class="number">41.10 ?s</td>
<td class="number">0.025×</td>
<td><span class="badge badge-success">40× gyorsabb!</span></td>
</tr>
<tr>
<td><strong>Populate</strong></td>
<td class="number">39.27 ?s</td>
<td class="number"></td>
<td class="number"></td>
<td><span class="badge badge-info">Csak AcBinary</span></td>
</tr>
<tr>
<td><strong>PopulateMerge</strong></td>
<td class="number">40.73 ?s</td>
<td class="number"></td>
<td class="number"></td>
<td><span class="badge badge-info">Csak AcBinary</span></td>
</tr>
</tbody>
</table>
</div>
<!-- Memória használat -->
<div class="card">
<h2>?? Memória Allokáció (GC Diagnosztika)</h2>
<table>
<thead>
<tr>
<th>Mûvelet</th>
<th class="number">AcBinary</th>
<th class="number">MessagePack</th>
<th class="number">Gen0</th>
<th class="number">Gen1</th>
<th>Értékelés</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Serialize WithRef</strong></td>
<td class="number">55.34 KB</td>
<td class="number">12.50 KB</td>
<td class="number">9.03</td>
<td class="number"></td>
<td><span class="badge badge-warning">4.43× több</span></td>
</tr>
<tr>
<td><strong>Serialize NoRef</strong></td>
<td class="number">46.30 KB</td>
<td class="number">12.50 KB</td>
<td class="number">7.45</td>
<td class="number"></td>
<td><span class="badge badge-warning">3.70× több</span></td>
</tr>
<tr>
<td><strong>Deserialize WithRef</strong></td>
<td class="number">38.17 KB</td>
<td class="number">26.24 KB</td>
<td class="number">6.23</td>
<td class="number">0.43</td>
<td><span class="badge badge-warning">1.45× több</span></td>
</tr>
<tr class="winner">
<td><strong>Deserialize NoRef</strong></td>
<td class="number">2.85 KB</td>
<td class="number">26.24 KB</td>
<td class="number">0.47</td>
<td class="number">0.004</td>
<td><span class="badge badge-success">9× kevesebb!</span></td>
</tr>
</tbody>
</table>
<div class="note warning-note">
?? <strong>GC Pressure:</strong> A Serialize WithRef 9.03 Gen0 GC-t triggerel 1000 mûveletre. Ez jelentõs GC nyomást jelent nagy áteresztõképességû szerver alkalmazásokban.
</div>
</div>
<!-- Allokációs hotspotok -->
<div class="card">
<h2>?? Memória Allokációs Hotspotok</h2>
<h3>Serialize (55.34 KB allokáció)</h3>
<table>
<thead>
<tr>
<th>Forrás</th>
<th class="number">Becsült méret</th>
<th>Leírás</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>ToArray()</code></td>
<td class="number">~19 KB</td>
<td>Végsõ byte[] allokáció a visszatérési értékhez</td>
</tr>
<tr>
<td><code>Dictionary&lt;object,int&gt;</code></td>
<td class="number">~8 KB</td>
<td>Reference scanning: _scanOccurrences, _writtenRefs</td>
</tr>
<tr>
<td><code>HashSet&lt;object&gt;</code></td>
<td class="number">~4 KB</td>
<td>Multi-referenced objektumok nyilvántartása</td>
</tr>
<tr>
<td><code>Dictionary&lt;string,int&gt;</code></td>
<td class="number">~6 KB</td>
<td>Property name table + string interning</td>
</tr>
<tr>
<td><code>List&lt;string&gt;</code></td>
<td class="number">~4 KB</td>
<td>Property nevek és interned stringek listái</td>
</tr>
<tr>
<td>Boxing</td>
<td class="number">~10 KB</td>
<td>Value type boxing a property getter-ekben</td>
</tr>
<tr>
<td>Egyéb</td>
<td class="number">~4 KB</td>
<td>Closure-ok, delegate-ek, átmeneti objektumok</td>
</tr>
</tbody>
</table>
</div>
<!-- Optimalizálási javaslatok -->
<div class="card">
<h2>?? További Optimalizálási Lehetõségek</h2>
<div class="optimization-item high">
<h4>
<span class="impact-badge impact-high">MAGAS HATÁS</span>
1. IBufferWriter&lt;byte&gt; alapú Serialize
</h4>
<p>
A <code>ToArray()</code> hívás ~19 KB allokációt okoz. Ehelyett <code>IBufferWriter&lt;byte&gt;</code>
interfész használatával a hívó biztosíthatja a buffert, elkerülve az allokációt.
</p>
<p><strong>Becsült megtakarítás:</strong> ~35% memória csökkenés serialize-nál</p>
</div>
<div class="optimization-item high">
<h4>
<span class="impact-badge impact-high">MAGAS HATÁS</span>
2. Typed Property Getter-ek (Boxing elkerülése)
</h4>
<p>
A jelenlegi <code>Func&lt;object, object?&gt;</code> getter minden value type-ot boxol.
Típusos getter-ek (<code>Func&lt;T, int&gt;</code>, stb.) használatával megszüntethetõ.
</p>
<p><strong>Becsült megtakarítás:</strong> ~10 KB / serialize (~18% csökkenés)</p>
</div>
<div class="optimization-item medium">
<h4>
<span class="impact-badge impact-medium">KÖZEPES HATÁS</span>
3. Reference Tracking gyûjtemények poolozása
</h4>
<p>
A <code>Dictionary</code> és <code>HashSet</code> objektumok a context pool-ban maradnak,
de <code>Clear()</code> után is megtartják a kapacitásukat. A poolban tárolás elõtt
érdemes lenne <code>TrimExcess()</code> hívni, vagy kisebb initial capacity-t használni.
</p>
<p><strong>Becsült megtakarítás:</strong> ~5 KB / serialize</p>
</div>
<div class="optimization-item medium">
<h4>
<span class="impact-badge impact-medium">KÖZEPES HATÁS</span>
4. String.Create() UTF8 kódoláshoz
</h4>
<p>
A deszerializálásnál a <code>Encoding.UTF8.GetString()</code> új stringet allokál.
<code>String.Create()</code> span callback-kel közvetlenül a string bufferbe írhat.
</p>
<p><strong>Becsült megtakarítás:</strong> ~2 KB / deserialize komplex objektumoknál</p>
</div>
<div class="optimization-item low">
<h4>
<span class="impact-badge impact-low">ALACSONY HATÁS</span>
5. Two-pass serialize elkerülése NoRef módban
</h4>
<p>
NoRef módban is fut a <code>CollectPropertyNames</code> fázis a metaadathoz.
Ha a típus ismert és stabil, a property nevek elõre cache-elhetõk.
</p>
<p><strong>Becsült megtakarítás:</strong> ~10% sebesség javulás NoRef serialize-nál</p>
</div>
<div class="optimization-item low">
<h4>
<span class="impact-badge impact-low">ALACSONY HATÁS</span>
6. Span-based enum serialization
</h4>
<p>
A <code>Convert.ToInt32(value)</code> enum értékeknél boxing-ot okoz.
Típusos enum kezelés vagy <code>Unsafe.As</code> használatával elkerülhetõ.
</p>
<p><strong>Becsült megtakarítás:</strong> ~24 byte / enum érték</p>
</div>
</div>
<!-- Funkció összehasonlítás -->
<div class="card">
<h2>?? Funkció Összehasonlítás</h2>
<table>
<thead>
<tr>
<th>Funkció</th>
<th style="text-align: center;">AcBinary</th>
<th style="text-align: center;">MessagePack</th>
<th>Megjegyzés</th>
</tr>
</thead>
<tbody>
<tr>
<td>Reference Handling ($id/$ref)</td>
<td style="text-align: center;">?</td>
<td style="text-align: center;">?</td>
<td>Közös objektumok deduplikálása</td>
</tr>
<tr>
<td>Populate (meglévõ objektum)</td>
<td style="text-align: center;">?</td>
<td style="text-align: center;">?</td>
<td>Létezõ objektum frissítése</td>
</tr>
<tr>
<td>PopulateMerge (IId merge)</td>
<td style="text-align: center;">?</td>
<td style="text-align: center;">?</td>
<td>Lista elemek ID alapú merge</td>
</tr>
<tr>
<td>Schema Evolution</td>
<td style="text-align: center;">?</td>
<td style="text-align: center;">??</td>
<td>Property név alapú mapping</td>
</tr>
<tr>
<td>Metadata Table</td>
<td style="text-align: center;">?</td>
<td style="text-align: center;">?</td>
<td>Property nevek indexelése</td>
</tr>
<tr>
<td>String Interning</td>
<td style="text-align: center;">?</td>
<td style="text-align: center;">?</td>
<td>Ismétlõdõ stringek deduplikálása</td>
</tr>
<tr>
<td>Kompakt méret</td>
<td style="text-align: center;">??</td>
<td style="text-align: center;">?</td>
<td>MsgPack ~40% kisebb</td>
</tr>
<tr>
<td>Serialize sebesség</td>
<td style="text-align: center;">??</td>
<td style="text-align: center;">?</td>
<td>MsgPack 3-4× gyorsabb</td>
</tr>
<tr>
<td>Deserialize sebesség</td>
<td style="text-align: center;">?</td>
<td style="text-align: center;">??</td>
<td>AcBinary akár 40× gyorsabb</td>
</tr>
</tbody>
</table>
</div>
<!-- Ajánlások -->
<div class="card">
<h2>?? Mikor Melyiket Használd?</h2>
<div class="summary-grid">
<div class="summary-item" style="border-left-color: var(--primary);">
<h4>?? AcBinary ajánlott</h4>
<ul>
<li>Deserialize-heavy workload (kliens oldal)</li>
<li>Populate/Merge szükséges</li>
<li>Reference handling kell (shared objects)</li>
<li>Schema változások várhatóak</li>
<li>NoRef mód használható (40× gyorsabb!)</li>
</ul>
</div>
<div class="summary-item" style="border-left-color: #f97316;">
<h4>?? MessagePack ajánlott</h4>
<ul>
<li>Serialize-heavy workload (szerver oldal)</li>
<li>Méret kritikus (hálózati átvitel)</li>
<li>Egyszerû objektumok (nincs referencia)</li>
<li>Külsõ rendszerekkel kompatibilitás</li>
<li>Minimális GC pressure kell</li>
</ul>
</div>
</div>
</div>
<!-- Kiemelt eredmények -->
<div class="card">
<h2>?? Kiemelt Eredmények</h2>
<div class="stats-grid">
<div class="stat-card success">
<div class="value">40×</div>
<div class="label">AcBinary NoRef Deserialize gyorsabb</div>
</div>
<div class="stat-card success">
<div class="value">9×</div>
<div class="label">Kevesebb memória (NoRef Deser.)</div>
</div>
<div class="stat-card warning">
<div class="value">3.7×</div>
<div class="label">MsgPack Serialize gyorsabb</div>
</div>
<div class="stat-card warning">
<div class="value">4.4×</div>
<div class="label">Több memória (Serialize WithRef)</div>
</div>
</div>
</div>
<!-- Összegzés -->
<div class="card">
<h2>?? Összegzés</h2>
<div class="note">
<p><strong>AcBinary erõsségei:</strong></p>
<ul style="margin: 0.5rem 0 0.5rem 1.5rem;">
<li>Kiemelkedõen gyors deserializálás, különösen NoRef módban (40× gyorsabb)</li>
<li>Beépített reference handling és populate/merge támogatás</li>
<li>Schema evolution friendly (property név alapú)</li>
</ul>
<br>
<p><strong>Fejlesztendõ területek:</strong></p>
<ul style="margin: 0.5rem 0 0.5rem 1.5rem;">
<li>Serialize sebesség (3-4× lassabb MessagePack-nél)</li>
<li>Memória allokáció serialize-nál (4.4× több)</li>
<li>Output méret (~68% nagyobb)</li>
</ul>
<br>
<p><strong>Javasolt következõ lépések prioritás szerint:</strong></p>
<ol style="margin: 0.5rem 0 0 1.5rem;">
<li>IBufferWriter&lt;byte&gt; támogatás hozzáadása</li>
<li>Typed property getter-ek boxing elkerülésére</li>
<li>Reference tracking collection pooling optimalizálása</li>
</ol>
</div>
</div>
</div>
<footer>
AcBinary Serializer Benchmark Report | AyCode.Core | 2024
</footer>
</body>
</html>