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:
parent
6faed09f9f
commit
f69b14c195
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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<object,int></code></td>
|
||||
<td class="number">~8 KB</td>
|
||||
<td>Reference scanning: _scanOccurrences, _writtenRefs</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>HashSet<object></code></td>
|
||||
<td class="number">~4 KB</td>
|
||||
<td>Multi-referenced objektumok nyilvántartása</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>Dictionary<string,int></code></td>
|
||||
<td class="number">~6 KB</td>
|
||||
<td>Property name table + string interning</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>List<string></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<byte> alapú Serialize
|
||||
</h4>
|
||||
<p>
|
||||
A <code>ToArray()</code> hívás ~19 KB allokációt okoz. Ehelyett <code>IBufferWriter<byte></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<object, object?></code> getter minden value type-ot boxol.
|
||||
Típusos getter-ek (<code>Func<T, int></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<byte> 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>
|
||||
Loading…
Reference in New Issue