Compare commits

...

3 Commits

Author SHA1 Message Date
Loretta 5601c0d3e2 Refactor serialization core, add pooled buffer support
Centralize serialization steps into a new SerializeCore method for consistency and maintainability. Rework property metadata registration to operate on types instead of instances, improving efficiency. Replace property index tracking with stack-allocated or pooled buffers to reduce allocations. Add SerializeToPooledBuffer and BinarySerializationResult for zero-copy serialization with proper buffer pooling and disposal. Simplify string writing logic and use GC.AllocateUninitializedArray for result arrays. Refactor and add helper methods for buffer management and metadata handling.
2025-12-14 10:20:07 +01:00
Loretta 3b5a895fbc 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.
2025-12-14 04:47:16 +01:00
Loretta 74b4bbfd30 Add size_output.txt to .gitignore
Prevent size_output.txt from being tracked by Git by adding it to the .gitignore file. This helps keep the repository clean of generated or temporary files.
2025-12-14 04:46:54 +01:00
4 changed files with 486 additions and 240 deletions

1
.gitignore vendored
View File

@ -376,3 +376,4 @@ FodyWeavers.xsd
/BenchmarkSuite1/Results /BenchmarkSuite1/Results
/CoverageReport /CoverageReport
/Test_Benchmark_Results /Test_Benchmark_Results
/size_output.txt

View File

