Compare commits
No commits in common. "5601c0d3e25adca519b6d6e6b221c720de17a670" and "2f1c00fd5c934f1525762efe05195ef857dcb0e0" have entirely different histories.
5601c0d3e2
...
2f1c00fd5c
|
|
@ -376,4 +376,3 @@ FodyWeavers.xsd
|
||||||
/BenchmarkSuite1/Results
|
/BenchmarkSuite1/Results
|
||||||
/CoverageReport
|
/CoverageReport
|
||||||
/Test_Benchmark_Results
|
/Test_Benchmark_Results
|
||||||
/size_output.txt
|
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,6 @@ 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;
|
||||||
|
|
||||||
|
|
@ -57,14 +53,14 @@ public class SimpleBinaryBenchmark
|
||||||
public void Setup()
|
public void Setup()
|
||||||
{
|
{
|
||||||
_testData = TestDataFactory.CreatePrimitiveTestData();
|
_testData = TestDataFactory.CreatePrimitiveTestData();
|
||||||
_binaryData = AcBinarySerializer.Serialize(_testData, AcBinarySerializerOptions.WithoutReferenceHandling());
|
_binaryData = AcBinarySerializer.Serialize(_testData);
|
||||||
_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, AcBinarySerializerOptions.WithoutReferenceHandling());
|
public byte[] SerializeBinary() => AcBinarySerializer.Serialize(_testData);
|
||||||
|
|
||||||
[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());
|
||||||
|
|
@ -78,7 +74,6 @@ 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]
|
||||||
|
|
@ -103,7 +98,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.WithoutReferenceHandling();
|
_binaryOptions = AcBinarySerializerOptions.Default;
|
||||||
_jsonOptions = AcJsonSerializerOptions.WithoutReferenceHandling();
|
_jsonOptions = AcJsonSerializerOptions.WithoutReferenceHandling();
|
||||||
|
|
||||||
Console.WriteLine("Serializing AcBinary...");
|
Console.WriteLine("Serializing AcBinary...");
|
||||||
|
|
@ -134,7 +129,7 @@ public class ComplexBinaryBenchmark
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Full comparison with MessagePack and BSON - AcBinary uses NO reference handling everywhere.
|
/// Full comparison with MessagePack - separate class to isolate potential issues.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ShortRunJob]
|
[ShortRunJob]
|
||||||
[MemoryDiagnoser]
|
[MemoryDiagnoser]
|
||||||
|
|
@ -144,7 +139,6 @@ 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!;
|
||||||
|
|
@ -161,7 +155,7 @@ public class MessagePackComparisonBenchmark
|
||||||
measurementsPerPallet: 2,
|
measurementsPerPallet: 2,
|
||||||
pointsPerMeasurement: 3);
|
pointsPerMeasurement: 3);
|
||||||
|
|
||||||
_binaryOptions = AcBinarySerializerOptions.WithoutReferenceHandling();
|
_binaryOptions = AcBinarySerializerOptions.Default;
|
||||||
_msgPackOptions = ContractlessStandardResolver.Options.WithCompression(MessagePackCompression.None);
|
_msgPackOptions = ContractlessStandardResolver.Options.WithCompression(MessagePackCompression.None);
|
||||||
_jsonOptions = AcJsonSerializerOptions.WithoutReferenceHandling();
|
_jsonOptions = AcJsonSerializerOptions.WithoutReferenceHandling();
|
||||||
|
|
||||||
|
|
@ -181,25 +175,10 @@ 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%)");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -209,28 +188,16 @@ 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: NoRef (everywhere), Populate, Serialize, Deserialize, Size
|
/// Tests: WithRef, NoRef, Populate, Serialize, Deserialize, Size
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ShortRunJob]
|
[ShortRunJob]
|
||||||
[MemoryDiagnoser]
|
[MemoryDiagnoser]
|
||||||
|
|
@ -247,7 +214,6 @@ 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!;
|
||||||
|
|
@ -272,8 +238,8 @@ public class AcBinaryVsMessagePackFullBenchmark
|
||||||
sharedUser: sharedUser,
|
sharedUser: sharedUser,
|
||||||
sharedMetadata: sharedMeta);
|
sharedMetadata: sharedMeta);
|
||||||
|
|
||||||
// Setup options - enforce no reference handling everywhere
|
// Setup options
|
||||||
_withRefOptions = AcBinarySerializerOptions.WithoutReferenceHandling();
|
_withRefOptions = AcBinarySerializerOptions.Default; // WithRef by default
|
||||||
_noRefOptions = AcBinarySerializerOptions.WithoutReferenceHandling();
|
_noRefOptions = AcBinarySerializerOptions.WithoutReferenceHandling();
|
||||||
_msgPackOptions = ContractlessStandardResolver.Options.WithCompression(MessagePackCompression.None);
|
_msgPackOptions = ContractlessStandardResolver.Options.WithCompression(MessagePackCompression.None);
|
||||||
|
|
||||||
|
|
@ -282,16 +248,6 @@ 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)
|
||||||
|
|
@ -306,66 +262,59 @@ 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 vs BSON)");
|
Console.WriteLine("?? SIZE COMPARISON (AcBinary vs MessagePack)");
|
||||||
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 / Math.Max(1, _msgPackData.Length):F1}% (WithRef - actually NoRef)");
|
Console.WriteLine($" AcBinary/MsgPack: {100.0 * _acBinaryWithRef.Length / _msgPackData.Length:F1}% (WithRef)");
|
||||||
Console.WriteLine($" AcBinary/MsgPack: {100.0 * _acBinaryNoRef.Length / Math.Max(1, _msgPackData.Length):F1}% (NoRef)");
|
Console.WriteLine($" AcBinary/MsgPack: {100.0 * _acBinaryNoRef.Length / _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 NoRef")]
|
[Benchmark(Description = "AcBinary Populate WithRef")]
|
||||||
public void Populate_AcBinary_NoRef()
|
public void Populate_AcBinary_WithRef()
|
||||||
{
|
{
|
||||||
// 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(_acBinaryNoRef, target);
|
AcBinaryDeserializer.Populate(_acBinaryWithRef, target);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Benchmark(Description = "AcBinary PopulateMerge NoRef")]
|
[Benchmark(Description = "AcBinary PopulateMerge WithRef")]
|
||||||
public void PopulateMerge_AcBinary_NoRef()
|
public void PopulateMerge_AcBinary_WithRef()
|
||||||
{
|
{
|
||||||
// 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(_acBinaryNoRef.AsSpan(), target);
|
AcBinaryDeserializer.PopulateMerge(_acBinaryWithRef.AsSpan(), target);
|
||||||
}
|
}
|
||||||
|
|
||||||
private TestOrder CreatePopulateTarget()
|
private TestOrder CreatePopulateTarget()
|
||||||
|
|
@ -383,7 +332,6 @@ 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]
|
||||||
|
|
@ -401,7 +349,7 @@ public class SizeComparisonBenchmark
|
||||||
public void Setup()
|
public void Setup()
|
||||||
{
|
{
|
||||||
_msgPackOptions = ContractlessStandardResolver.Options.WithCompression(MessagePackCompression.None);
|
_msgPackOptions = ContractlessStandardResolver.Options.WithCompression(MessagePackCompression.None);
|
||||||
_withRefOptions = AcBinarySerializerOptions.WithoutReferenceHandling();
|
_withRefOptions = AcBinarySerializerOptions.Default;
|
||||||
_noRefOptions = AcBinarySerializerOptions.WithoutReferenceHandling();
|
_noRefOptions = AcBinarySerializerOptions.WithoutReferenceHandling();
|
||||||
|
|
||||||
// Small order
|
// Small order
|
||||||
|
|
@ -431,7 +379,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 vs BSON");
|
Console.WriteLine("?? DETAILED SIZE COMPARISON: AcBinary vs MessagePack");
|
||||||
Console.WriteLine(new string('=', 80));
|
Console.WriteLine(new string('=', 80));
|
||||||
|
|
||||||
PrintOrderSize("Small Order (1x1x1x2)", _smallOrder);
|
PrintOrderSize("Small Order (1x1x1x2)", _smallOrder);
|
||||||
|
|
@ -446,14 +394,11 @@ 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 / Math.Max(1, msgPack.Length),5:F1}% of MsgPack)");
|
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 / 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($" 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;
|
||||||
|
|
|
||||||
|
|
@ -42,49 +42,48 @@ 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()
|
||||||
{
|
{
|
||||||
RegisterReader(BinaryTypeCode.Null, static (ref BinaryDeserializationContext _, Type _, int _) => null);
|
// Initialize type reader dispatch table
|
||||||
RegisterReader(BinaryTypeCode.True, static (ref BinaryDeserializationContext _, Type _, int _) => true);
|
var readers = new Dictionary<byte, TypeReader>
|
||||||
RegisterReader(BinaryTypeCode.False, static (ref BinaryDeserializationContext _, Type _, int _) => false);
|
{
|
||||||
RegisterReader(BinaryTypeCode.Int8, static (ref BinaryDeserializationContext ctx, Type _, int _) => (sbyte)ctx.ReadByte());
|
[BinaryTypeCode.Null] = static (ref BinaryDeserializationContext _, Type _, int _) => null,
|
||||||
RegisterReader(BinaryTypeCode.UInt8, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadByte());
|
[BinaryTypeCode.True] = static (ref BinaryDeserializationContext _, Type _, int _) => true,
|
||||||
RegisterReader(BinaryTypeCode.Int16, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadInt16Unsafe());
|
[BinaryTypeCode.False] = static (ref BinaryDeserializationContext _, Type _, int _) => false,
|
||||||
RegisterReader(BinaryTypeCode.UInt16, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadUInt16Unsafe());
|
[BinaryTypeCode.Int8] = static (ref BinaryDeserializationContext ctx, Type _, int _) => (sbyte)ctx.ReadByte(),
|
||||||
RegisterReader(BinaryTypeCode.Int32, static (ref BinaryDeserializationContext ctx, Type type, int _) => ReadInt32Value(ref ctx, type));
|
[BinaryTypeCode.UInt8] = static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadByte(),
|
||||||
RegisterReader(BinaryTypeCode.UInt32, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadVarUInt());
|
[BinaryTypeCode.Int16] = static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadInt16Unsafe(),
|
||||||
RegisterReader(BinaryTypeCode.Int64, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadVarLong());
|
[BinaryTypeCode.UInt16] = static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadUInt16Unsafe(),
|
||||||
RegisterReader(BinaryTypeCode.UInt64, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadVarULong());
|
[BinaryTypeCode.Int32] = static (ref BinaryDeserializationContext ctx, Type type, int _) => ReadInt32Value(ref ctx, type),
|
||||||
RegisterReader(BinaryTypeCode.Float32, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadSingleUnsafe());
|
[BinaryTypeCode.UInt32] = static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadVarUInt(),
|
||||||
RegisterReader(BinaryTypeCode.Float64, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadDoubleUnsafe());
|
[BinaryTypeCode.Int64] = static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadVarLong(),
|
||||||
RegisterReader(BinaryTypeCode.Decimal, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadDecimalUnsafe());
|
[BinaryTypeCode.UInt64] = static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadVarULong(),
|
||||||
RegisterReader(BinaryTypeCode.Char, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadCharUnsafe());
|
[BinaryTypeCode.Float32] = static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadSingleUnsafe(),
|
||||||
RegisterReader(BinaryTypeCode.String, static (ref BinaryDeserializationContext ctx, Type _, int _) => ReadAndInternString(ref ctx));
|
[BinaryTypeCode.Float64] = static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadDoubleUnsafe(),
|
||||||
RegisterReader(BinaryTypeCode.StringInterned, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.GetInternedString((int)ctx.ReadVarUInt()));
|
[BinaryTypeCode.Decimal] = static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadDecimalUnsafe(),
|
||||||
RegisterReader(BinaryTypeCode.StringEmpty, static (ref BinaryDeserializationContext _, Type _, int _) => string.Empty);
|
[BinaryTypeCode.Char] = static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadCharUnsafe(),
|
||||||
RegisterReader(BinaryTypeCode.DateTime, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadDateTimeUnsafe());
|
[BinaryTypeCode.String] = static (ref BinaryDeserializationContext ctx, Type _, int _) => ReadAndInternString(ref ctx),
|
||||||
RegisterReader(BinaryTypeCode.DateTimeOffset, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadDateTimeOffsetUnsafe());
|
[BinaryTypeCode.StringInterned] = static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.GetInternedString((int)ctx.ReadVarUInt()),
|
||||||
RegisterReader(BinaryTypeCode.TimeSpan, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadTimeSpanUnsafe());
|
[BinaryTypeCode.StringEmpty] = static (ref BinaryDeserializationContext _, Type _, int _) => string.Empty,
|
||||||
RegisterReader(BinaryTypeCode.Guid, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadGuidUnsafe());
|
[BinaryTypeCode.DateTime] = static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadDateTimeUnsafe(),
|
||||||
RegisterReader(BinaryTypeCode.Enum, static (ref BinaryDeserializationContext ctx, Type type, int _) => ReadEnumValue(ref ctx, type));
|
[BinaryTypeCode.DateTimeOffset] = static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadDateTimeOffsetUnsafe(),
|
||||||
RegisterReader(BinaryTypeCode.Object, ReadObject);
|
[BinaryTypeCode.TimeSpan] = static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadTimeSpanUnsafe(),
|
||||||
RegisterReader(BinaryTypeCode.ObjectRef, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.GetReferencedObject(ctx.ReadVarInt()));
|
[BinaryTypeCode.Guid] = static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadGuidUnsafe(),
|
||||||
RegisterReader(BinaryTypeCode.Array, ReadArray);
|
[BinaryTypeCode.Enum] = static (ref BinaryDeserializationContext ctx, Type type, int _) => ReadEnumValue(ref ctx, type),
|
||||||
RegisterReader(BinaryTypeCode.Dictionary, ReadDictionary);
|
[BinaryTypeCode.Object] = ReadObject,
|
||||||
RegisterReader(BinaryTypeCode.ByteArray, static (ref BinaryDeserializationContext ctx, Type _, int _) => ReadByteArray(ref ctx));
|
[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();
|
||||||
}
|
}
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
||||||
private static void RegisterReader(byte typeCode, TypeReader reader) => TypeReaders[typeCode] = reader;
|
|
||||||
|
|
||||||
#region Public API
|
#region Public API
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -270,8 +269,8 @@ public static class AcBinaryDeserializer
|
||||||
return ConvertToTargetType(intValue, targetType);
|
return ConvertToTargetType(intValue, targetType);
|
||||||
}
|
}
|
||||||
|
|
||||||
var reader = TypeReaders[typeCode];
|
// Use dispatch table for type-specific reading
|
||||||
if (reader != null)
|
if (TypeReaders.TryGetValue(typeCode, out var reader))
|
||||||
{
|
{
|
||||||
return reader(ref context, targetType, depth);
|
return reader(ref context, targetType, depth);
|
||||||
}
|
}
|
||||||
|
|
@ -295,7 +294,6 @@ public static class AcBinaryDeserializer
|
||||||
{
|
{
|
||||||
context.RegisterInternedString(str);
|
context.RegisterInternedString(str);
|
||||||
}
|
}
|
||||||
|
|
||||||
return str;
|
return str;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -309,11 +307,12 @@ 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 info = GetConversionInfo(targetType);
|
var underlying = Nullable.GetUnderlyingType(targetType) ?? targetType;
|
||||||
if (info.IsEnum)
|
if (underlying.IsEnum)
|
||||||
return Enum.ToObject(info.UnderlyingType, value);
|
return Enum.ToObject(underlying, value);
|
||||||
|
|
||||||
return info.TypeCode switch
|
var typeCode = Type.GetTypeCode(underlying);
|
||||||
|
return typeCode switch
|
||||||
{
|
{
|
||||||
TypeCode.Int32 => value,
|
TypeCode.Int32 => value,
|
||||||
TypeCode.Int64 => (long)value,
|
TypeCode.Int64 => (long)value,
|
||||||
|
|
@ -330,20 +329,12 @@ 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 info = GetConversionInfo(targetType);
|
var underlying = Nullable.GetUnderlyingType(targetType) ?? targetType;
|
||||||
var nextByte = context.ReadByte();
|
var nextByte = context.ReadByte();
|
||||||
|
|
||||||
int intValue;
|
int intValue;
|
||||||
if (BinaryTypeCode.IsTinyInt(nextByte))
|
if (BinaryTypeCode.IsTinyInt(nextByte))
|
||||||
{
|
{
|
||||||
|
|
@ -360,7 +351,9 @@ public static class AcBinaryDeserializer
|
||||||
context.Position, targetType);
|
context.Position, targetType);
|
||||||
}
|
}
|
||||||
|
|
||||||
return info.IsEnum ? Enum.ToObject(info.UnderlyingType, intValue) : intValue;
|
if (underlying.IsEnum)
|
||||||
|
return Enum.ToObject(underlying, intValue);
|
||||||
|
return intValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
|
@ -407,7 +400,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)
|
||||||
{
|
{
|
||||||
|
|
@ -466,7 +459,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)
|
||||||
{
|
{
|
||||||
|
|
@ -478,7 +471,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)
|
||||||
{
|
{
|
||||||
|
|
@ -741,7 +734,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)
|
||||||
{
|
{
|
||||||
|
|
@ -751,7 +744,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;
|
||||||
}
|
}
|
||||||
|
|
@ -759,7 +752,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
|
||||||
|
|
@ -783,7 +776,6 @@ public static class AcBinaryDeserializer
|
||||||
acObservable?.EndUpdate();
|
acObservable?.EndUpdate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Array Reading
|
#region Array Reading
|
||||||
|
|
@ -811,7 +803,6 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -821,10 +812,7 @@ 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
|
catch { /* Fallback to List<T> */ }
|
||||||
{
|
|
||||||
/* Fallback to List<T> */
|
|
||||||
}
|
|
||||||
|
|
||||||
list ??= GetOrCreateListFactory(elementType)();
|
list ??= GetOrCreateListFactory(elementType)();
|
||||||
|
|
||||||
|
|
@ -867,7 +855,6 @@ public static class AcBinaryDeserializer
|
||||||
else
|
else
|
||||||
return null; // Fall back to generic path
|
return null; // Fall back to generic path
|
||||||
}
|
}
|
||||||
|
|
||||||
return array;
|
return array;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -881,7 +868,6 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -901,7 +887,6 @@ public static class AcBinaryDeserializer
|
||||||
else
|
else
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return array;
|
return array;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -916,7 +901,6 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -930,7 +914,6 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -944,7 +927,6 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -958,7 +940,6 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1014,7 +995,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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1147,7 +1128,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)
|
||||||
{
|
{
|
||||||
|
|
@ -1186,7 +1167,6 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
@ -1248,7 +1228,7 @@ public static class AcBinaryDeserializer
|
||||||
|
|
||||||
internal sealed class BinaryDeserializeTypeMetadata
|
internal sealed class BinaryDeserializeTypeMetadata
|
||||||
{
|
{
|
||||||
private readonly FrozenDictionary<string, BinaryPropertySetterInfo> _propertiesDict;
|
private readonly Dictionary<string, BinaryPropertySetterInfo> _propertiesDict;
|
||||||
public BinaryPropertySetterInfo[] PropertiesArray { get; }
|
public BinaryPropertySetterInfo[] PropertiesArray { get; }
|
||||||
public Func<object>? CompiledConstructor { get; }
|
public Func<object>? CompiledConstructor { get; }
|
||||||
|
|
||||||
|
|
@ -1272,20 +1252,16 @@ public static class AcBinaryDeserializer
|
||||||
propsList.Add(p);
|
propsList.Add(p);
|
||||||
}
|
}
|
||||||
|
|
||||||
var propInfos = new BinaryPropertySetterInfo[propsList.Count];
|
_propertiesDict = new Dictionary<string, BinaryPropertySetterInfo>(propsList.Count, StringComparer.OrdinalIgnoreCase);
|
||||||
|
PropertiesArray = new BinaryPropertySetterInfo[propsList.Count];
|
||||||
|
|
||||||
for (int i = 0; i < propsList.Count; i++)
|
for (int i = 0; i < propsList.Count; i++)
|
||||||
{
|
{
|
||||||
propInfos[i] = new BinaryPropertySetterInfo(propsList[i], type);
|
var prop = propsList[i];
|
||||||
|
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)]
|
||||||
|
|
@ -1319,20 +1295,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)
|
||||||
{
|
{
|
||||||
|
|
@ -1408,7 +1384,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)
|
||||||
|
|
@ -1627,7 +1603,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;
|
||||||
|
|
@ -1714,7 +1690,14 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
@ -1759,18 +1742,4 @@ 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
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;
|
||||||
|
|
@ -48,10 +47,34 @@ public static class AcBinarySerializer
|
||||||
return [BinaryTypeCode.Null];
|
return [BinaryTypeCode.Null];
|
||||||
}
|
}
|
||||||
|
|
||||||
var runtimeType = value.GetType();
|
var type = 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
|
||||||
|
|
@ -74,10 +97,26 @@ public static class AcBinarySerializer
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var runtimeType = value.GetType();
|
var type = value.GetType();
|
||||||
var context = SerializeCore(value, runtimeType, options);
|
var context = BinarySerializationContextPool.Get(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
|
||||||
|
|
@ -94,10 +133,25 @@ public static class AcBinarySerializer
|
||||||
{
|
{
|
||||||
if (value == null) return 1;
|
if (value == null) return 1;
|
||||||
|
|
||||||
var runtimeType = value.GetType();
|
var type = value.GetType();
|
||||||
var context = SerializeCore(value, runtimeType, options);
|
var context = BinarySerializationContextPool.Get(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
|
||||||
|
|
@ -106,49 +160,6 @@ 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
|
||||||
|
|
@ -194,28 +205,33 @@ public static class AcBinarySerializer
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Property Metadata Registration
|
#region Property Name Collection
|
||||||
|
|
||||||
private static void RegisterMetadataForType(Type type, BinarySerializationContext context, HashSet<Type>? visited = null)
|
private static void CollectPropertyNames(object? value, BinarySerializationContext context, int depth)
|
||||||
{
|
{
|
||||||
|
if (value == null || depth > context.MaxDepth) return;
|
||||||
|
|
||||||
|
var type = value.GetType();
|
||||||
if (IsPrimitiveOrStringFast(type)) return;
|
if (IsPrimitiveOrStringFast(type)) return;
|
||||||
|
|
||||||
visited ??= new HashSet<Type>();
|
if (value is byte[]) return;
|
||||||
if (!visited.Add(type)) return;
|
|
||||||
|
|
||||||
if (IsDictionaryType(type, out var keyType, out var valueType))
|
if (value is IDictionary dictionary)
|
||||||
{
|
{
|
||||||
if (keyType != null) RegisterMetadataForType(keyType, context, visited);
|
foreach (DictionaryEntry entry in dictionary)
|
||||||
if (valueType != null) RegisterMetadataForType(valueType, context, visited);
|
{
|
||||||
|
if (entry.Value != null)
|
||||||
|
CollectPropertyNames(entry.Value, context, depth + 1);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof(IEnumerable).IsAssignableFrom(type) && !ReferenceEquals(type, StringType))
|
if (value is IEnumerable enumerable && !ReferenceEquals(type, StringType))
|
||||||
{
|
{
|
||||||
var elementType = GetCollectionElementType(type);
|
foreach (var item in enumerable)
|
||||||
if (elementType != null)
|
|
||||||
{
|
{
|
||||||
RegisterMetadataForType(elementType, context, visited);
|
if (item != null)
|
||||||
|
CollectPropertyNames(item, context, depth + 1);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -224,46 +240,12 @@ 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 (TryResolveNestedMetadataType(prop.PropertyType, out var nestedType))
|
if (propValue != null)
|
||||||
{
|
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
|
||||||
|
|
@ -624,35 +606,27 @@ public static class AcBinarySerializer
|
||||||
var properties = metadata.Properties;
|
var properties = metadata.Properties;
|
||||||
var propCount = properties.Length;
|
var propCount = properties.Length;
|
||||||
|
|
||||||
const int StackThreshold = 64;
|
// Single-pass: count and collect non-null, non-default properties
|
||||||
byte[]? rentedStates = null;
|
// Use stackalloc for small property counts to avoid allocation
|
||||||
Span<byte> propertyStates = propCount <= StackThreshold
|
Span<int> validIndices = propCount <= 32 ? stackalloc int[propCount] : new int[propCount];
|
||||||
? 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++)
|
||||||
{
|
{
|
||||||
if (IsPropertyDefaultOrNull(value, properties[i]))
|
var prop = 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);
|
||||||
|
|
||||||
for (var i = 0; i < propCount; i++)
|
// Write only the valid properties
|
||||||
|
for (var j = 0; j < writtenCount; j++)
|
||||||
{
|
{
|
||||||
if (propertyStates[i] == 0)
|
var prop = properties[validIndices[j]];
|
||||||
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);
|
||||||
|
|
@ -663,13 +637,9 @@ 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>
|
||||||
|
|
@ -1119,12 +1089,9 @@ 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;
|
||||||
|
|
@ -1139,8 +1106,6 @@ 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; }
|
||||||
|
|
@ -1150,8 +1115,8 @@ public static class AcBinarySerializer
|
||||||
|
|
||||||
public BinarySerializationContext(AcBinarySerializerOptions options)
|
public BinarySerializationContext(AcBinarySerializerOptions options)
|
||||||
{
|
{
|
||||||
_initialBufferSize = Math.Max(options.InitialBufferCapacity, MinBufferSize);
|
var size = Math.Max(options.InitialBufferCapacity, MinBufferSize);
|
||||||
_buffer = ArrayPool<byte>.Shared.Rent(_initialBufferSize);
|
_buffer = ArrayPool<byte>.Shared.Rent(size);
|
||||||
Reset(options);
|
Reset(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1164,13 +1129,6 @@ 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()
|
||||||
|
|
@ -1188,17 +1146,29 @@ public static class AcBinarySerializer
|
||||||
|
|
||||||
_internedStringList?.Clear();
|
_internedStringList?.Clear();
|
||||||
_propertyNameList?.Clear();
|
_propertyNameList?.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
if (_propertyIndexBuffer != null && _propertyIndexBuffer.Length > PropertyIndexBufferMaxCache)
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
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)
|
||||||
{
|
{
|
||||||
ArrayPool<int>.Shared.Return(_propertyIndexBuffer);
|
dict.TrimExcess();
|
||||||
_propertyIndexBuffer = null;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (_propertyStateBuffer != null && _propertyStateBuffer.Length > PropertyStateBufferMaxCache)
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
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)
|
||||||
{
|
{
|
||||||
ArrayPool<byte>.Shared.Return(_propertyStateBuffer);
|
set.TrimExcess();
|
||||||
_propertyStateBuffer = null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1209,18 +1179,6 @@ 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
|
||||||
|
|
@ -1383,11 +1341,22 @@ public static class AcBinarySerializer
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public void WriteStringUtf8(string value)
|
public void WriteStringUtf8(string value)
|
||||||
{
|
{
|
||||||
var byteCount = Utf8NoBom.GetByteCount(value);
|
// For small strings, use stackalloc to avoid GetByteCount call
|
||||||
WriteVarUInt((uint)byteCount);
|
if (value.Length <= 128)
|
||||||
EnsureCapacity(byteCount);
|
{
|
||||||
Utf8NoBom.GetBytes(value.AsSpan(), _buffer.AsSpan(_position, byteCount));
|
Span<byte> tempBuffer = stackalloc byte[value.Length * 3]; // Max UTF8 expansion
|
||||||
_position += byteCount;
|
var bytesWritten = Encoding.UTF8.GetBytes(value.AsSpan(), tempBuffer);
|
||||||
|
WriteVarUInt((uint)bytesWritten);
|
||||||
|
WriteBytes(tempBuffer.Slice(0, bytesWritten));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var utf8Length = Encoding.UTF8.GetByteCount(value);
|
||||||
|
WriteVarUInt((uint)utf8Length);
|
||||||
|
EnsureCapacity(utf8Length);
|
||||||
|
Encoding.UTF8.GetBytes(value, _buffer.AsSpan(_position, utf8Length));
|
||||||
|
_position += utf8Length;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
@ -1651,78 +1620,15 @@ 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 = GC.AllocateUninitializedArray<byte>(_position);
|
var result = new 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
|
||||||
|
|
@ -1732,71 +1638,6 @@ 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
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue