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.
This commit is contained in:
parent
74b4bbfd30
commit
3b5a895fbc
|
|
@ -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
|
|||
|
||||
/// <summary>
|
||||
/// Complex hierarchy benchmark - AcBinary vs JSON only (no MessagePack to isolate the issue).
|
||||
/// Uses AcBinary without reference handling.
|
||||
/// </summary>
|
||||
[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
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Full comparison with MessagePack - separate class to isolate potential issues.
|
||||
/// Full comparison with MessagePack and BSON - AcBinary uses NO reference handling everywhere.
|
||||
/// </summary>
|
||||
[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<byte>();
|
||||
}
|
||||
|
||||
// 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<byte>();
|
||||
}
|
||||
|
||||
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<TestOrder>(_acBinaryData);
|
||||
|
||||
[Benchmark(Description = "MessagePack Deserialize")]
|
||||
public TestOrder? Deserialize_MsgPack() => MessagePackSerializer.Deserialize<TestOrder>(_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<TestOrder>(reader);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Comprehensive AcBinary vs MessagePack comparison benchmark.
|
||||
/// Tests: WithRef, NoRef, Populate, Serialize, Deserialize, Size
|
||||
/// Tests: NoRef (everywhere), Populate, Serialize, Deserialize, Size
|
||||
/// </summary>
|
||||
[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<byte>();
|
||||
}
|
||||
|
||||
// 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<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);
|
||||
|
||||
[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<TestOrder>(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
|
|||
|
||||
/// <summary>
|
||||
/// Detailed size comparison - not a performance benchmark, just size output.
|
||||
/// Now includes BSON size output and uses AcBinary without reference handling.
|
||||
/// </summary>
|
||||
[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<byte>(); }
|
||||
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -42,48 +42,49 @@ public class AcBinaryDeserializationException : Exception
|
|||
public static class AcBinaryDeserializer
|
||||
{
|
||||
private static readonly ConcurrentDictionary<Type, BinaryDeserializeTypeMetadata> TypeMetadataCache = new();
|
||||
private static readonly ConcurrentDictionary<Type, TypeConversionInfo> TypeConversionCache = 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;
|
||||
|
||||
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<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();
|
||||
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
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -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<T> */ }
|
||||
catch
|
||||
{
|
||||
/* Fallback to List<T> */
|
||||
}
|
||||
|
||||
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<string, BinaryPropertySetterInfo> _propertiesDict;
|
||||
private readonly FrozenDictionary<string, BinaryPropertySetterInfo> _propertiesDict;
|
||||
public BinaryPropertySetterInfo[] PropertiesArray { get; }
|
||||
public Func<object>? CompiledConstructor { get; }
|
||||
|
||||
|
|
@ -1252,16 +1272,20 @@ public static class AcBinaryDeserializer
|
|||
propsList.Add(p);
|
||||
}
|
||||
|
||||
_propertiesDict = new Dictionary<string, BinaryPropertySetterInfo>(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<string, BinaryPropertySetterInfo>(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; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 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<int> bits = stackalloc int[4];
|
||||
MemoryMarshal.Cast<byte, int>(_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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue