Add header-based string interning to AcBinarySerializer
- Support preloaded string intern table in binary header for efficient string interning and reduced output size. - Add HeaderFlag_StringInternTable to BinaryTypeCode and update serializer/deserializer to handle intern tables. - Simplify string interning logic: always intern eligible strings, remove candidate tracking. - Refactor property name table and buffer management for clarity and efficiency. - Remove obsolete interning/property name methods from serializer context. - Add new output methods (ToArray, WriteTo, DetachResult) for buffer/result handling. - Introduce QuickBenchmark.cs with benchmarks comparing AcBinarySerializer (with/without interning), JSON, and MessagePack, including repeated string scenarios.
This commit is contained in:
parent
271f23d0f6
commit
b17c2df6c2
|
|
@ -0,0 +1,295 @@
|
|||
using System.Diagnostics;
|
||||
using AyCode.Core.Extensions;
|
||||
using AyCode.Core.Tests.TestModels;
|
||||
using MessagePack;
|
||||
using MessagePack.Resolvers;
|
||||
|
||||
namespace AyCode.Core.Tests.serialization;
|
||||
|
||||
[TestClass]
|
||||
public class QuickBenchmark
|
||||
{
|
||||
private static readonly MessagePackSerializerOptions MsgPackOptions =
|
||||
ContractlessStandardResolver.Options.WithCompression(MessagePackCompression.None);
|
||||
|
||||
[TestMethod]
|
||||
public void RunQuickBenchmark()
|
||||
{
|
||||
// Warmup
|
||||
var order = TestDataFactory.CreateBenchmarkOrder(2, 2, 2, 3);
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
var bytes = order.ToBinary();
|
||||
var result = bytes.BinaryTo<TestOrder>();
|
||||
}
|
||||
|
||||
// Measure serialize
|
||||
const int iterations = 1000;
|
||||
var sw = Stopwatch.StartNew();
|
||||
byte[] serialized = null!;
|
||||
for (int i = 0; i < iterations; i++)
|
||||
{
|
||||
serialized = order.ToBinary();
|
||||
}
|
||||
sw.Stop();
|
||||
var serializeMs = sw.Elapsed.TotalMilliseconds;
|
||||
|
||||
// Measure deserialize
|
||||
sw.Restart();
|
||||
TestOrder? deserialized = null;
|
||||
for (int i = 0; i < iterations; i++)
|
||||
{
|
||||
deserialized = serialized.BinaryTo<TestOrder>();
|
||||
}
|
||||
sw.Stop();
|
||||
var deserializeMs = sw.Elapsed.TotalMilliseconds;
|
||||
|
||||
// JSON comparison
|
||||
var jsonOptions = AcJsonSerializerOptions.WithoutReferenceHandling();
|
||||
sw.Restart();
|
||||
string json = null!;
|
||||
for (int i = 0; i < iterations; i++)
|
||||
{
|
||||
json = order.ToJson(jsonOptions);
|
||||
}
|
||||
sw.Stop();
|
||||
var jsonSerializeMs = sw.Elapsed.TotalMilliseconds;
|
||||
|
||||
sw.Restart();
|
||||
for (int i = 0; i < iterations; i++)
|
||||
{
|
||||
var _ = json.JsonTo<TestOrder>(jsonOptions);
|
||||
}
|
||||
sw.Stop();
|
||||
var jsonDeserializeMs = sw.Elapsed.TotalMilliseconds;
|
||||
|
||||
Console.WriteLine($"=== Quick Benchmark ({iterations} iterations) ===");
|
||||
Console.WriteLine($"Binary size: {serialized.Length} bytes");
|
||||
Console.WriteLine($"JSON size: {json.Length} chars ({System.Text.Encoding.UTF8.GetByteCount(json)} bytes)");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine($"Binary Serialize: {serializeMs:F2}ms ({serializeMs / iterations:F4}ms/op)");
|
||||
Console.WriteLine($"Binary Deserialize: {deserializeMs:F2}ms ({deserializeMs / iterations:F4}ms/op)");
|
||||
Console.WriteLine($"JSON Serialize: {jsonSerializeMs:F2}ms ({jsonSerializeMs / iterations:F4}ms/op)");
|
||||
Console.WriteLine($"JSON Deserialize: {jsonDeserializeMs:F2}ms ({jsonDeserializeMs / iterations:F4}ms/op)");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine($"Binary vs JSON Serialize: {serializeMs / jsonSerializeMs:F2}x");
|
||||
Console.WriteLine($"Binary vs JSON Deserialize: {deserializeMs / jsonDeserializeMs:F2}x");
|
||||
Console.WriteLine($"Size ratio: {100.0 * serialized.Length / System.Text.Encoding.UTF8.GetByteCount(json):F1}%");
|
||||
|
||||
Assert.IsNotNull(deserialized);
|
||||
Assert.AreEqual(order.Id, deserialized.Id);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void RunStringInterningBenchmark()
|
||||
{
|
||||
// Create data with repeated strings
|
||||
var items = Enumerable.Range(0, 100).Select(i => new TestClassWithRepeatedValues
|
||||
{
|
||||
Id = i,
|
||||
Status = i % 3 == 0 ? "Pending" : i % 3 == 1 ? "Processing" : "Completed",
|
||||
Category = $"Category_{i % 5}",
|
||||
Priority = i % 2 == 0 ? "High_Priority" : "Low_Priority"
|
||||
}).ToList();
|
||||
|
||||
// Warmup
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
var bytes = items.ToBinary();
|
||||
var result = bytes.BinaryTo<List<TestClassWithRepeatedValues>>();
|
||||
}
|
||||
|
||||
const int iterations = 1000;
|
||||
|
||||
// With interning (default)
|
||||
var sw = Stopwatch.StartNew();
|
||||
byte[] withInterning = null!;
|
||||
for (int i = 0; i < iterations; i++)
|
||||
{
|
||||
withInterning = AcBinarySerializer.Serialize(items, AcBinarySerializerOptions.Default);
|
||||
}
|
||||
sw.Stop();
|
||||
var withInterningMs = sw.Elapsed.TotalMilliseconds;
|
||||
|
||||
// Without interning
|
||||
var noInternOptions = new AcBinarySerializerOptions { UseStringInterning = false };
|
||||
sw.Restart();
|
||||
byte[] withoutInterning = null!;
|
||||
for (int i = 0; i < iterations; i++)
|
||||
{
|
||||
withoutInterning = AcBinarySerializer.Serialize(items, noInternOptions);
|
||||
}
|
||||
sw.Stop();
|
||||
var withoutInterningMs = sw.Elapsed.TotalMilliseconds;
|
||||
|
||||
Console.WriteLine($"=== String Interning Benchmark ({iterations} iterations) ===");
|
||||
Console.WriteLine($"With interning: {withInterning.Length} bytes, {withInterningMs:F2}ms");
|
||||
Console.WriteLine($"Without interning: {withoutInterning.Length} bytes, {withoutInterningMs:F2}ms");
|
||||
Console.WriteLine($"Size savings: {withoutInterning.Length - withInterning.Length} bytes ({100.0 * (withoutInterning.Length - withInterning.Length) / withoutInterning.Length:F1}%)");
|
||||
Console.WriteLine($"Speed ratio: {withInterningMs / withoutInterningMs:F2}x");
|
||||
|
||||
// Verify both deserialize correctly
|
||||
var result1 = withInterning.BinaryTo<List<TestClassWithRepeatedValues>>();
|
||||
var result2 = withoutInterning.BinaryTo<List<TestClassWithRepeatedValues>>();
|
||||
Assert.AreEqual(100, result1!.Count);
|
||||
Assert.AreEqual(100, result2!.Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void RunMessagePackComparison()
|
||||
{
|
||||
// Create test data
|
||||
var order = TestDataFactory.CreateBenchmarkOrder(3, 3, 3, 4);
|
||||
|
||||
// Warmup
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
var binBytes = AcBinarySerializer.Serialize(order, AcBinarySerializerOptions.Default);
|
||||
var binResult = AcBinaryDeserializer.Deserialize<TestOrder>(binBytes);
|
||||
var msgBytes = MessagePackSerializer.Serialize(order, MsgPackOptions);
|
||||
var msgResult = MessagePackSerializer.Deserialize<TestOrder>(msgBytes, MsgPackOptions);
|
||||
}
|
||||
|
||||
const int iterations = 1000;
|
||||
|
||||
// === AcBinary Serialize ===
|
||||
var sw = Stopwatch.StartNew();
|
||||
byte[] acBinaryData = null!;
|
||||
for (int i = 0; i < iterations; i++)
|
||||
{
|
||||
acBinaryData = AcBinarySerializer.Serialize(order, AcBinarySerializerOptions.Default);
|
||||
}
|
||||
sw.Stop();
|
||||
var acBinarySerMs = sw.Elapsed.TotalMilliseconds;
|
||||
|
||||
// === MessagePack Serialize ===
|
||||
sw.Restart();
|
||||
byte[] msgPackData = null!;
|
||||
for (int i = 0; i < iterations; i++)
|
||||
{
|
||||
msgPackData = MessagePackSerializer.Serialize(order, MsgPackOptions);
|
||||
}
|
||||
sw.Stop();
|
||||
var msgPackSerMs = sw.Elapsed.TotalMilliseconds;
|
||||
|
||||
// === AcBinary Deserialize ===
|
||||
sw.Restart();
|
||||
TestOrder? acBinaryResult = null;
|
||||
for (int i = 0; i < iterations; i++)
|
||||
{
|
||||
acBinaryResult = AcBinaryDeserializer.Deserialize<TestOrder>(acBinaryData);
|
||||
}
|
||||
sw.Stop();
|
||||
var acBinaryDeserMs = sw.Elapsed.TotalMilliseconds;
|
||||
|
||||
// === MessagePack Deserialize ===
|
||||
sw.Restart();
|
||||
TestOrder? msgPackResult = null;
|
||||
for (int i = 0; i < iterations; i++)
|
||||
{
|
||||
msgPackResult = MessagePackSerializer.Deserialize<TestOrder>(msgPackData, MsgPackOptions);
|
||||
}
|
||||
sw.Stop();
|
||||
var msgPackDeserMs = sw.Elapsed.TotalMilliseconds;
|
||||
|
||||
// Print results
|
||||
Console.WriteLine($"=== AcBinary vs MessagePack Benchmark ({iterations} iterations) ===");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine($"{"Metric",-25} {"AcBinary",12} {"MessagePack",12} {"Ratio",10}");
|
||||
Console.WriteLine(new string('-', 60));
|
||||
Console.WriteLine($"{"Size (bytes)",-25} {acBinaryData.Length,12:N0} {msgPackData.Length,12:N0} {100.0 * acBinaryData.Length / msgPackData.Length,9:F1}%");
|
||||
Console.WriteLine($"{"Serialize (ms)",-25} {acBinarySerMs,12:F2} {msgPackSerMs,12:F2} {acBinarySerMs / msgPackSerMs,9:F2}x");
|
||||
Console.WriteLine($"{"Deserialize (ms)",-25} {acBinaryDeserMs,12:F2} {msgPackDeserMs,12:F2} {acBinaryDeserMs / msgPackDeserMs,9:F2}x");
|
||||
Console.WriteLine($"{"Round-trip (ms)",-25} {acBinarySerMs + acBinaryDeserMs,12:F2} {msgPackSerMs + msgPackDeserMs,12:F2} {(acBinarySerMs + acBinaryDeserMs) / (msgPackSerMs + msgPackDeserMs),9:F2}x");
|
||||
Console.WriteLine();
|
||||
|
||||
var sizeDiff = msgPackData.Length - acBinaryData.Length;
|
||||
if (sizeDiff > 0)
|
||||
Console.WriteLine($"? AcBinary {sizeDiff:N0} bytes smaller ({100.0 * sizeDiff / msgPackData.Length:F1}% savings)");
|
||||
else
|
||||
Console.WriteLine($"?? AcBinary {-sizeDiff:N0} bytes larger");
|
||||
|
||||
Assert.IsNotNull(acBinaryResult);
|
||||
Assert.IsNotNull(msgPackResult);
|
||||
Assert.AreEqual(order.Id, acBinaryResult.Id);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void RunStringInterningVsMessagePack()
|
||||
{
|
||||
// Create data with many repeated strings (worst case for MessagePack, best for interning)
|
||||
var items = Enumerable.Range(0, 200).Select(i => new TestClassWithRepeatedValues
|
||||
{
|
||||
Id = i,
|
||||
Status = i % 3 == 0 ? "PendingStatus" : i % 3 == 1 ? "ProcessingStatus" : "CompletedStatus",
|
||||
Category = $"Category_{i % 5}",
|
||||
Priority = i % 2 == 0 ? "HighPriority" : "LowPriority"
|
||||
}).ToList();
|
||||
|
||||
// Warmup
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
var b1 = AcBinarySerializer.Serialize(items, AcBinarySerializerOptions.Default);
|
||||
var r1 = AcBinaryDeserializer.Deserialize<List<TestClassWithRepeatedValues>>(b1);
|
||||
var b2 = MessagePackSerializer.Serialize(items, MsgPackOptions);
|
||||
var r2 = MessagePackSerializer.Deserialize<List<TestClassWithRepeatedValues>>(b2, MsgPackOptions);
|
||||
}
|
||||
|
||||
const int iterations = 1000;
|
||||
|
||||
// AcBinary with interning
|
||||
var sw = Stopwatch.StartNew();
|
||||
byte[] acWithIntern = null!;
|
||||
for (int i = 0; i < iterations; i++)
|
||||
{
|
||||
acWithIntern = AcBinarySerializer.Serialize(items, AcBinarySerializerOptions.Default);
|
||||
}
|
||||
var acSerMs = sw.Elapsed.TotalMilliseconds;
|
||||
|
||||
sw.Restart();
|
||||
for (int i = 0; i < iterations; i++)
|
||||
{
|
||||
var _ = AcBinaryDeserializer.Deserialize<List<TestClassWithRepeatedValues>>(acWithIntern);
|
||||
}
|
||||
var acDeserMs = sw.Elapsed.TotalMilliseconds;
|
||||
|
||||
// MessagePack
|
||||
sw.Restart();
|
||||
byte[] msgPack = null!;
|
||||
for (int i = 0; i < iterations; i++)
|
||||
{
|
||||
msgPack = MessagePackSerializer.Serialize(items, MsgPackOptions);
|
||||
}
|
||||
var msgSerMs = sw.Elapsed.TotalMilliseconds;
|
||||
|
||||
sw.Restart();
|
||||
for (int i = 0; i < iterations; i++)
|
||||
{
|
||||
var _ = MessagePackSerializer.Deserialize<List<TestClassWithRepeatedValues>>(msgPack, MsgPackOptions);
|
||||
}
|
||||
var msgDeserMs = sw.Elapsed.TotalMilliseconds;
|
||||
|
||||
Console.WriteLine($"=== String Interning Advantage ({iterations} iterations, 200 items with repeated strings) ===");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine($"{"Metric",-25} {"AcBinary",12} {"MessagePack",12} {"Ratio",10}");
|
||||
Console.WriteLine(new string('-', 60));
|
||||
Console.WriteLine($"{"Size (bytes)",-25} {acWithIntern.Length,12:N0} {msgPack.Length,12:N0} {100.0 * acWithIntern.Length / msgPack.Length,9:F1}%");
|
||||
Console.WriteLine($"{"Serialize (ms)",-25} {acSerMs,12:F2} {msgSerMs,12:F2} {acSerMs / msgSerMs,9:F2}x");
|
||||
Console.WriteLine($"{"Deserialize (ms)",-25} {acDeserMs,12:F2} {msgDeserMs,12:F2} {acDeserMs / msgDeserMs,9:F2}x");
|
||||
Console.WriteLine();
|
||||
|
||||
var sizeSaving = msgPack.Length - acWithIntern.Length;
|
||||
Console.WriteLine($"? String interning saves {sizeSaving:N0} bytes ({100.0 * sizeSaving / msgPack.Length:F1}%)");
|
||||
|
||||
Assert.IsTrue(acWithIntern.Length < msgPack.Length, "AcBinary with interning should be smaller");
|
||||
}
|
||||
|
||||
// Public for MessagePack dynamic serializer compatibility
|
||||
public class TestClassWithRepeatedValues
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Status { get; set; } = "";
|
||||
public string Category { get; set; } = "";
|
||||
public string Priority { get; set; } = "";
|
||||
}
|
||||
}
|
||||
|
|
@ -1462,6 +1462,7 @@ public static class AcBinaryDeserializer
|
|||
public byte FormatVersion { get; private set; }
|
||||
public bool HasMetadata { get; private set; }
|
||||
public bool HasReferenceHandling { get; private set; }
|
||||
public bool HasPreloadedInternTable { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Minimum string length for interning. Must match serializer's MinStringInternLength.
|
||||
|
|
@ -1488,8 +1489,9 @@ public static class AcBinaryDeserializer
|
|||
_position = 0;
|
||||
FormatVersion = 0;
|
||||
HasMetadata = false;
|
||||
HasReferenceHandling = true; // Assume true by default
|
||||
MinStringInternLength = 4; // Default from AcBinarySerializerOptions
|
||||
HasReferenceHandling = true;
|
||||
HasPreloadedInternTable = false;
|
||||
MinStringInternLength = 4;
|
||||
_propertyNames = null;
|
||||
_internedStrings = null;
|
||||
_references = null;
|
||||
|
|
@ -1503,11 +1505,14 @@ public static class AcBinaryDeserializer
|
|||
FormatVersion = ReadByte();
|
||||
var flags = ReadByte();
|
||||
|
||||
// Handle new flag-based header format (34+)
|
||||
bool hasInternTable = false;
|
||||
|
||||
// Handle new flag-based header format (48+)
|
||||
if (flags >= BinaryTypeCode.HeaderFlagsBase)
|
||||
{
|
||||
HasMetadata = (flags & BinaryTypeCode.HeaderFlag_Metadata) != 0;
|
||||
HasReferenceHandling = (flags & BinaryTypeCode.HeaderFlag_ReferenceHandling) != 0;
|
||||
hasInternTable = (flags & BinaryTypeCode.HeaderFlag_StringInternTable) != 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -1531,6 +1536,21 @@ public static class AcBinaryDeserializer
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Read preloaded string intern table from header
|
||||
if (hasInternTable)
|
||||
{
|
||||
HasPreloadedInternTable = true;
|
||||
var internCount = (int)ReadVarUInt();
|
||||
// Always initialize the list, even if empty
|
||||
_internedStrings = new List<string>(internCount > 0 ? internCount : 4);
|
||||
for (int i = 0; i < internCount; i++)
|
||||
{
|
||||
var len = (int)ReadVarUInt();
|
||||
var str = ReadStringUtf8(len);
|
||||
_internedStrings.Add(str);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
|
|
@ -1784,6 +1804,9 @@ public static class AcBinaryDeserializer
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void RegisterInternedString(string value)
|
||||
{
|
||||
// Skip registration if intern table was preloaded from header
|
||||
if (HasPreloadedInternTable) return;
|
||||
|
||||
_internedStrings ??= new List<string>(16);
|
||||
_internedStrings.Add(value);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -155,8 +155,8 @@ public static class AcBinarySerializer
|
|||
RegisterMetadataForType(runtimeType, context);
|
||||
}
|
||||
|
||||
context.WriteMetadata();
|
||||
WriteValue(value, runtimeType, context, 0);
|
||||
context.FinalizeHeaderSections();
|
||||
return context;
|
||||
}
|
||||
|
||||
|
|
@ -597,23 +597,10 @@ public static class AcBinarySerializer
|
|||
|
||||
if (context.UseStringInterning && value.Length >= context.MinStringInternLength)
|
||||
{
|
||||
if (context.TryGetInternedStringIndex(value, out var index))
|
||||
{
|
||||
// Már regisztrált string - csak index
|
||||
context.WriteByte(BinaryTypeCode.StringInterned);
|
||||
context.WriteVarUInt((uint)index);
|
||||
return;
|
||||
}
|
||||
|
||||
if (context.TryPromoteInternCandidate(value, out var promotedIndex))
|
||||
{
|
||||
// Második előfordulás - StringInternNew: teljes tartalom + regisztráció
|
||||
context.WriteByte(BinaryTypeCode.StringInternNew);
|
||||
context.WriteStringUtf8(value);
|
||||
return;
|
||||
}
|
||||
|
||||
context.TrackInternCandidate(value);
|
||||
var index = context.RegisterInternedString(value);
|
||||
context.WriteByte(BinaryTypeCode.StringInterned);
|
||||
context.WriteVarUInt((uint)index);
|
||||
return;
|
||||
}
|
||||
|
||||
// Első előfordulás vagy nincs interning - sima string
|
||||
|
|
@ -1341,6 +1328,8 @@ public static class AcBinarySerializer
|
|||
private const int MinBufferSize = 256;
|
||||
private const int PropertyIndexBufferMaxCache = 512;
|
||||
private const int PropertyStateBufferMaxCache = 512;
|
||||
private const int InitialInternCapacity = 32;
|
||||
private const int InitialPropertyNameCapacity = 32;
|
||||
|
||||
// Reference handling
|
||||
private Dictionary<object, int>? _scanOccurrences;
|
||||
|
|
@ -1351,7 +1340,6 @@ public static class AcBinarySerializer
|
|||
// String interning
|
||||
private Dictionary<string, int>? _internedStrings;
|
||||
private List<string>? _internedStringList;
|
||||
private HashSet<string>? _internCandidates;
|
||||
|
||||
// Property name table
|
||||
private Dictionary<string, int>? _propertyNames;
|
||||
|
|
@ -1366,6 +1354,8 @@ public static class AcBinarySerializer
|
|||
public byte MinStringInternLength { get; private set; }
|
||||
public BinaryPropertyFilter? PropertyFilter { get; private set; }
|
||||
|
||||
public int Position => _position;
|
||||
|
||||
public BinarySerializationContext(AcBinarySerializerOptions options)
|
||||
{
|
||||
_initialBufferSize = Math.Max(options.InitialBufferCapacity, MinBufferSize);
|
||||
|
|
@ -1403,9 +1393,8 @@ public static class AcBinarySerializer
|
|||
ClearAndTrimIfNeeded(_internedStrings, InitialInternCapacity * 4);
|
||||
ClearAndTrimIfNeeded(_propertyNames, InitialPropertyNameCapacity * 4);
|
||||
|
||||
_internedStringList?.Clear();
|
||||
_propertyNameList?.Clear();
|
||||
_internCandidates?.Clear();
|
||||
_internedStringList?.Clear();
|
||||
|
||||
if (_propertyIndexBuffer != null && _propertyIndexBuffer.Length > PropertyIndexBufferMaxCache)
|
||||
{
|
||||
|
|
@ -1441,6 +1430,135 @@ public static class AcBinarySerializer
|
|||
}
|
||||
}
|
||||
|
||||
#region String Interning
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public int RegisterInternedString(string value)
|
||||
{
|
||||
_internedStrings ??= new Dictionary<string, int>(InitialInternCapacity, StringComparer.Ordinal);
|
||||
_internedStringList ??= new List<string>(InitialInternCapacity);
|
||||
|
||||
ref var index = ref CollectionsMarshal.GetValueRefOrAddDefault(_internedStrings, value, out var exists);
|
||||
if (exists)
|
||||
{
|
||||
return index;
|
||||
}
|
||||
|
||||
index = _internedStringList.Count;
|
||||
_internedStringList.Add(value);
|
||||
return index;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Property Name Table
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void RegisterPropertyName(string name)
|
||||
{
|
||||
_propertyNames ??= new Dictionary<string, int>(InitialPropertyNameCapacity, StringComparer.Ordinal);
|
||||
_propertyNameList ??= new List<string>(InitialPropertyNameCapacity);
|
||||
|
||||
if (!_propertyNames.ContainsKey(name))
|
||||
{
|
||||
var index = _propertyNameList.Count;
|
||||
_propertyNames[name] = index;
|
||||
_propertyNameList.Add(name);
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public int GetPropertyNameIndex(string name)
|
||||
{
|
||||
return _propertyNames != null && _propertyNames.TryGetValue(name, out var index) ? index : -1;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Property State Buffer
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public byte[] RentPropertyStateBuffer(int size)
|
||||
{
|
||||
if (_propertyStateBuffer != null && _propertyStateBuffer.Length >= size)
|
||||
{
|
||||
return _propertyStateBuffer;
|
||||
}
|
||||
|
||||
if (_propertyStateBuffer != null)
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(_propertyStateBuffer);
|
||||
}
|
||||
|
||||
_propertyStateBuffer = ArrayPool<byte>.Shared.Rent(size);
|
||||
return _propertyStateBuffer;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void ReturnPropertyStateBuffer(byte[] buffer)
|
||||
{
|
||||
// Buffer stays cached in _propertyStateBuffer for reuse
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Output
|
||||
|
||||
public byte[] ToArray()
|
||||
{
|
||||
var result = GC.AllocateUninitializedArray<byte>(_position);
|
||||
_buffer.AsSpan(0, _position).CopyTo(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
public void WriteTo(IBufferWriter<byte> writer)
|
||||
{
|
||||
var span = writer.GetSpan(_position);
|
||||
_buffer.AsSpan(0, _position).CopyTo(span);
|
||||
writer.Advance(_position);
|
||||
}
|
||||
|
||||
public BinarySerializationResult DetachResult()
|
||||
{
|
||||
var resultBuffer = _buffer;
|
||||
var resultLength = _position;
|
||||
|
||||
// Get a new buffer for this context
|
||||
_buffer = ArrayPool<byte>.Shared.Rent(_initialBufferSize);
|
||||
_position = 0;
|
||||
|
||||
return new BinarySerializationResult(resultBuffer, resultLength, pooled: true);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Helpers
|
||||
|
||||
[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 Property Filtering
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
|
|
@ -1728,14 +1846,53 @@ public static class AcBinarySerializer
|
|||
_position += 2;
|
||||
}
|
||||
|
||||
public void WriteMetadata()
|
||||
public void FinalizeHeaderSections()
|
||||
{
|
||||
_buffer[_headerPosition] = AcBinarySerializerOptions.FormatVersion;
|
||||
var hasPropertyNames = UseMetadata && _propertyNameList is { Count: > 0 };
|
||||
var hasInternTable = UseStringInterning && _internedStringList is { Count: > 0 };
|
||||
|
||||
var hasPropertyNames = _propertyNameList != null && _propertyNameList.Count > 0;
|
||||
// ArrayBufferWriter requires initialCapacity > 0
|
||||
var estimatedCapacity = hasPropertyNames ? _propertyNameList!.Count * 8 : 16;
|
||||
if (hasInternTable && _internedStringList != null)
|
||||
{
|
||||
estimatedCapacity += _internedStringList.Count * 8;
|
||||
}
|
||||
var headerWriter = new ArrayBufferWriter<byte>(Math.Max(estimatedCapacity, 16));
|
||||
|
||||
if (hasPropertyNames)
|
||||
{
|
||||
WriteHeaderVarUInt(headerWriter, (uint)_propertyNameList!.Count);
|
||||
foreach (var name in _propertyNameList)
|
||||
{
|
||||
WriteHeaderString(headerWriter, name);
|
||||
}
|
||||
}
|
||||
|
||||
if (hasInternTable)
|
||||
{
|
||||
WriteHeaderVarUInt(headerWriter, (uint)_internedStringList!.Count);
|
||||
foreach (var value in _internedStringList)
|
||||
{
|
||||
WriteHeaderString(headerWriter, value);
|
||||
}
|
||||
}
|
||||
|
||||
var headerPayload = headerWriter.WrittenSpan;
|
||||
if (headerPayload.Length > 0)
|
||||
{
|
||||
EnsureCapacity(headerPayload.Length);
|
||||
var bodyLength = _position - (_headerPosition + 2);
|
||||
if (bodyLength > 0)
|
||||
{
|
||||
Array.Copy(_buffer, _headerPosition + 2, _buffer, _headerPosition + 2 + headerPayload.Length, bodyLength);
|
||||
}
|
||||
|
||||
headerPayload.CopyTo(_buffer.AsSpan(_headerPosition + 2));
|
||||
_position += headerPayload.Length;
|
||||
}
|
||||
|
||||
byte flags = BinaryTypeCode.HeaderFlagsBase;
|
||||
if (UseMetadata && hasPropertyNames)
|
||||
if (hasPropertyNames)
|
||||
{
|
||||
flags |= BinaryTypeCode.HeaderFlag_Metadata;
|
||||
}
|
||||
|
|
@ -1745,16 +1902,36 @@ public static class AcBinarySerializer
|
|||
flags |= BinaryTypeCode.HeaderFlag_ReferenceHandling;
|
||||
}
|
||||
|
||||
_buffer[_headerPosition + 1] = flags;
|
||||
|
||||
if ((flags & BinaryTypeCode.HeaderFlag_Metadata) != 0)
|
||||
if (hasInternTable)
|
||||
{
|
||||
WriteVarUInt((uint)_propertyNameList!.Count);
|
||||
foreach (var name in _propertyNameList)
|
||||
{
|
||||
WriteStringUtf8(name);
|
||||
}
|
||||
flags |= BinaryTypeCode.HeaderFlag_StringInternTable;
|
||||
}
|
||||
|
||||
_buffer[_headerPosition] = AcBinarySerializerOptions.FormatVersion;
|
||||
_buffer[_headerPosition + 1] = flags;
|
||||
}
|
||||
|
||||
private static void WriteHeaderVarUInt(ArrayBufferWriter<byte> writer, uint value)
|
||||
{
|
||||
var span = writer.GetSpan(5);
|
||||
var index = 0;
|
||||
while (value >= 0x80)
|
||||
{
|
||||
span[index++] = (byte)(value | 0x80);
|
||||
value >>= 7;
|
||||
}
|
||||
|
||||
span[index++] = (byte)value;
|
||||
writer.Advance(index);
|
||||
}
|
||||
|
||||
private static void WriteHeaderString(ArrayBufferWriter<byte> writer, string value)
|
||||
{
|
||||
var byteCount = Utf8NoBom.GetByteCount(value);
|
||||
WriteHeaderVarUInt(writer, (uint)byteCount);
|
||||
var span = writer.GetSpan(byteCount);
|
||||
Utf8NoBom.GetBytes(value.AsSpan(), span);
|
||||
writer.Advance(byteCount);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
|
@ -1818,194 +1995,7 @@ public static class AcBinarySerializer
|
|||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region String Interning
|
||||
|
||||
private const int InitialInternCapacity = 16;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool TryGetInternedStringIndex(string value, out int index)
|
||||
{
|
||||
if (_internedStrings != null && _internedStrings.TryGetValue(value, out index))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
index = -1;
|
||||
return false;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void RegisterInternedString(string value)
|
||||
{
|
||||
_internedStrings ??= new Dictionary<string, int>(InitialInternCapacity, StringComparer.Ordinal);
|
||||
_internedStringList ??= new List<string>(InitialInternCapacity);
|
||||
|
||||
if (!_internedStrings.ContainsKey(value))
|
||||
{
|
||||
var index = _internedStringList.Count;
|
||||
_internedStrings[value] = index;
|
||||
_internedStringList.Add(value);
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void TrackInternCandidate(string value)
|
||||
{
|
||||
_internCandidates ??= new HashSet<string>(InitialInternCapacity, StringComparer.Ordinal);
|
||||
_internCandidates.Add(value);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool TryPromoteInternCandidate(string value, out int index)
|
||||
{
|
||||
if (_internCandidates != null && _internCandidates.Remove(value))
|
||||
{
|
||||
RegisterInternedString(value);
|
||||
return TryGetInternedStringIndex(value, out index);
|
||||
}
|
||||
|
||||
index = -1;
|
||||
return false;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Property Names
|
||||
|
||||
private const int InitialPropertyNameCapacity = 16;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void RegisterPropertyName(string name)
|
||||
{
|
||||
_propertyNames ??= new Dictionary<string, int>(InitialPropertyNameCapacity, StringComparer.Ordinal);
|
||||
_propertyNameList ??= new List<string>(InitialPropertyNameCapacity);
|
||||
|
||||
if (!_propertyNames.ContainsKey(name))
|
||||
{
|
||||
_propertyNames[name] = _propertyNameList.Count;
|
||||
_propertyNameList.Add(name);
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public int GetPropertyNameIndex(string name)
|
||||
{
|
||||
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
|
||||
|
||||
public byte[] ToArray()
|
||||
{
|
||||
var result = GC.AllocateUninitializedArray<byte>(_position);
|
||||
_buffer.AsSpan(0, _position).CopyTo(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)
|
||||
{
|
||||
var span = writer.GetSpan(_position);
|
||||
_buffer.AsSpan(0, _position).CopyTo(span);
|
||||
writer.Advance(_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
|
||||
|
||||
}
|
||||
|
|
@ -159,6 +159,7 @@ internal static class BinaryTypeCode
|
|||
public const byte HeaderFlagsBase = 48; // Base value for flag-based headers (0x30)
|
||||
public const byte HeaderFlag_Metadata = 0x01;
|
||||
public const byte HeaderFlag_ReferenceHandling = 0x02;
|
||||
public const byte HeaderFlag_StringInternTable = 0x04; // String intern pool preloaded in header
|
||||
|
||||
// Compact integer variants (for VarInt optimization)
|
||||
public const byte Int32Tiny = 64; // -16 to 111 stored in single byte (value = code - 64 - 16)
|
||||
|
|
|
|||
Loading…
Reference in New Issue