diff --git a/AyCode.Core.Tests/Serialization/QuickBenchmark.cs b/AyCode.Core.Tests/Serialization/QuickBenchmark.cs new file mode 100644 index 0000000..4767b73 --- /dev/null +++ b/AyCode.Core.Tests/Serialization/QuickBenchmark.cs @@ -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(); + } + + // 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(); + } + 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(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>(); + } + + 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>(); + var result2 = withoutInterning.BinaryTo>(); + 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(binBytes); + var msgBytes = MessagePackSerializer.Serialize(order, MsgPackOptions); + var msgResult = MessagePackSerializer.Deserialize(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(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(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>(b1); + var b2 = MessagePackSerializer.Serialize(items, MsgPackOptions); + var r2 = MessagePackSerializer.Deserialize>(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>(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>(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; } = ""; + } +} diff --git a/AyCode.Core/Extensions/AcBinaryDeserializer.cs b/AyCode.Core/Extensions/AcBinaryDeserializer.cs index 23775d3..ef6cae6 100644 --- a/AyCode.Core/Extensions/AcBinaryDeserializer.cs +++ b/AyCode.Core/Extensions/AcBinaryDeserializer.cs @@ -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; } /// /// 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(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(16); _internedStrings.Add(value); } diff --git a/AyCode.Core/Extensions/AcBinarySerializer.cs b/AyCode.Core/Extensions/AcBinarySerializer.cs index 95188ac..5d4b892 100644 --- a/AyCode.Core/Extensions/AcBinarySerializer.cs +++ b/AyCode.Core/Extensions/AcBinarySerializer.cs @@ -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? _scanOccurrences; @@ -1351,7 +1340,6 @@ public static class AcBinarySerializer // String interning private Dictionary? _internedStrings; private List? _internedStringList; - private HashSet? _internCandidates; // Property name table private Dictionary? _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(InitialInternCapacity, StringComparer.Ordinal); + _internedStringList ??= new List(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(InitialPropertyNameCapacity, StringComparer.Ordinal); + _propertyNameList ??= new List(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.Shared.Return(_propertyStateBuffer); + } + + _propertyStateBuffer = ArrayPool.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(_position); + _buffer.AsSpan(0, _position).CopyTo(result); + return result; + } + + public void WriteTo(IBufferWriter 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.Shared.Rent(_initialBufferSize); + _position = 0; + + return new BinarySerializationResult(resultBuffer, resultLength, pooled: true); + } + + #endregion + + #region Helpers + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void ClearAndTrimIfNeeded(Dictionary? 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(HashSet? 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(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 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 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(InitialInternCapacity, StringComparer.Ordinal); - _internedStringList ??= new List(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(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(InitialPropertyNameCapacity, StringComparer.Ordinal); - _propertyNameList ??= new List(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.Shared.Rent(minimumLength); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void ReturnPropertyIndexBuffer(int[] buffer) - { - if (_propertyIndexBuffer == null && buffer.Length <= PropertyIndexBufferMaxCache) - { - _propertyIndexBuffer = buffer; - return; - } - - ArrayPool.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.Shared.Rent(minimumLength); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void ReturnPropertyStateBuffer(byte[] buffer) - { - if (_propertyStateBuffer == null && buffer.Length <= PropertyStateBufferMaxCache) - { - _propertyStateBuffer = buffer; - return; - } - - ArrayPool.Shared.Return(buffer); - } - - #endregion - - public byte[] ToArray() - { - var result = GC.AllocateUninitializedArray(_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.Shared.Rent(newSize); - _position = 0; - - return result; - } - - public void WriteTo(IBufferWriter 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(Dictionary? 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(HashSet? set, int maxCapacity) - { - if (set == null) - { - return; - } - - set.Clear(); - if (set.EnsureCapacity(0) > maxCapacity) - { - set.TrimExcess(); - } - } } #endregion - } \ No newline at end of file diff --git a/AyCode.Core/Extensions/AcBinarySerializerOptions.cs b/AyCode.Core/Extensions/AcBinarySerializerOptions.cs index 9b4de01..4ff28b4 100644 --- a/AyCode.Core/Extensions/AcBinarySerializerOptions.cs +++ b/AyCode.Core/Extensions/AcBinarySerializerOptions.cs @@ -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)