@ -8,6 +8,10 @@ using System.Text;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using JsonSerializer = System.Text.Json.JsonSerializer; using JsonSerializer = System.Text.Json.JsonSerializer;
using MongoDB.Bson;
using MongoDB.Bson.IO;
using MongoDB.Bson.Serialization;
using System.IO;
namespace AyCode.Core.Benchmarks; namespace AyCode.Core.Benchmarks;
@ -53,14 +57,14 @@ public class SimpleBinaryBenchmark
public void Setup() public void Setup()
{ {
_testData = TestDataFactory.CreatePrimitiveTestData(); _testData = TestDataFactory.CreatePrimitiveTestData();
_binaryData = AcBinarySerializer.Serialize(_testData); _binaryData = AcBinarySerializer.Serialize(_testData, AcBinarySerializerOptions.WithoutReferenceHandling());
_jsonData = AcJsonSerializer.Serialize(_testData, AcJsonSerializerOptions.WithoutReferenceHandling()); _jsonData = AcJsonSerializer.Serialize(_testData, AcJsonSerializerOptions.WithoutReferenceHandling());
Console.WriteLine($"Binary: {_binaryData.Length} bytes, JSON: {_jsonData.Length} chars"); Console.WriteLine($"Binary: {_binaryData.Length} bytes, JSON: {_jsonData.Length} chars");
} }
[Benchmark(Description = "Binary Serialize")] [Benchmark(Description = "Binary Serialize")]
public byte[] SerializeBinary() => AcBinarySerializer.Serialize(_testData); public byte[] SerializeBinary() => AcBinarySerializer.Serialize(_testData, AcBinarySerializerOptions.WithoutReferenceHandling());
[Benchmark(Description = "JSON Serialize", Baseline = true)] [Benchmark(Description = "JSON Serialize", Baseline = true)]
public string SerializeJson() => AcJsonSerializer.Serialize(_testData, AcJsonSerializerOptions.WithoutReferenceHandling()); public string SerializeJson() => AcJsonSerializer.Serialize(_testData, AcJsonSerializerOptions.WithoutReferenceHandling());
@ -74,6 +78,7 @@ public class SimpleBinaryBenchmark
/// <summary> /// <summary>
/// Complex hierarchy benchmark - AcBinary vs JSON only (no MessagePack to isolate the issue). /// Complex hierarchy benchmark - AcBinary vs JSON only (no MessagePack to isolate the issue).
/// Uses AcBinary without reference handling.
/// </summary> /// </summary>
[ShortRunJob] [ShortRunJob]
[MemoryDiagnoser] [MemoryDiagnoser]
@ -98,7 +103,7 @@ public class ComplexBinaryBenchmark
pointsPerMeasurement: 3); pointsPerMeasurement: 3);
Console.WriteLine($"Created order with {_testOrder.Items.Count} items"); Console.WriteLine($"Created order with {_testOrder.Items.Count} items");
_binaryOptions = AcBinarySerializerOptions.Default; _binaryOptions = AcBinarySerializerOptions.WithoutReferenceHandling();
_jsonOptions = AcJsonSerializerOptions.WithoutReferenceHandling(); _jsonOptions = AcJsonSerializerOptions.WithoutReferenceHandling();
Console.WriteLine("Serializing AcBinary..."); Console.WriteLine("Serializing AcBinary...");
@ -129,7 +134,7 @@ public class ComplexBinaryBenchmark
} }
/// <summary> /// <summary>
/// Full comparison with MessagePack - separate class to isolate potential issues. /// Full comparison with MessagePack and BSON - AcBinary uses NO reference handling everywhere.
/// </summary> /// </summary>
[ShortRunJob] [ShortRunJob]
[MemoryDiagnoser] [MemoryDiagnoser]
@ -139,6 +144,7 @@ public class MessagePackComparisonBenchmark
private TestOrder _testOrder = null!; private TestOrder _testOrder = null!;
private byte[] _acBinaryData = null!; private byte[] _acBinaryData = null!;
private byte[] _msgPackData = null!; private byte[] _msgPackData = null!;
private byte[] _bsonData = null!;
private string _jsonData = null!; private string _jsonData = null!;
private AcBinarySerializerOptions _binaryOptions = null!; private AcBinarySerializerOptions _binaryOptions = null!;
@ -155,7 +161,7 @@ public class MessagePackComparisonBenchmark
measurementsPerPallet: 2, measurementsPerPallet: 2,
pointsPerMeasurement: 3); pointsPerMeasurement: 3);
_binaryOptions = AcBinarySerializerOptions.Default; _binaryOptions = AcBinarySerializerOptions.WithoutReferenceHandling();
_msgPackOptions = ContractlessStandardResolver.Options.WithCompression(MessagePackCompression.None); _msgPackOptions = ContractlessStandardResolver.Options.WithCompression(MessagePackCompression.None);
_jsonOptions = AcJsonSerializerOptions.WithoutReferenceHandling(); _jsonOptions = AcJsonSerializerOptions.WithoutReferenceHandling();
@ -175,10 +181,25 @@ public class MessagePackComparisonBenchmark
_msgPackData = Array.Empty<byte>(); _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); var jsonBytes = Encoding.UTF8.GetByteCount(_jsonData);
Console.WriteLine($"\n=== SIZE COMPARISON ==="); Console.WriteLine($"\n=== SIZE COMPARISON ===");
Console.WriteLine($"AcBinary: {_acBinaryData.Length,8:N0} bytes ({100.0 * _acBinaryData.Length / jsonBytes:F1}%)"); 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($"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%)"); Console.WriteLine($"JSON: {jsonBytes,8:N0} bytes (100.0%)");
} }
@ -188,16 +209,28 @@ public class MessagePackComparisonBenchmark
[Benchmark(Description = "MessagePack Serialize", Baseline = true)] [Benchmark(Description = "MessagePack Serialize", Baseline = true)]
public byte[] Serialize_MsgPack() => MessagePackSerializer.Serialize(_testOrder, _msgPackOptions); public byte[] Serialize_MsgPack() => MessagePackSerializer.Serialize(_testOrder, _msgPackOptions);
[Benchmark(Description = "BSON Serialize")]
public byte[] Serialize_Bson() => _testOrder.ToBsonDocument().ToBson();
[Benchmark(Description = "AcBinary Deserialize")] [Benchmark(Description = "AcBinary Deserialize")]
public TestOrder? Deserialize_AcBinary() => AcBinaryDeserializer.Deserialize<TestOrder>(_acBinaryData); public TestOrder? Deserialize_AcBinary() => AcBinaryDeserializer.Deserialize<TestOrder>(_acBinaryData);
[Benchmark(Description = "MessagePack Deserialize")] [Benchmark(Description = "MessagePack Deserialize")]
public TestOrder? Deserialize_MsgPack() => MessagePackSerializer.Deserialize<TestOrder>(_msgPackData, _msgPackOptions); 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> /// <summary>
/// Comprehensive AcBinary vs MessagePack comparison benchmark. /// Comprehensive AcBinary vs MessagePack comparison benchmark.
/// Tests: WithRef, NoRef, Populate, Serialize, Deserialize, Size /// Tests: NoRef (everywhere), Populate, Serialize, Deserialize, Size
/// </summary> /// </summary>
[ShortRunJob] [ShortRunJob]
[MemoryDiagnoser] [MemoryDiagnoser]
@ -214,6 +247,7 @@ public class AcBinaryVsMessagePackFullBenchmark
// Serialized data - MessagePack // Serialized data - MessagePack
private byte[] _msgPackData = null!; private byte[] _msgPackData = null!;
private byte[] _bsonData = null!;
// Options // Options
private AcBinarySerializerOptions _withRefOptions = null!; private AcBinarySerializerOptions _withRefOptions = null!;
@ -238,8 +272,8 @@ public class AcBinaryVsMessagePackFullBenchmark
sharedUser: sharedUser, sharedUser: sharedUser,
sharedMetadata: sharedMeta); sharedMetadata: sharedMeta);
// Setup options // Setup options - enforce no reference handling everywhere
_withRefOptions = AcBinarySerializerOptions.Default; // WithRef by default _withRefOptions = AcBinarySerializerOptions.WithoutReferenceHandling();
_noRefOptions = AcBinarySerializerOptions.WithoutReferenceHandling(); _noRefOptions = AcBinarySerializerOptions.WithoutReferenceHandling();
_msgPackOptions = ContractlessStandardResolver.Options.WithCompression(MessagePackCompression.None); _msgPackOptions = ContractlessStandardResolver.Options.WithCompression(MessagePackCompression.None);
@ -248,6 +282,16 @@ public class AcBinaryVsMessagePackFullBenchmark
_acBinaryNoRef = AcBinarySerializer.Serialize(_testOrder, _noRefOptions); _acBinaryNoRef = AcBinarySerializer.Serialize(_testOrder, _noRefOptions);
_msgPackData = MessagePackSerializer.Serialize(_testOrder, _msgPackOptions); _msgPackData = MessagePackSerializer.Serialize(_testOrder, _msgPackOptions);
// BSON
try
{
_bsonData = _testOrder.ToBsonDocument().ToBson();
}
catch
{
_bsonData = Array.Empty<byte>();
}
// Create populate target // Create populate target
_populateTarget = new TestOrder { Id = _testOrder.Id }; _populateTarget = new TestOrder { Id = _testOrder.Id };
foreach (var item in _testOrder.Items) foreach (var item in _testOrder.Items)
@ -262,59 +306,66 @@ public class AcBinaryVsMessagePackFullBenchmark
private void PrintSizeComparison() private void PrintSizeComparison()
{ {
Console.WriteLine("\n" + new string('=', 60)); 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(new string('=', 60));
Console.WriteLine($" AcBinary WithRef: {_acBinaryWithRef.Length,8:N0} bytes"); Console.WriteLine($" AcBinary WithRef: {_acBinaryWithRef.Length,8:N0} bytes");
Console.WriteLine($" AcBinary NoRef: {_acBinaryNoRef.Length,8:N0} bytes"); Console.WriteLine($" AcBinary NoRef: {_acBinaryNoRef.Length,8:N0} bytes");
Console.WriteLine($" MessagePack: {_msgPackData.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(new string('-', 60));
Console.WriteLine($" AcBinary/MsgPack: {100.0 * _acBinaryWithRef.Length / _msgPackData.Length:F1}% (WithRef)"); 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 / _msgPackData.Length:F1}% (NoRef)"); Console.WriteLine($" AcBinary/MsgPack: {100.0 * _acBinaryNoRef.Length / Math.Max(1, _msgPackData.Length):F1}% (NoRef)");
Console.WriteLine(new string('=', 60) + "\n"); Console.WriteLine(new string('=', 60) + "\n");
} }
#region Serialize Benchmarks #region Serialize Benchmarks
[Benchmark(Description = "AcBinary Serialize WithRef")]
public byte[] Serialize_AcBinary_WithRef() => AcBinarySerializer.Serialize(_testOrder, _withRefOptions);
[Benchmark(Description = "AcBinary Serialize NoRef")] [Benchmark(Description = "AcBinary Serialize NoRef")]
public byte[] Serialize_AcBinary_NoRef() => AcBinarySerializer.Serialize(_testOrder, _noRefOptions); public byte[] Serialize_AcBinary_NoRef() => AcBinarySerializer.Serialize(_testOrder, _noRefOptions);
[Benchmark(Description = "MessagePack Serialize", Baseline = true)] [Benchmark(Description = "MessagePack Serialize", Baseline = true)]
public byte[] Serialize_MsgPack() => MessagePackSerializer.Serialize(_testOrder, _msgPackOptions); public byte[] Serialize_MsgPack() => MessagePackSerializer.Serialize(_testOrder, _msgPackOptions);
[Benchmark(Description = "BSON Serialize")]
public byte[] Serialize_Bson() => _testOrder.ToBsonDocument().ToBson();
#endregion #endregion
#region Deserialize Benchmarks #region Deserialize Benchmarks
[Benchmark(Description = "AcBinary Deserialize WithRef")]
public TestOrder? Deserialize_AcBinary_WithRef() => AcBinaryDeserializer.Deserialize<TestOrder>(_acBinaryWithRef);
[Benchmark(Description = "AcBinary Deserialize NoRef")] [Benchmark(Description = "AcBinary Deserialize NoRef")]
public TestOrder? Deserialize_AcBinary_NoRef() => AcBinaryDeserializer.Deserialize<TestOrder>(_acBinaryNoRef); public TestOrder? Deserialize_AcBinary_NoRef() => AcBinaryDeserializer.Deserialize<TestOrder>(_acBinaryNoRef);
[Benchmark(Description = "MessagePack Deserialize")] [Benchmark(Description = "MessagePack Deserialize")]
public TestOrder? Deserialize_MsgPack() => MessagePackSerializer.Deserialize<TestOrder>(_msgPackData, _msgPackOptions); 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 #endregion
#region Populate Benchmarks #region Populate Benchmarks
[Benchmark(Description = "AcBinary Populate WithRef")] [Benchmark(Description = "AcBinary Populate NoRef")]
public void Populate_AcBinary_WithRef() public void Populate_AcBinary_NoRef()
{ {
// Create fresh target each time to avoid state accumulation // Create fresh target each time to avoid state accumulation
var target = CreatePopulateTarget(); var target = CreatePopulateTarget();
AcBinaryDeserializer.Populate(_acBinaryWithRef, target); AcBinaryDeserializer.Populate(_acBinaryNoRef, target);
} }
[Benchmark(Description = "AcBinary PopulateMerge WithRef")] [Benchmark(Description = "AcBinary PopulateMerge NoRef")]
public void PopulateMerge_AcBinary_WithRef() public void PopulateMerge_AcBinary_NoRef()
{ {
// Create fresh target each time to avoid state accumulation // Create fresh target each time to avoid state accumulation
var target = CreatePopulateTarget(); var target = CreatePopulateTarget();
AcBinaryDeserializer.PopulateMerge(_acBinaryWithRef.AsSpan(), target); AcBinaryDeserializer.PopulateMerge(_acBinaryNoRef.AsSpan(), target);
} }
private TestOrder CreatePopulateTarget() private TestOrder CreatePopulateTarget()
@ -332,6 +383,7 @@ public class AcBinaryVsMessagePackFullBenchmark
/// <summary> /// <summary>
/// Detailed size comparison - not a performance benchmark, just size output. /// Detailed size comparison - not a performance benchmark, just size output.
/// Now includes BSON size output and uses AcBinary without reference handling.
/// </summary> /// </summary>
[ShortRunJob] [ShortRunJob]
[MemoryDiagnoser] [MemoryDiagnoser]
@ -349,7 +401,7 @@ public class SizeComparisonBenchmark
public void Setup() public void Setup()
{ {
_msgPackOptions = ContractlessStandardResolver.Options.WithCompression(MessagePackCompression.None); _msgPackOptions = ContractlessStandardResolver.Options.WithCompression(MessagePackCompression.None);
_withRefOptions = AcBinarySerializerOptions.Default; _withRefOptions = AcBinarySerializerOptions.WithoutReferenceHandling();
_noRefOptions = AcBinarySerializerOptions.WithoutReferenceHandling(); _noRefOptions = AcBinarySerializerOptions.WithoutReferenceHandling();
// Small order // Small order
@ -379,7 +431,7 @@ public class SizeComparisonBenchmark
private void PrintDetailedSizeComparison() private void PrintDetailedSizeComparison()
{ {
Console.WriteLine("\n" + new string('=', 80)); 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)); Console.WriteLine(new string('=', 80));
PrintOrderSize("Small Order (1x1x1x2)", _smallOrder); PrintOrderSize("Small Order (1x1x1x2)", _smallOrder);
@ -394,11 +446,14 @@ public class SizeComparisonBenchmark
var acWithRef = AcBinarySerializer.Serialize(order, _withRefOptions); var acWithRef = AcBinarySerializer.Serialize(order, _withRefOptions);
var acNoRef = AcBinarySerializer.Serialize(order, _noRefOptions); var acNoRef = AcBinarySerializer.Serialize(order, _noRefOptions);
var msgPack = MessagePackSerializer.Serialize(order, _msgPackOptions); var msgPack = MessagePackSerializer.Serialize(order, _msgPackOptions);
byte[] bson;
try { bson = order.ToBsonDocument().ToBson(); } catch { bson = Array.Empty<byte>(); }
Console.WriteLine($"\n {name}:"); 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 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 / 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($" 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 withRefSaving = msgPack.Length - acWithRef.Length;
var noRefSaving = msgPack.Length - acNoRef.Length; var noRefSaving = msgPack.Length - acNoRef.Length;

View File

@ -42,48 +42,49 @@ public class AcBinaryDeserializationException : Exception
public static class AcBinaryDeserializer public static class AcBinaryDeserializer
{ {
private static readonly ConcurrentDictionary<Type, BinaryDeserializeTypeMetadata> TypeMetadataCache = new(); private static readonly ConcurrentDictionary<Type, BinaryDeserializeTypeMetadata> TypeMetadataCache = new();
private static readonly ConcurrentDictionary<Type, TypeConversionInfo> TypeConversionCache = new();
// Type dispatch table for fast ReadValue // Type dispatch table for fast ReadValue
private delegate object? TypeReader(ref BinaryDeserializationContext context, Type targetType, int depth); 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() static AcBinaryDeserializer()
{ {
// Initialize type reader dispatch table RegisterReader(BinaryTypeCode.Null, static (ref BinaryDeserializationContext _, Type _, int _) => null);
var readers = new Dictionary<byte, TypeReader> RegisterReader(BinaryTypeCode.True, static (ref BinaryDeserializationContext _, Type _, int _) => true);
{ RegisterReader(BinaryTypeCode.False, static (ref BinaryDeserializationContext _, Type _, int _) => false);
[BinaryTypeCode.Null] = static (ref BinaryDeserializationContext _, Type _, int _) => null, RegisterReader(BinaryTypeCode.Int8, static (ref BinaryDeserializationContext ctx, Type _, int _) => (sbyte)ctx.ReadByte());
[BinaryTypeCode.True] = static (ref BinaryDeserializationContext _, Type _, int _) => true, RegisterReader(BinaryTypeCode.UInt8, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadByte());
[BinaryTypeCode.False] = static (ref BinaryDeserializationContext _, Type _, int _) => false, RegisterReader(BinaryTypeCode.Int16, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadInt16Unsafe());
[BinaryTypeCode.Int8] = static (ref BinaryDeserializationContext ctx, Type _, int _) => (sbyte)ctx.ReadByte(), RegisterReader(BinaryTypeCode.UInt16, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadUInt16Unsafe());
[BinaryTypeCode.UInt8] = static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadByte(), RegisterReader(BinaryTypeCode.Int32, static (ref BinaryDeserializationContext ctx, Type type, int _) => ReadInt32Value(ref ctx, type));
[BinaryTypeCode.Int16] = static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadInt16Unsafe(), RegisterReader(BinaryTypeCode.UInt32, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadVarUInt());
[BinaryTypeCode.UInt16] = static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadUInt16Unsafe(), RegisterReader(BinaryTypeCode.Int64, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadVarLong());
[BinaryTypeCode.Int32] = static (ref BinaryDeserializationContext ctx, Type type, int _) => ReadInt32Value(ref ctx, type), RegisterReader(BinaryTypeCode.UInt64, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadVarULong());
[BinaryTypeCode.UInt32] = static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadVarUInt(), RegisterReader(BinaryTypeCode.Float32, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadSingleUnsafe());
[BinaryTypeCode.Int64] = static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadVarLong(), RegisterReader(BinaryTypeCode.Float64, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadDoubleUnsafe());
[BinaryTypeCode.UInt64] = static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadVarULong(), RegisterReader(BinaryTypeCode.Decimal, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadDecimalUnsafe());
[BinaryTypeCode.Float32] = static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadSingleUnsafe(), RegisterReader(BinaryTypeCode.Char, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadCharUnsafe());
[BinaryTypeCode.Float64] = static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadDoubleUnsafe(), RegisterReader(BinaryTypeCode.String, static (ref BinaryDeserializationContext ctx, Type _, int _) => ReadAndInternString(ref ctx));
[BinaryTypeCode.Decimal] = static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadDecimalUnsafe(), RegisterReader(BinaryTypeCode.StringInterned, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.GetInternedString((int)ctx.ReadVarUInt()));
[BinaryTypeCode.Char] = static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadCharUnsafe(), RegisterReader(BinaryTypeCode.StringEmpty, static (ref BinaryDeserializationContext _, Type _, int _) => string.Empty);
[BinaryTypeCode.String] = static (ref BinaryDeserializationContext ctx, Type _, int _) => ReadAndInternString(ref ctx), RegisterReader(BinaryTypeCode.DateTime, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadDateTimeUnsafe());
[BinaryTypeCode.StringInterned] = static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.GetInternedString((int)ctx.ReadVarUInt()), RegisterReader(BinaryTypeCode.DateTimeOffset, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadDateTimeOffsetUnsafe());
[BinaryTypeCode.StringEmpty] = static (ref BinaryDeserializationContext _, Type _, int _) => string.Empty, RegisterReader(BinaryTypeCode.TimeSpan, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadTimeSpanUnsafe());
[BinaryTypeCode.DateTime] = static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadDateTimeUnsafe(), RegisterReader(BinaryTypeCode.Guid, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadGuidUnsafe());
[BinaryTypeCode.DateTimeOffset] = static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadDateTimeOffsetUnsafe(), RegisterReader(BinaryTypeCode.Enum, static (ref BinaryDeserializationContext ctx, Type type, int _) => ReadEnumValue(ref ctx, type));
[BinaryTypeCode.TimeSpan] = static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadTimeSpanUnsafe(), RegisterReader(BinaryTypeCode.Object, ReadObject);
[BinaryTypeCode.Guid] = static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadGuidUnsafe(), RegisterReader(BinaryTypeCode.ObjectRef, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.GetReferencedObject(ctx.ReadVarInt()));
[BinaryTypeCode.Enum] = static (ref BinaryDeserializationContext ctx, Type type, int _) => ReadEnumValue(ref ctx, type), RegisterReader(BinaryTypeCode.Array, ReadArray);
[BinaryTypeCode.Object] = ReadObject, RegisterReader(BinaryTypeCode.Dictionary, ReadDictionary);
[BinaryTypeCode.ObjectRef] = static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.GetReferencedObject(ctx.ReadVarInt()), RegisterReader(BinaryTypeCode.ByteArray, static (ref BinaryDeserializationContext ctx, Type _, int _) => ReadByteArray(ref ctx));
[BinaryTypeCode.Array] = ReadArray,
[BinaryTypeCode.Dictionary] = ReadDictionary,
[BinaryTypeCode.ByteArray] = static (ref BinaryDeserializationContext ctx, Type _, int _) => ReadByteArray(ref ctx),
};
TypeReaders = readers.ToFrozenDictionary();
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void RegisterReader(byte typeCode, TypeReader reader) => TypeReaders[typeCode] = reader;
#region Public API #region Public API
/// <summary> /// <summary>
@ -269,8 +270,8 @@ public static class AcBinaryDeserializer
return ConvertToTargetType(intValue, targetType); return ConvertToTargetType(intValue, targetType);
} }
// Use dispatch table for type-specific reading var reader = TypeReaders[typeCode];
if (TypeReaders.TryGetValue(typeCode, out var reader)) if (reader != null)
{ {
return reader(ref context, targetType, depth); return reader(ref context, targetType, depth);
} }
@ -294,6 +295,7 @@ public static class AcBinaryDeserializer
{ {
context.RegisterInternedString(str); context.RegisterInternedString(str);
} }
return str; return str;
} }
@ -307,12 +309,11 @@ public static class AcBinaryDeserializer
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static object ConvertToTargetType(int value, Type targetType) private static object ConvertToTargetType(int value, Type targetType)
{ {
var underlying = Nullable.GetUnderlyingType(targetType) ?? targetType; var info = GetConversionInfo(targetType);
if (underlying.IsEnum) if (info.IsEnum)
return Enum.ToObject(underlying, value); return Enum.ToObject(info.UnderlyingType, value);
var typeCode = Type.GetTypeCode(underlying); return info.TypeCode switch
return typeCode switch
{ {
TypeCode.Int32 => value, TypeCode.Int32 => value,
TypeCode.Int64 => (long)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)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static object ReadEnumValue(ref BinaryDeserializationContext context, Type targetType) private static object ReadEnumValue(ref BinaryDeserializationContext context, Type targetType)
{ {
var underlying = Nullable.GetUnderlyingType(targetType) ?? targetType; var info = GetConversionInfo(targetType);
var nextByte = context.ReadByte(); var nextByte = context.ReadByte();
int intValue; int intValue;
if (BinaryTypeCode.IsTinyInt(nextByte)) if (BinaryTypeCode.IsTinyInt(nextByte))
{ {
@ -351,9 +360,7 @@ public static class AcBinaryDeserializer
context.Position, targetType); context.Position, targetType);
} }
if (underlying.IsEnum) return info.IsEnum ? Enum.ToObject(info.UnderlyingType, intValue) : intValue;
return Enum.ToObject(underlying, intValue);
return intValue;
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
@ -400,7 +407,7 @@ public static class AcBinaryDeserializer
private static void PopulateObject(ref BinaryDeserializationContext context, object target, Type targetType, int depth) private static void PopulateObject(ref BinaryDeserializationContext context, object target, Type targetType, int depth)
{ {
var metadata = GetTypeMetadata(targetType); var metadata = GetTypeMetadata(targetType);
// Skip ref ID if present // Skip ref ID if present
if (context.HasReferenceHandling) if (context.HasReferenceHandling)
{ {
@ -459,7 +466,7 @@ public static class AcBinaryDeserializer
// OPTIMIZATION: Reuse existing nested objects instead of creating new ones // OPTIMIZATION: Reuse existing nested objects instead of creating new ones
var peekCode = context.PeekByte(); var peekCode = context.PeekByte();
// Handle nested complex objects - reuse existing if available // Handle nested complex objects - reuse existing if available
if (peekCode == BinaryTypeCode.Object && propInfo.IsComplexType) if (peekCode == BinaryTypeCode.Object && propInfo.IsComplexType)
{ {
@ -471,7 +478,7 @@ public static class AcBinaryDeserializer
continue; continue;
} }
} }
// Handle collections - reuse existing collection and populate items // Handle collections - reuse existing collection and populate items
if (peekCode == BinaryTypeCode.Array && propInfo.IsCollection) if (peekCode == BinaryTypeCode.Array && propInfo.IsCollection)
{ {
@ -734,7 +741,7 @@ public static class AcBinaryDeserializer
if (existingItem != null) if (existingItem != null)
{ {
context.ReadByte(); // consume Object marker context.ReadByte(); // consume Object marker
// Handle ref ID if present // Handle ref ID if present
if (context.HasReferenceHandling) if (context.HasReferenceHandling)
{ {
@ -744,7 +751,7 @@ public static class AcBinaryDeserializer
context.RegisterObject(refId, existingItem); context.RegisterObject(refId, existingItem);
} }
} }
PopulateObject(ref context, existingItem, elementMetadata, nextDepth); PopulateObject(ref context, existingItem, elementMetadata, nextDepth);
continue; continue;
} }
@ -752,7 +759,7 @@ public static class AcBinaryDeserializer
// Read new value // Read new value
var value = ReadValue(ref context, elementType, nextDepth); var value = ReadValue(ref context, elementType, nextDepth);
if (i < existingCount) if (i < existingCount)
{ {
// Replace existing item // Replace existing item
@ -776,6 +783,7 @@ public static class AcBinaryDeserializer
acObservable?.EndUpdate(); acObservable?.EndUpdate();
} }
} }
#endregion #endregion
#region Array Reading #region Array Reading
@ -803,6 +811,7 @@ public static class AcBinaryDeserializer
var value = ReadValue(ref context, elementType, nextDepth); var value = ReadValue(ref context, elementType, nextDepth);
array.SetValue(value, i); array.SetValue(value, i);
} }
return array; return array;
} }
@ -812,7 +821,10 @@ public static class AcBinaryDeserializer
var instance = Activator.CreateInstance(targetType); var instance = Activator.CreateInstance(targetType);
if (instance is IList l) list = l; if (instance is IList l) list = l;
} }
catch { /* Fallback to List<T> */ } catch
{
/* Fallback to List<T> */
}
list ??= GetOrCreateListFactory(elementType)(); list ??= GetOrCreateListFactory(elementType)();
@ -855,6 +867,7 @@ public static class AcBinaryDeserializer
else else
return null; // Fall back to generic path return null; // Fall back to generic path
} }
return array; return array;
} }
@ -868,6 +881,7 @@ public static class AcBinaryDeserializer
if (typeCode != BinaryTypeCode.Float64) return null; if (typeCode != BinaryTypeCode.Float64) return null;
array[i] = context.ReadDoubleUnsafe(); array[i] = context.ReadDoubleUnsafe();
} }
return array; return array;
} }
@ -887,6 +901,7 @@ public static class AcBinaryDeserializer
else else
return null; return null;
} }
return array; return array;
} }
@ -901,6 +916,7 @@ public static class AcBinaryDeserializer
else if (typeCode == BinaryTypeCode.False) array[i] = false; else if (typeCode == BinaryTypeCode.False) array[i] = false;
else return null; else return null;
} }
return array; return array;
} }
@ -914,6 +930,7 @@ public static class AcBinaryDeserializer
if (typeCode != BinaryTypeCode.Guid) return null; if (typeCode != BinaryTypeCode.Guid) return null;
array[i] = context.ReadGuidUnsafe(); array[i] = context.ReadGuidUnsafe();
} }
return array; return array;
} }
@ -927,6 +944,7 @@ public static class AcBinaryDeserializer
if (typeCode != BinaryTypeCode.Decimal) return null; if (typeCode != BinaryTypeCode.Decimal) return null;
array[i] = context.ReadDecimalUnsafe(); array[i] = context.ReadDecimalUnsafe();
} }
return array; return array;
} }
@ -940,6 +958,7 @@ public static class AcBinaryDeserializer
if (typeCode != BinaryTypeCode.DateTime) return null; if (typeCode != BinaryTypeCode.DateTime) return null;
array[i] = context.ReadDateTimeUnsafe(); array[i] = context.ReadDateTimeUnsafe();
} }
return array; return array;
} }
@ -995,7 +1014,7 @@ public static class AcBinaryDeserializer
var idGetter = CreateCompiledGetter(elementType, idProp); var idGetter = CreateCompiledGetter(elementType, idProp);
var propInfo = new BinaryPropertySetterInfo( var propInfo = new BinaryPropertySetterInfo(
"Items", elementType, true, elementType, idType, idGetter); "Items", elementType, true, elementType, idType, idGetter);
MergeIIdCollection(ref context, targetList, propInfo, depth); MergeIIdCollection(ref context, targetList, propInfo, depth);
} }
@ -1128,7 +1147,7 @@ public static class AcBinaryDeserializer
{ {
var byteLen = (int)context.ReadVarUInt(); var byteLen = (int)context.ReadVarUInt();
if (byteLen == 0) return; if (byteLen == 0) return;
var str = context.ReadStringUtf8(byteLen); var str = context.ReadStringUtf8(byteLen);
if (str.Length >= context.MinStringInternLength) if (str.Length >= context.MinStringInternLength)
{ {
@ -1167,6 +1186,7 @@ public static class AcBinaryDeserializer
} }
// StringEmpty doesn't need any action // StringEmpty doesn't need any action
} }
// Skip value // Skip value
SkipValue(ref context); SkipValue(ref context);
} }
@ -1228,7 +1248,7 @@ public static class AcBinaryDeserializer
internal sealed class BinaryDeserializeTypeMetadata internal sealed class BinaryDeserializeTypeMetadata
{ {
private readonly Dictionary<string, BinaryPropertySetterInfo> _propertiesDict; private readonly FrozenDictionary<string, BinaryPropertySetterInfo> _propertiesDict;
public BinaryPropertySetterInfo[] PropertiesArray { get; } public BinaryPropertySetterInfo[] PropertiesArray { get; }
public Func<object>? CompiledConstructor { get; } public Func<object>? CompiledConstructor { get; }
@ -1252,16 +1272,20 @@ public static class AcBinaryDeserializer
propsList.Add(p); propsList.Add(p);
} }
_propertiesDict = new Dictionary<string, BinaryPropertySetterInfo>(propsList.Count, StringComparer.OrdinalIgnoreCase); var propInfos = new BinaryPropertySetterInfo[propsList.Count];
PropertiesArray = new BinaryPropertySetterInfo[propsList.Count];
for (int i = 0; i < propsList.Count; i++) for (int i = 0; i < propsList.Count; i++)
{ {
var prop = propsList[i]; propInfos[i] = new BinaryPropertySetterInfo(propsList[i], type);
var propInfo = new BinaryPropertySetterInfo(prop, type);
_propertiesDict[prop.Name] = propInfo;
PropertiesArray[i] = propInfo;
} }
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)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
@ -1295,20 +1319,20 @@ public static class AcBinaryDeserializer
ElementType = GetCollectionElementType(PropertyType); ElementType = GetCollectionElementType(PropertyType);
IsCollection = ElementType != null && ElementType != typeof(object) && IsCollection = ElementType != null && ElementType != typeof(object) &&
typeof(IEnumerable).IsAssignableFrom(PropertyType) && typeof(IEnumerable).IsAssignableFrom(PropertyType) &&
!ReferenceEquals(PropertyType, StringType); !ReferenceEquals(PropertyType, StringType);
// Determine if this is a complex type that can be populated // Determine if this is a complex type that can be populated
IsComplexType = !PropertyType.IsPrimitive && IsComplexType = !PropertyType.IsPrimitive &&
!ReferenceEquals(PropertyType, StringType) && !ReferenceEquals(PropertyType, StringType) &&
!PropertyType.IsEnum && !PropertyType.IsEnum &&
!ReferenceEquals(PropertyType, GuidType) && !ReferenceEquals(PropertyType, GuidType) &&
!ReferenceEquals(PropertyType, DateTimeType) && !ReferenceEquals(PropertyType, DateTimeType) &&
!ReferenceEquals(PropertyType, DecimalType) && !ReferenceEquals(PropertyType, DecimalType) &&
!ReferenceEquals(PropertyType, TimeSpanType) && !ReferenceEquals(PropertyType, TimeSpanType) &&
!ReferenceEquals(PropertyType, DateTimeOffsetType) && !ReferenceEquals(PropertyType, DateTimeOffsetType) &&
Nullable.GetUnderlyingType(PropertyType) == null && Nullable.GetUnderlyingType(PropertyType) == null &&
!IsCollection; !IsCollection;
if (IsCollection && ElementType != null) if (IsCollection && ElementType != null)
{ {
@ -1384,7 +1408,7 @@ public static class AcBinaryDeserializer
public byte FormatVersion { get; private set; } public byte FormatVersion { get; private set; }
public bool HasMetadata { get; private set; } public bool HasMetadata { get; private set; }
public bool HasReferenceHandling { get; private set; } public bool HasReferenceHandling { get; private set; }
/// <summary> /// <summary>
/// Minimum string length for interning. Must match serializer's MinStringInternLength. /// Minimum string length for interning. Must match serializer's MinStringInternLength.
/// Default: 4 (from AcBinarySerializerOptions) /// Default: 4 (from AcBinarySerializerOptions)
@ -1603,7 +1627,7 @@ public static class AcBinaryDeserializer
{ {
if (_position + 16 > _data.Length) if (_position + 16 > _data.Length)
throw new AcBinaryDeserializationException("Unexpected end of data", _position); throw new AcBinaryDeserializationException("Unexpected end of data", _position);
Span<int> bits = stackalloc int[4]; Span<int> bits = stackalloc int[4];
MemoryMarshal.Cast<byte, int>(_data.Slice(_position, 16)).CopyTo(bits); MemoryMarshal.Cast<byte, int>(_data.Slice(_position, 16)).CopyTo(bits);
_position += 16; _position += 16;
@ -1690,14 +1714,7 @@ public static class AcBinaryDeserializer
throw new AcBinaryDeserializationException("Unexpected end of data", _position); throw new AcBinaryDeserializationException("Unexpected end of data", _position);
var src = _data.Slice(_position, byteCount); var src = _data.Slice(_position, byteCount);
var result = Utf8NoBom.GetString(src);
// 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);
});
_position += byteCount; _position += byteCount;
return result; return result;
} }
@ -1742,4 +1759,18 @@ public static class AcBinaryDeserializer
} }
#endregion #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;
}
}
} }

