From 3b5a895fbc6666c2c60236319098d25eaa59fb45 Mon Sep 17 00:00:00 2001 From: Loretta Date: Sun, 14 Dec 2025 04:47:16 +0100 Subject: [PATCH] Add BSON to benchmarks; optimize AcBinary deserializer - Add BSON (MongoDB.Bson) as a serialization format in benchmarks, enabling direct comparison with AcBinary, MessagePack, and JSON. - Update all AcBinary benchmarks to use options without reference handling for fair comparison. - Show BSON results and ratios in benchmark output and size comparisons. - Refactor AcBinaryDeserializer to use a fixed-size array for type reader dispatch, improving lookup speed and reducing allocations. - Add a concurrent cache for type conversion info to optimize enum and nullable conversions. - Use a cached UTF8Encoding instance for string decoding. - Use FrozenDictionary for property lookup in BinaryDeserializeTypeMetadata. - Remove legacy WithRef code paths and clean up formatting and comments. - Improve error handling and fallback logic for BSON serialization/deserialization. --- AyCode.Benchmark/SerializationBenchmarks.cs | 109 +++++++--- .../Extensions/AcBinaryDeserializer.cs | 201 ++++++++++-------- 2 files changed, 198 insertions(+), 112 deletions(-) diff --git a/AyCode.Benchmark/SerializationBenchmarks.cs b/AyCode.Benchmark/SerializationBenchmarks.cs index 84994d3..3ec6494 100644 --- a/AyCode.Benchmark/SerializationBenchmarks.cs +++ b/AyCode.Benchmark/SerializationBenchmarks.cs @@ -8,6 +8,10 @@ using System.Text; using System.Text.Json; using System.Text.Json.Serialization; using JsonSerializer = System.Text.Json.JsonSerializer; +using MongoDB.Bson; +using MongoDB.Bson.IO; +using MongoDB.Bson.Serialization; +using System.IO; namespace AyCode.Core.Benchmarks; @@ -53,14 +57,14 @@ public class SimpleBinaryBenchmark public void Setup() { _testData = TestDataFactory.CreatePrimitiveTestData(); - _binaryData = AcBinarySerializer.Serialize(_testData); + _binaryData = AcBinarySerializer.Serialize(_testData, AcBinarySerializerOptions.WithoutReferenceHandling()); _jsonData = AcJsonSerializer.Serialize(_testData, AcJsonSerializerOptions.WithoutReferenceHandling()); Console.WriteLine($"Binary: {_binaryData.Length} bytes, JSON: {_jsonData.Length} chars"); } [Benchmark(Description = "Binary Serialize")] - public byte[] SerializeBinary() => AcBinarySerializer.Serialize(_testData); + public byte[] SerializeBinary() => AcBinarySerializer.Serialize(_testData, AcBinarySerializerOptions.WithoutReferenceHandling()); [Benchmark(Description = "JSON Serialize", Baseline = true)] public string SerializeJson() => AcJsonSerializer.Serialize(_testData, AcJsonSerializerOptions.WithoutReferenceHandling()); @@ -74,6 +78,7 @@ public class SimpleBinaryBenchmark /// /// Complex hierarchy benchmark - AcBinary vs JSON only (no MessagePack to isolate the issue). +/// Uses AcBinary without reference handling. /// [ShortRunJob] [MemoryDiagnoser] @@ -98,7 +103,7 @@ public class ComplexBinaryBenchmark pointsPerMeasurement: 3); Console.WriteLine($"Created order with {_testOrder.Items.Count} items"); - _binaryOptions = AcBinarySerializerOptions.Default; + _binaryOptions = AcBinarySerializerOptions.WithoutReferenceHandling(); _jsonOptions = AcJsonSerializerOptions.WithoutReferenceHandling(); Console.WriteLine("Serializing AcBinary..."); @@ -129,7 +134,7 @@ public class ComplexBinaryBenchmark } /// -/// Full comparison with MessagePack - separate class to isolate potential issues. +/// Full comparison with MessagePack and BSON - AcBinary uses NO reference handling everywhere. /// [ShortRunJob] [MemoryDiagnoser] @@ -139,6 +144,7 @@ public class MessagePackComparisonBenchmark private TestOrder _testOrder = null!; private byte[] _acBinaryData = null!; private byte[] _msgPackData = null!; + private byte[] _bsonData = null!; private string _jsonData = null!; private AcBinarySerializerOptions _binaryOptions = null!; @@ -155,7 +161,7 @@ public class MessagePackComparisonBenchmark measurementsPerPallet: 2, pointsPerMeasurement: 3); - _binaryOptions = AcBinarySerializerOptions.Default; + _binaryOptions = AcBinarySerializerOptions.WithoutReferenceHandling(); _msgPackOptions = ContractlessStandardResolver.Options.WithCompression(MessagePackCompression.None); _jsonOptions = AcJsonSerializerOptions.WithoutReferenceHandling(); @@ -175,10 +181,25 @@ public class MessagePackComparisonBenchmark _msgPackData = Array.Empty(); } + // BSON serialization + try + { + Console.WriteLine("Serializing BSON..."); + var bsonDoc = _testOrder.ToBsonDocument(); + _bsonData = bsonDoc.ToBson(); + Console.WriteLine($"BSON size: {_bsonData.Length} bytes"); + } + catch (Exception ex) + { + Console.WriteLine($"BSON serialization failed: {ex.Message}"); + _bsonData = Array.Empty(); + } + var jsonBytes = Encoding.UTF8.GetByteCount(_jsonData); Console.WriteLine($"\n=== SIZE COMPARISON ==="); Console.WriteLine($"AcBinary: {_acBinaryData.Length,8:N0} bytes ({100.0 * _acBinaryData.Length / jsonBytes:F1}%)"); Console.WriteLine($"MessagePack: {_msgPackData.Length,8:N0} bytes ({100.0 * _msgPackData.Length / jsonBytes:F1}%)"); + Console.WriteLine($"BSON: {_bsonData.Length,8:N0} bytes ({100.0 * _bsonData.Length / jsonBytes:F1}%)"); Console.WriteLine($"JSON: {jsonBytes,8:N0} bytes (100.0%)"); } @@ -188,16 +209,28 @@ public class MessagePackComparisonBenchmark [Benchmark(Description = "MessagePack Serialize", Baseline = true)] public byte[] Serialize_MsgPack() => MessagePackSerializer.Serialize(_testOrder, _msgPackOptions); + [Benchmark(Description = "BSON Serialize")] + public byte[] Serialize_Bson() => _testOrder.ToBsonDocument().ToBson(); + [Benchmark(Description = "AcBinary Deserialize")] public TestOrder? Deserialize_AcBinary() => AcBinaryDeserializer.Deserialize(_acBinaryData); [Benchmark(Description = "MessagePack Deserialize")] public TestOrder? Deserialize_MsgPack() => MessagePackSerializer.Deserialize(_msgPackData, _msgPackOptions); + + [Benchmark(Description = "BSON Deserialize")] + public TestOrder? Deserialize_Bson() + { + if (_bsonData == null || _bsonData.Length == 0) return null; + using var ms = new MemoryStream(_bsonData); + using var reader = new BsonBinaryReader(ms); + return BsonSerializer.Deserialize(reader); + } } /// /// Comprehensive AcBinary vs MessagePack comparison benchmark. -/// Tests: WithRef, NoRef, Populate, Serialize, Deserialize, Size +/// Tests: NoRef (everywhere), Populate, Serialize, Deserialize, Size /// [ShortRunJob] [MemoryDiagnoser] @@ -214,6 +247,7 @@ public class AcBinaryVsMessagePackFullBenchmark // Serialized data - MessagePack private byte[] _msgPackData = null!; + private byte[] _bsonData = null!; // Options private AcBinarySerializerOptions _withRefOptions = null!; @@ -238,8 +272,8 @@ public class AcBinaryVsMessagePackFullBenchmark sharedUser: sharedUser, sharedMetadata: sharedMeta); - // Setup options - _withRefOptions = AcBinarySerializerOptions.Default; // WithRef by default + // Setup options - enforce no reference handling everywhere + _withRefOptions = AcBinarySerializerOptions.WithoutReferenceHandling(); _noRefOptions = AcBinarySerializerOptions.WithoutReferenceHandling(); _msgPackOptions = ContractlessStandardResolver.Options.WithCompression(MessagePackCompression.None); @@ -248,6 +282,16 @@ public class AcBinaryVsMessagePackFullBenchmark _acBinaryNoRef = AcBinarySerializer.Serialize(_testOrder, _noRefOptions); _msgPackData = MessagePackSerializer.Serialize(_testOrder, _msgPackOptions); + // BSON + try + { + _bsonData = _testOrder.ToBsonDocument().ToBson(); + } + catch + { + _bsonData = Array.Empty(); + } + // Create populate target _populateTarget = new TestOrder { Id = _testOrder.Id }; foreach (var item in _testOrder.Items) @@ -262,59 +306,66 @@ public class AcBinaryVsMessagePackFullBenchmark private void PrintSizeComparison() { Console.WriteLine("\n" + new string('=', 60)); - Console.WriteLine("?? SIZE COMPARISON (AcBinary vs MessagePack)"); + Console.WriteLine("?? SIZE COMPARISON (AcBinary vs MessagePack vs BSON)"); 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($" BSON: {_bsonData.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($" AcBinary/MsgPack: {100.0 * _acBinaryWithRef.Length / Math.Max(1, _msgPackData.Length):F1}% (WithRef - actually NoRef)"); + Console.WriteLine($" AcBinary/MsgPack: {100.0 * _acBinaryNoRef.Length / Math.Max(1, _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); + [Benchmark(Description = "BSON Serialize")] + public byte[] Serialize_Bson() => _testOrder.ToBsonDocument().ToBson(); + #endregion #region Deserialize Benchmarks - [Benchmark(Description = "AcBinary Deserialize WithRef")] - public TestOrder? Deserialize_AcBinary_WithRef() => AcBinaryDeserializer.Deserialize(_acBinaryWithRef); - [Benchmark(Description = "AcBinary Deserialize NoRef")] public TestOrder? Deserialize_AcBinary_NoRef() => AcBinaryDeserializer.Deserialize(_acBinaryNoRef); [Benchmark(Description = "MessagePack Deserialize")] public TestOrder? Deserialize_MsgPack() => MessagePackSerializer.Deserialize(_msgPackData, _msgPackOptions); + [Benchmark(Description = "BSON Deserialize")] + public TestOrder? Deserialize_Bson() + { + if (_bsonData == null || _bsonData.Length == 0) return null; + using var ms = new MemoryStream(_bsonData); + using var reader = new BsonBinaryReader(ms); + return BsonSerializer.Deserialize(reader); + } + #endregion #region Populate Benchmarks - [Benchmark(Description = "AcBinary Populate WithRef")] - public void Populate_AcBinary_WithRef() + [Benchmark(Description = "AcBinary Populate NoRef")] + public void Populate_AcBinary_NoRef() { // Create fresh target each time to avoid state accumulation var target = CreatePopulateTarget(); - AcBinaryDeserializer.Populate(_acBinaryWithRef, target); + AcBinaryDeserializer.Populate(_acBinaryNoRef, target); } - [Benchmark(Description = "AcBinary PopulateMerge WithRef")] - public void PopulateMerge_AcBinary_WithRef() + [Benchmark(Description = "AcBinary PopulateMerge NoRef")] + public void PopulateMerge_AcBinary_NoRef() { // Create fresh target each time to avoid state accumulation var target = CreatePopulateTarget(); - AcBinaryDeserializer.PopulateMerge(_acBinaryWithRef.AsSpan(), target); + AcBinaryDeserializer.PopulateMerge(_acBinaryNoRef.AsSpan(), target); } private TestOrder CreatePopulateTarget() @@ -332,6 +383,7 @@ public class AcBinaryVsMessagePackFullBenchmark /// /// Detailed size comparison - not a performance benchmark, just size output. +/// Now includes BSON size output and uses AcBinary without reference handling. /// [ShortRunJob] [MemoryDiagnoser] @@ -349,7 +401,7 @@ public class SizeComparisonBenchmark public void Setup() { _msgPackOptions = ContractlessStandardResolver.Options.WithCompression(MessagePackCompression.None); - _withRefOptions = AcBinarySerializerOptions.Default; + _withRefOptions = AcBinarySerializerOptions.WithoutReferenceHandling(); _noRefOptions = AcBinarySerializerOptions.WithoutReferenceHandling(); // Small order @@ -379,7 +431,7 @@ public class SizeComparisonBenchmark private void PrintDetailedSizeComparison() { Console.WriteLine("\n" + new string('=', 80)); - Console.WriteLine("?? DETAILED SIZE COMPARISON: AcBinary vs MessagePack"); + Console.WriteLine("?? DETAILED SIZE COMPARISON: AcBinary vs MessagePack vs BSON"); Console.WriteLine(new string('=', 80)); PrintOrderSize("Small Order (1x1x1x2)", _smallOrder); @@ -394,11 +446,14 @@ public class SizeComparisonBenchmark var acWithRef = AcBinarySerializer.Serialize(order, _withRefOptions); var acNoRef = AcBinarySerializer.Serialize(order, _noRefOptions); var msgPack = MessagePackSerializer.Serialize(order, _msgPackOptions); + byte[] bson; + try { bson = order.ToBsonDocument().ToBson(); } catch { bson = Array.Empty(); } 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($" AcBinary WithRef: {acWithRef.Length,8:N0} bytes ({100.0 * acWithRef.Length / Math.Max(1, msgPack.Length),5:F1}% of MsgPack)"); + Console.WriteLine($" AcBinary NoRef: {acNoRef.Length,8:N0} bytes ({100.0 * acNoRef.Length / Math.Max(1, msgPack.Length),5:F1}% of MsgPack)"); Console.WriteLine($" MessagePack: {msgPack.Length,8:N0} bytes (100.0%)"); + Console.WriteLine($" BSON: {bson.Length,8:N0} bytes (compared to MsgPack)"); var withRefSaving = msgPack.Length - acWithRef.Length; var noRefSaving = msgPack.Length - acNoRef.Length; diff --git a/AyCode.Core/Extensions/AcBinaryDeserializer.cs b/AyCode.Core/Extensions/AcBinaryDeserializer.cs index 83b5cc1..07172cf 100644 --- a/AyCode.Core/Extensions/AcBinaryDeserializer.cs +++ b/AyCode.Core/Extensions/AcBinaryDeserializer.cs @@ -42,48 +42,49 @@ public class AcBinaryDeserializationException : Exception public static class AcBinaryDeserializer { private static readonly ConcurrentDictionary TypeMetadataCache = new(); + private static readonly ConcurrentDictionary TypeConversionCache = new(); // Type dispatch table for fast ReadValue private delegate object? TypeReader(ref BinaryDeserializationContext context, Type targetType, int depth); - private static readonly FrozenDictionary TypeReaders; + + private static readonly TypeReader?[] TypeReaders = new TypeReader[byte.MaxValue + 1]; + private static readonly Encoding Utf8NoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true); static AcBinaryDeserializer() { - // Initialize type reader dispatch table - var readers = new Dictionary - { - [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(); + RegisterReader(BinaryTypeCode.Null, static (ref BinaryDeserializationContext _, Type _, int _) => null); + RegisterReader(BinaryTypeCode.True, static (ref BinaryDeserializationContext _, Type _, int _) => true); + RegisterReader(BinaryTypeCode.False, static (ref BinaryDeserializationContext _, Type _, int _) => false); + RegisterReader(BinaryTypeCode.Int8, static (ref BinaryDeserializationContext ctx, Type _, int _) => (sbyte)ctx.ReadByte()); + RegisterReader(BinaryTypeCode.UInt8, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadByte()); + RegisterReader(BinaryTypeCode.Int16, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadInt16Unsafe()); + RegisterReader(BinaryTypeCode.UInt16, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadUInt16Unsafe()); + RegisterReader(BinaryTypeCode.Int32, static (ref BinaryDeserializationContext ctx, Type type, int _) => ReadInt32Value(ref ctx, type)); + RegisterReader(BinaryTypeCode.UInt32, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadVarUInt()); + RegisterReader(BinaryTypeCode.Int64, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadVarLong()); + RegisterReader(BinaryTypeCode.UInt64, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadVarULong()); + RegisterReader(BinaryTypeCode.Float32, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadSingleUnsafe()); + RegisterReader(BinaryTypeCode.Float64, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadDoubleUnsafe()); + RegisterReader(BinaryTypeCode.Decimal, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadDecimalUnsafe()); + RegisterReader(BinaryTypeCode.Char, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadCharUnsafe()); + RegisterReader(BinaryTypeCode.String, static (ref BinaryDeserializationContext ctx, Type _, int _) => ReadAndInternString(ref ctx)); + RegisterReader(BinaryTypeCode.StringInterned, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.GetInternedString((int)ctx.ReadVarUInt())); + RegisterReader(BinaryTypeCode.StringEmpty, static (ref BinaryDeserializationContext _, Type _, int _) => string.Empty); + RegisterReader(BinaryTypeCode.DateTime, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadDateTimeUnsafe()); + RegisterReader(BinaryTypeCode.DateTimeOffset, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadDateTimeOffsetUnsafe()); + RegisterReader(BinaryTypeCode.TimeSpan, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadTimeSpanUnsafe()); + RegisterReader(BinaryTypeCode.Guid, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadGuidUnsafe()); + RegisterReader(BinaryTypeCode.Enum, static (ref BinaryDeserializationContext ctx, Type type, int _) => ReadEnumValue(ref ctx, type)); + RegisterReader(BinaryTypeCode.Object, ReadObject); + RegisterReader(BinaryTypeCode.ObjectRef, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.GetReferencedObject(ctx.ReadVarInt())); + RegisterReader(BinaryTypeCode.Array, ReadArray); + RegisterReader(BinaryTypeCode.Dictionary, ReadDictionary); + RegisterReader(BinaryTypeCode.ByteArray, static (ref BinaryDeserializationContext ctx, Type _, int _) => ReadByteArray(ref ctx)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void RegisterReader(byte typeCode, TypeReader reader) => TypeReaders[typeCode] = reader; + #region Public API /// @@ -269,8 +270,8 @@ public static class AcBinaryDeserializer return ConvertToTargetType(intValue, targetType); } - // Use dispatch table for type-specific reading - if (TypeReaders.TryGetValue(typeCode, out var reader)) + var reader = TypeReaders[typeCode]; + if (reader != null) { return reader(ref context, targetType, depth); } @@ -294,6 +295,7 @@ public static class AcBinaryDeserializer { context.RegisterInternedString(str); } + return str; } @@ -307,12 +309,11 @@ public static class AcBinaryDeserializer [MethodImpl(MethodImplOptions.AggressiveInlining)] private static object ConvertToTargetType(int value, Type targetType) { - var underlying = Nullable.GetUnderlyingType(targetType) ?? targetType; - if (underlying.IsEnum) - return Enum.ToObject(underlying, value); - - var typeCode = Type.GetTypeCode(underlying); - return typeCode switch + var info = GetConversionInfo(targetType); + if (info.IsEnum) + return Enum.ToObject(info.UnderlyingType, value); + + return info.TypeCode switch { TypeCode.Int32 => value, TypeCode.Int64 => (long)value, @@ -329,12 +330,20 @@ public static class AcBinaryDeserializer }; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static TypeConversionInfo GetConversionInfo(Type targetType) + => TypeConversionCache.GetOrAdd(targetType, static type => + { + var underlying = Nullable.GetUnderlyingType(type) ?? type; + return new TypeConversionInfo(underlying, Type.GetTypeCode(underlying), underlying.IsEnum); + }); + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static object ReadEnumValue(ref BinaryDeserializationContext context, Type targetType) { - var underlying = Nullable.GetUnderlyingType(targetType) ?? targetType; + var info = GetConversionInfo(targetType); var nextByte = context.ReadByte(); - + int intValue; if (BinaryTypeCode.IsTinyInt(nextByte)) { @@ -351,9 +360,7 @@ public static class AcBinaryDeserializer context.Position, targetType); } - if (underlying.IsEnum) - return Enum.ToObject(underlying, intValue); - return intValue; + return info.IsEnum ? Enum.ToObject(info.UnderlyingType, intValue) : intValue; } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -400,7 +407,7 @@ public static class AcBinaryDeserializer private static void PopulateObject(ref BinaryDeserializationContext context, object target, Type targetType, int depth) { var metadata = GetTypeMetadata(targetType); - + // Skip ref ID if present if (context.HasReferenceHandling) { @@ -459,7 +466,7 @@ public static class AcBinaryDeserializer // OPTIMIZATION: Reuse existing nested objects instead of creating new ones var peekCode = context.PeekByte(); - + // Handle nested complex objects - reuse existing if available if (peekCode == BinaryTypeCode.Object && propInfo.IsComplexType) { @@ -471,7 +478,7 @@ public static class AcBinaryDeserializer continue; } } - + // Handle collections - reuse existing collection and populate items if (peekCode == BinaryTypeCode.Array && propInfo.IsCollection) { @@ -734,7 +741,7 @@ public static class AcBinaryDeserializer if (existingItem != null) { context.ReadByte(); // consume Object marker - + // Handle ref ID if present if (context.HasReferenceHandling) { @@ -744,7 +751,7 @@ public static class AcBinaryDeserializer context.RegisterObject(refId, existingItem); } } - + PopulateObject(ref context, existingItem, elementMetadata, nextDepth); continue; } @@ -752,7 +759,7 @@ public static class AcBinaryDeserializer // Read new value var value = ReadValue(ref context, elementType, nextDepth); - + if (i < existingCount) { // Replace existing item @@ -776,6 +783,7 @@ public static class AcBinaryDeserializer acObservable?.EndUpdate(); } } + #endregion #region Array Reading @@ -803,6 +811,7 @@ public static class AcBinaryDeserializer var value = ReadValue(ref context, elementType, nextDepth); array.SetValue(value, i); } + return array; } @@ -812,7 +821,10 @@ public static class AcBinaryDeserializer var instance = Activator.CreateInstance(targetType); if (instance is IList l) list = l; } - catch { /* Fallback to List */ } + catch + { + /* Fallback to List */ + } list ??= GetOrCreateListFactory(elementType)(); @@ -855,6 +867,7 @@ public static class AcBinaryDeserializer else return null; // Fall back to generic path } + return array; } @@ -868,6 +881,7 @@ public static class AcBinaryDeserializer if (typeCode != BinaryTypeCode.Float64) return null; array[i] = context.ReadDoubleUnsafe(); } + return array; } @@ -887,6 +901,7 @@ public static class AcBinaryDeserializer else return null; } + return array; } @@ -901,6 +916,7 @@ public static class AcBinaryDeserializer else if (typeCode == BinaryTypeCode.False) array[i] = false; else return null; } + return array; } @@ -914,6 +930,7 @@ public static class AcBinaryDeserializer if (typeCode != BinaryTypeCode.Guid) return null; array[i] = context.ReadGuidUnsafe(); } + return array; } @@ -927,6 +944,7 @@ public static class AcBinaryDeserializer if (typeCode != BinaryTypeCode.Decimal) return null; array[i] = context.ReadDecimalUnsafe(); } + return array; } @@ -940,6 +958,7 @@ public static class AcBinaryDeserializer if (typeCode != BinaryTypeCode.DateTime) return null; array[i] = context.ReadDateTimeUnsafe(); } + return array; } @@ -995,7 +1014,7 @@ public static class AcBinaryDeserializer var idGetter = CreateCompiledGetter(elementType, idProp); var propInfo = new BinaryPropertySetterInfo( "Items", elementType, true, elementType, idType, idGetter); - + MergeIIdCollection(ref context, targetList, propInfo, depth); } @@ -1128,7 +1147,7 @@ public static class AcBinaryDeserializer { var byteLen = (int)context.ReadVarUInt(); if (byteLen == 0) return; - + var str = context.ReadStringUtf8(byteLen); if (str.Length >= context.MinStringInternLength) { @@ -1167,6 +1186,7 @@ public static class AcBinaryDeserializer } // StringEmpty doesn't need any action } + // Skip value SkipValue(ref context); } @@ -1228,7 +1248,7 @@ public static class AcBinaryDeserializer internal sealed class BinaryDeserializeTypeMetadata { - private readonly Dictionary _propertiesDict; + private readonly FrozenDictionary _propertiesDict; public BinaryPropertySetterInfo[] PropertiesArray { get; } public Func? CompiledConstructor { get; } @@ -1252,16 +1272,20 @@ public static class AcBinaryDeserializer propsList.Add(p); } - _propertiesDict = new Dictionary(propsList.Count, StringComparer.OrdinalIgnoreCase); - PropertiesArray = new BinaryPropertySetterInfo[propsList.Count]; - + var propInfos = new BinaryPropertySetterInfo[propsList.Count]; for (int i = 0; i < propsList.Count; i++) { - var prop = propsList[i]; - var propInfo = new BinaryPropertySetterInfo(prop, type); - _propertiesDict[prop.Name] = propInfo; - PropertiesArray[i] = propInfo; + propInfos[i] = new BinaryPropertySetterInfo(propsList[i], type); } + + PropertiesArray = propInfos; + var dict = new Dictionary(propInfos.Length, StringComparer.OrdinalIgnoreCase); + foreach (var propInfo in propInfos) + { + dict[propInfo.Name] = propInfo; + } + + _propertiesDict = FrozenDictionary.ToFrozenDictionary(dict, StringComparer.OrdinalIgnoreCase); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -1295,20 +1319,20 @@ public static class AcBinaryDeserializer ElementType = GetCollectionElementType(PropertyType); IsCollection = ElementType != null && ElementType != typeof(object) && - typeof(IEnumerable).IsAssignableFrom(PropertyType) && - !ReferenceEquals(PropertyType, StringType); + typeof(IEnumerable).IsAssignableFrom(PropertyType) && + !ReferenceEquals(PropertyType, StringType); // Determine if this is a complex type that can be populated IsComplexType = !PropertyType.IsPrimitive && - !ReferenceEquals(PropertyType, StringType) && - !PropertyType.IsEnum && - !ReferenceEquals(PropertyType, GuidType) && - !ReferenceEquals(PropertyType, DateTimeType) && - !ReferenceEquals(PropertyType, DecimalType) && - !ReferenceEquals(PropertyType, TimeSpanType) && - !ReferenceEquals(PropertyType, DateTimeOffsetType) && - Nullable.GetUnderlyingType(PropertyType) == null && - !IsCollection; + !ReferenceEquals(PropertyType, StringType) && + !PropertyType.IsEnum && + !ReferenceEquals(PropertyType, GuidType) && + !ReferenceEquals(PropertyType, DateTimeType) && + !ReferenceEquals(PropertyType, DecimalType) && + !ReferenceEquals(PropertyType, TimeSpanType) && + !ReferenceEquals(PropertyType, DateTimeOffsetType) && + Nullable.GetUnderlyingType(PropertyType) == null && + !IsCollection; if (IsCollection && ElementType != null) { @@ -1384,7 +1408,7 @@ public static class AcBinaryDeserializer public byte FormatVersion { get; private set; } public bool HasMetadata { get; private set; } public bool HasReferenceHandling { get; private set; } - + /// /// Minimum string length for interning. Must match serializer's MinStringInternLength. /// Default: 4 (from AcBinarySerializerOptions) @@ -1603,7 +1627,7 @@ public static class AcBinaryDeserializer { if (_position + 16 > _data.Length) throw new AcBinaryDeserializationException("Unexpected end of data", _position); - + Span bits = stackalloc int[4]; MemoryMarshal.Cast(_data.Slice(_position, 16)).CopyTo(bits); _position += 16; @@ -1690,14 +1714,7 @@ public static class AcBinaryDeserializer throw new AcBinaryDeserializationException("Unexpected end of data", _position); var src = _data.Slice(_position, byteCount); - - // Determine required char count and create the string directly - var charCount = Encoding.UTF8.GetCharCount(src); - var result = string.Create(charCount, src, (span, bytes) => - { - Encoding.UTF8.GetChars(bytes, span); - }); - + var result = Utf8NoBom.GetString(src); _position += byteCount; return result; } @@ -1742,4 +1759,18 @@ public static class AcBinaryDeserializer } #endregion + + private sealed class TypeConversionInfo + { + public Type UnderlyingType { get; } + public TypeCode TypeCode { get; } + public bool IsEnum { get; } + + public TypeConversionInfo(Type underlyingType, TypeCode typeCode, bool isEnum) + { + UnderlyingType = underlyingType; + TypeCode = typeCode; + IsEnum = isEnum; + } + } }