View File

@ -1,6 +1,7 @@
using System.Buffers; using System.Buffers;
using System.Collections; using System.Collections;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Linq.Expressions; using System.Linq.Expressions;
using System.Reflection; using System.Reflection;
@ -47,34 +48,10 @@ public static class AcBinarySerializer
return [BinaryTypeCode.Null]; return [BinaryTypeCode.Null];
} }
var type = value.GetType(); var runtimeType = value.GetType();
var context = SerializeCore(value, runtimeType, options);
// Use context-based serialization for all types to ensure consistent format
var context = BinarySerializationContextPool.Get(options);
try try
{ {
// Reserve space for header
context.WriteHeaderPlaceholder();
// Phase 1: If reference handling enabled, scan for multi-referenced objects
if (options.UseReferenceHandling && !IsPrimitiveOrStringFast(type))
{
ScanReferences(value, context, 0);
}
// Phase 2: If metadata enabled, collect property names
if (options.UseMetadata && !IsPrimitiveOrStringFast(type))
{
CollectPropertyNames(value, context, 0);
}
// Write metadata section
context.WriteMetadata();
// Phase 3: Write the actual data
WriteValue(value, type, context, 0);
// Finalize and return
return context.ToArray(); return context.ToArray();
} }
finally finally
@ -97,26 +74,10 @@ public static class AcBinarySerializer
return; return;
} }
var type = value.GetType(); var runtimeType = value.GetType();
var context = BinarySerializationContextPool.Get(options); var context = SerializeCore(value, runtimeType, options);
try try
{ {
context.WriteHeaderPlaceholder();
if (options.UseReferenceHandling && !IsPrimitiveOrStringFast(type))
{
ScanReferences(value, context, 0);
}
if (options.UseMetadata && !IsPrimitiveOrStringFast(type))
{
CollectPropertyNames(value, context, 0);
}
context.WriteMetadata();
WriteValue(value, type, context, 0);
// Write directly to the IBufferWriter instead of creating a new array
context.WriteTo(writer); context.WriteTo(writer);
} }
finally finally
@ -133,25 +94,10 @@ public static class AcBinarySerializer
{ {
if (value == null) return 1; if (value == null) return 1;
var type = value.GetType(); var runtimeType = value.GetType();
var context = BinarySerializationContextPool.Get(options); var context = SerializeCore(value, runtimeType, options);
try try
{ {
context.WriteHeaderPlaceholder();
if (options.UseReferenceHandling && !IsPrimitiveOrStringFast(type))
{
ScanReferences(value, context, 0);
}
if (options.UseMetadata && !IsPrimitiveOrStringFast(type))
{
CollectPropertyNames(value, context, 0);
}
context.WriteMetadata();
WriteValue(value, type, context, 0);
return context.Position; return context.Position;
} }
finally finally
@ -160,6 +106,49 @@ public static class AcBinarySerializer
} }
} }
/// <summary>
/// Serialize object and keep the pooled buffer for zero-copy consumers.
/// Caller must dispose the returned result to release the buffer.
/// </summary>
public static BinarySerializationResult SerializeToPooledBuffer<T>(T value, AcBinarySerializerOptions options)
{
if (value == null)
{
return BinarySerializationResult.FromImmutable([BinaryTypeCode.Null]);
}
var runtimeType = value.GetType();
var context = SerializeCore(value, runtimeType, options);
try
{
return context.DetachResult();
}
finally
{
BinarySerializationContextPool.Return(context);
}
}
private static BinarySerializationContext SerializeCore(object value, Type runtimeType, AcBinarySerializerOptions options)
{
var context = BinarySerializationContextPool.Get(options);
context.WriteHeaderPlaceholder();
if (options.UseReferenceHandling && !IsPrimitiveOrStringFast(runtimeType))
{
ScanReferences(value, context, 0);
}
if (options.UseMetadata && !IsPrimitiveOrStringFast(runtimeType))
{
RegisterMetadataForType(runtimeType, context);
}
context.WriteMetadata();
WriteValue(value, runtimeType, context, 0);
return context;
}
#endregion #endregion
#region Reference Scanning #region Reference Scanning
@ -205,33 +194,28 @@ public static class AcBinarySerializer
#endregion #endregion
#region Property Name Collection #region Property Metadata Registration
private static void CollectPropertyNames(object? value, BinarySerializationContext context, int depth) private static void RegisterMetadataForType(Type type, BinarySerializationContext context, HashSet<Type>? visited = null)
{ {
if (value == null || depth > context.MaxDepth) return;
var type = value.GetType();
if (IsPrimitiveOrStringFast(type)) return; if (IsPrimitiveOrStringFast(type)) return;
if (value is byte[]) return; visited ??= new HashSet<Type>();
if (!visited.Add(type)) return;
if (value is IDictionary dictionary) if (IsDictionaryType(type, out var keyType, out var valueType))
{ {
foreach (DictionaryEntry entry in dictionary) if (keyType != null) RegisterMetadataForType(keyType, context, visited);
{ if (valueType != null) RegisterMetadataForType(valueType, context, visited);
if (entry.Value != null)
CollectPropertyNames(entry.Value, context, depth + 1);
}
return; return;
} }
if (value is IEnumerable enumerable && !ReferenceEquals(type, StringType)) if (typeof(IEnumerable).IsAssignableFrom(type) && !ReferenceEquals(type, StringType))
{ {
foreach (var item in enumerable) var elementType = GetCollectionElementType(type);
if (elementType != null)
{ {
if (item != null) RegisterMetadataForType(elementType, context, visited);
CollectPropertyNames(item, context, depth + 1);
} }
return; return;
} }
@ -240,12 +224,46 @@ public static class AcBinarySerializer
foreach (var prop in metadata.Properties) foreach (var prop in metadata.Properties)
{ {
context.RegisterPropertyName(prop.Name); context.RegisterPropertyName(prop.Name);
var propValue = prop.GetValue(value);
if (propValue != null) if (TryResolveNestedMetadataType(prop.PropertyType, out var nestedType))
CollectPropertyNames(propValue, context, depth + 1); {
RegisterMetadataForType(nestedType, context, visited);
}
} }
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool TryResolveNestedMetadataType(Type propertyType, out Type nestedType)
{
nestedType = Nullable.GetUnderlyingType(propertyType) ?? propertyType;
if (IsPrimitiveOrStringFast(nestedType))
return false;
if (IsDictionaryType(nestedType, out var _, out var valueType) && valueType != null)
{
if (!IsPrimitiveOrStringFast(valueType))
{
nestedType = valueType;
return true;
}
return false;
}
if (typeof(IEnumerable).IsAssignableFrom(nestedType) && !ReferenceEquals(nestedType, StringType))
{
var elementType = GetCollectionElementType(nestedType);
if (elementType != null && !IsPrimitiveOrStringFast(elementType))
{
nestedType = elementType;
return true;
}
return false;
}
return true;
}
#endregion #endregion
#region Value Writing #region Value Writing
@ -606,27 +624,35 @@ public static class AcBinarySerializer
var properties = metadata.Properties; var properties = metadata.Properties;
var propCount = properties.Length; var propCount = properties.Length;
// Single-pass: count and collect non-null, non-default properties const int StackThreshold = 64;
// Use stackalloc for small property counts to avoid allocation byte[]? rentedStates = null;
Span<int> validIndices = propCount <= 32 ? stackalloc int[propCount] : new int[propCount]; Span<byte> propertyStates = propCount <= StackThreshold
? stackalloc byte[propCount]
: (rentedStates = context.RentPropertyStateBuffer(propCount)).AsSpan(0, propCount);
propertyStates.Clear();
var writtenCount = 0; var writtenCount = 0;
for (var i = 0; i < propCount; i++) for (var i = 0; i < propCount; i++)
{ {
var prop = properties[i]; if (IsPropertyDefaultOrNull(value, properties[i]))
if (IsPropertyDefaultOrNull(value, prop)) {
propertyStates[i] = 0;
continue; continue;
validIndices[writtenCount++] = i; }
propertyStates[i] = 1;
writtenCount++;
} }
context.WriteVarUInt((uint)writtenCount); context.WriteVarUInt((uint)writtenCount);
// Write only the valid properties for (var i = 0; i < propCount; i++)
for (var j = 0; j < writtenCount; j++)
{ {
var prop = properties[validIndices[j]]; if (propertyStates[i] == 0)
continue;
var prop = properties[i];
// Write property index or name
if (context.UseMetadata) if (context.UseMetadata)
{ {
var propIndex = context.GetPropertyNameIndex(prop.Name); var propIndex = context.GetPropertyNameIndex(prop.Name);
@ -637,9 +663,13 @@ public static class AcBinarySerializer
WriteString(prop.Name, context); WriteString(prop.Name, context);
} }
// Use typed writers to avoid boxing
WritePropertyValue(value, prop, context, nextDepth); WritePropertyValue(value, prop, context, nextDepth);
} }
if (rentedStates != null)
{
context.ReturnPropertyStateBuffer(rentedStates);
}
} }
/// <summary> /// <summary>
@ -1089,9 +1119,12 @@ public static class AcBinarySerializer
{ {
private byte[] _buffer; private byte[] _buffer;
private int _position; private int _position;
private int _initialBufferSize;
// Minimum buffer size for ArrayPool (reduces fragmentation) // Minimum buffer size for ArrayPool (reduces fragmentation)
private const int MinBufferSize = 256; private const int MinBufferSize = 256;
private const int PropertyIndexBufferMaxCache = 512;
private const int PropertyStateBufferMaxCache = 512;
// Reference handling // Reference handling
private Dictionary<object, int>? _scanOccurrences; private Dictionary<object, int>? _scanOccurrences;
@ -1106,6 +1139,8 @@ public static class AcBinarySerializer
// Property name table // Property name table
private Dictionary<string, int>? _propertyNames; private Dictionary<string, int>? _propertyNames;
private List<string>? _propertyNameList; private List<string>? _propertyNameList;
private int[]? _propertyIndexBuffer;
private byte[]? _propertyStateBuffer;
public bool UseReferenceHandling { get; private set; } public bool UseReferenceHandling { get; private set; }
public bool UseStringInterning { get; private set; } public bool UseStringInterning { get; private set; }
@ -1115,8 +1150,8 @@ public static class AcBinarySerializer
public BinarySerializationContext(AcBinarySerializerOptions options) public BinarySerializationContext(AcBinarySerializerOptions options)
{ {
var size = Math.Max(options.InitialBufferCapacity, MinBufferSize); _initialBufferSize = Math.Max(options.InitialBufferCapacity, MinBufferSize);
_buffer = ArrayPool<byte>.Shared.Rent(size); _buffer = ArrayPool<byte>.Shared.Rent(_initialBufferSize);
Reset(options); Reset(options);
} }
@ -1129,6 +1164,13 @@ public static class AcBinarySerializer
UseMetadata = options.UseMetadata; UseMetadata = options.UseMetadata;
MaxDepth = options.MaxDepth; MaxDepth = options.MaxDepth;
MinStringInternLength = options.MinStringInternLength; MinStringInternLength = options.MinStringInternLength;
_initialBufferSize = Math.Max(options.InitialBufferCapacity, MinBufferSize);
if (_buffer.Length < _initialBufferSize)
{
ArrayPool<byte>.Shared.Return(_buffer);
_buffer = ArrayPool<byte>.Shared.Rent(_initialBufferSize);
}
} }
public void Clear() public void Clear()
@ -1146,29 +1188,17 @@ public static class AcBinarySerializer
_internedStringList?.Clear(); _internedStringList?.Clear();
_propertyNameList?.Clear(); _propertyNameList?.Clear();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)] if (_propertyIndexBuffer != null && _propertyIndexBuffer.Length > PropertyIndexBufferMaxCache)
private static void ClearAndTrimIfNeeded<TKey, TValue>(Dictionary<TKey, TValue>? dict, int maxCapacity) where TKey : notnull
{
if (dict == null) return;
dict.Clear();
// TrimExcess only if the dictionary grew significantly beyond initial capacity
if (dict.EnsureCapacity(0) > maxCapacity)
{ {
dict.TrimExcess(); ArrayPool<int>.Shared.Return(_propertyIndexBuffer);
_propertyIndexBuffer = null;
} }
}
[MethodImpl(MethodImplOptions.AggressiveInlining)] if (_propertyStateBuffer != null && _propertyStateBuffer.Length > PropertyStateBufferMaxCache)
private static void ClearAndTrimIfNeeded<T>(HashSet<T>? set, int maxCapacity)
{
if (set == null) return;
set.Clear();
// TrimExcess only if the set grew significantly beyond initial capacity
if (set.EnsureCapacity(0) > maxCapacity)
{ {
set.TrimExcess(); ArrayPool<byte>.Shared.Return(_propertyStateBuffer);
_propertyStateBuffer = null;
} }
} }
@ -1179,6 +1209,18 @@ public static class AcBinarySerializer
ArrayPool<byte>.Shared.Return(_buffer); ArrayPool<byte>.Shared.Return(_buffer);
_buffer = null!; _buffer = null!;
} }
if (_propertyIndexBuffer != null)
{
ArrayPool<int>.Shared.Return(_propertyIndexBuffer);
_propertyIndexBuffer = null;
}
if (_propertyStateBuffer != null)
{
ArrayPool<byte>.Shared.Return(_propertyStateBuffer);
_propertyStateBuffer = null;
}
} }
#region Optimized Buffer Writing #region Optimized Buffer Writing
@ -1341,22 +1383,11 @@ public static class AcBinarySerializer
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteStringUtf8(string value) public void WriteStringUtf8(string value)
{ {
// For small strings, use stackalloc to avoid GetByteCount call var byteCount = Utf8NoBom.GetByteCount(value);
if (value.Length <= 128) WriteVarUInt((uint)byteCount);
{ EnsureCapacity(byteCount);
Span<byte> tempBuffer = stackalloc byte[value.Length * 3]; // Max UTF8 expansion Utf8NoBom.GetBytes(value.AsSpan(), _buffer.AsSpan(_position, byteCount));
var bytesWritten = Encoding.UTF8.GetBytes(value.AsSpan(), tempBuffer); _position += byteCount;
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 #endregion
@ -1620,15 +1651,78 @@ public static class AcBinarySerializer
return _propertyNames!.TryGetValue(name, out var index) ? index : -1; return _propertyNames!.TryGetValue(name, out var index) ? index : -1;
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int[] RentPropertyIndexBuffer(int minimumLength)
{
if (_propertyIndexBuffer != null && _propertyIndexBuffer.Length >= minimumLength)
{
var buffer = _propertyIndexBuffer;
_propertyIndexBuffer = null;
return buffer;
}
return ArrayPool<int>.Shared.Rent(minimumLength);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void ReturnPropertyIndexBuffer(int[] buffer)
{
if (_propertyIndexBuffer == null && buffer.Length <= PropertyIndexBufferMaxCache)
{
_propertyIndexBuffer = buffer;
return;
}
ArrayPool<int>.Shared.Return(buffer);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public byte[] RentPropertyStateBuffer(int minimumLength)
{
if (_propertyStateBuffer != null && _propertyStateBuffer.Length >= minimumLength)
{
var buffer = _propertyStateBuffer;
_propertyStateBuffer = null;
return buffer;
}
return ArrayPool<byte>.Shared.Rent(minimumLength);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void ReturnPropertyStateBuffer(byte[] buffer)
{
if (_propertyStateBuffer == null && buffer.Length <= PropertyStateBufferMaxCache)
{
_propertyStateBuffer = buffer;
return;
}
ArrayPool<byte>.Shared.Return(buffer);
}
#endregion #endregion
public byte[] ToArray() public byte[] ToArray()
{ {
var result = new byte[_position]; var result = GC.AllocateUninitializedArray<byte>(_position);
_buffer.AsSpan(0, _position).CopyTo(result); _buffer.AsSpan(0, _position).CopyTo(result);
return result; return result;
} }
public BinarySerializationResult DetachResult()
{
var buffer = _buffer;
var length = _position;
var result = new BinarySerializationResult(buffer, length, pooled: true);
var newSize = Math.Max(_initialBufferSize, MinBufferSize);
_buffer = ArrayPool<byte>.Shared.Rent(newSize);
_position = 0;
return result;
}
public void WriteTo(IBufferWriter<byte> writer) public void WriteTo(IBufferWriter<byte> writer)
{ {
// Directly write the internal buffer to the IBufferWriter // Directly write the internal buffer to the IBufferWriter
@ -1638,6 +1732,71 @@ public static class AcBinarySerializer
} }
public int Position => _position; public int Position => _position;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void ClearAndTrimIfNeeded<TKey, TValue>(Dictionary<TKey, TValue>? dict, int maxCapacity) where TKey : notnull
{
if (dict == null) return;
dict.Clear();
if (dict.EnsureCapacity(0) > maxCapacity)
{
dict.TrimExcess();
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void ClearAndTrimIfNeeded<T>(HashSet<T>? set, int maxCapacity)
{
if (set == null) return;
set.Clear();
if (set.EnsureCapacity(0) > maxCapacity)
{
set.TrimExcess();
}
}
}
#endregion
#region Serialization Result
public sealed class BinarySerializationResult : IDisposable
{
private readonly bool _pooled;
private bool _disposed;
internal BinarySerializationResult(byte[] buffer, int length, bool pooled)
{
Buffer = buffer;
Length = length;
_pooled = pooled;
}
public byte[] Buffer { get; }
public int Length { get; }
public ReadOnlySpan<byte> Span => Buffer.AsSpan(0, Length);
public ReadOnlyMemory<byte> Memory => new(Buffer, 0, Length);
public byte[] ToArray()
{
var result = GC.AllocateUninitializedArray<byte>(Length);
Buffer.AsSpan(0, Length).CopyTo(result);
return result;
}
public void Dispose()
{
if (_disposed) return;
_disposed = true;
if (_pooled)
{
ArrayPool<byte>.Shared.Return(Buffer);
}
}
internal static BinarySerializationResult FromImmutable(byte[] buffer)
=> new(buffer, buffer.Length, pooled: false);
} }
#endregion #endregion