From 145cc0a493f4a7b1d411ab56040bbd2eff8a4ecc Mon Sep 17 00:00:00 2001 From: Loretta Date: Sat, 24 Jan 2026 13:18:33 +0100 Subject: [PATCH] Add profiler mode & optimize AcBinary string interning - Add "profiler" mode for memory profiling AcBinary serialization - Reduce warmup iterations from 10 to 5 for faster benchmarks - Save large test binary output to separate .output file (hex dump) - Improve robustness of AcBinary vs MessagePack result comparison - Use DisplayName for test data in result output for clarity - Optimize AcBinary string interning: use single contiguous buffer - Update WriteFooterStrings to avoid per-string allocations - Clarify WithoutReferenceHandling() disables string interning for speed --- AyCode.Core.Serializers.Console/Program.cs | 188 +++++++++++++----- ...rySerializer.BinarySerializationContext.cs | 87 +++++--- .../Binaries/AcBinarySerializerOptions.cs | 7 +- 3 files changed, 210 insertions(+), 72 deletions(-) diff --git a/AyCode.Core.Serializers.Console/Program.cs b/AyCode.Core.Serializers.Console/Program.cs index 5a600a6..5fbc82b 100644 --- a/AyCode.Core.Serializers.Console/Program.cs +++ b/AyCode.Core.Serializers.Console/Program.cs @@ -43,7 +43,7 @@ public static class Program private static readonly UTF8Encoding Utf8NoBom = new(encoderShouldEmitUTF8Identifier: false); - private static int WarmupIterations = 10; + private static int WarmupIterations = 5; private static int TestIterations = 1000; public static void Main(string[] args) @@ -55,10 +55,17 @@ public static class Program if (mode == "quick") { - WarmupIterations = 10; + WarmupIterations = 5; TestIterations = 100; mode = "all"; } + + // Profiler mode: warmup only, then exit (for memory profiler analysis) + if (mode == "profiler") + { + RunProfilerMode(); + return; + } System.Console.WriteLine("╔══════════════════════════════════════════════════════════════════════╗"); System.Console.WriteLine("║ COMPREHENSIVE SERIALIZER BENCHMARK SUITE ║"); @@ -90,6 +97,47 @@ public static class Program System.Console.WriteLine("\n✓ Benchmark complete!"); } + + /// + /// Profiler mode: warmup only, then EXIT immediately. + /// Usage: dotnet run -- profiler + /// + private static void RunProfilerMode() + { + System.Console.WriteLine("╔══════════════════════════════════════════════════════════════════════╗"); + System.Console.WriteLine("║ PROFILER MODE (AcBinary only) ║"); + System.Console.WriteLine("╚══════════════════════════════════════════════════════════════════════╝"); + System.Console.WriteLine($"Build: {BuildConfiguration} | .NET: {Environment.Version}"); + System.Console.WriteLine(); + + // Create medium test data + TestDataFactory.ResetIdCounter(); + var sharedTag = TestDataFactory.CreateTag("SharedTag"); + var sharedUser = TestDataFactory.CreateUser("shareduser"); + var order = TestDataFactory.CreateOrder( + itemCount: 3, + palletsPerItem: 3, + measurementsPerPallet: 3, + pointsPerMeasurement: 4, + sharedTag: sharedTag, + sharedUser: sharedUser); + + var options = AcBinarySerializerOptions.WithoutReferenceHandling(); + + // Warmup (fills caches) + System.Console.WriteLine("Warming up (10 iterations)..."); + for (var i = 0; i < 10; i++) + { + _ = AcBinarySerializer.Serialize(order, options); + } + System.Console.WriteLine("Warmup complete. Caches are now populated."); + System.Console.WriteLine(); + System.Console.WriteLine(">>> ATTACH MEMORY PROFILER NOW <<<"); + System.Console.WriteLine("Press any key to exit..."); + System.Console.ReadKey(intercept: true); + System.Console.WriteLine(); + System.Console.WriteLine("✓ Profiler mode complete. Exiting now."); + } #region Test Data Creation @@ -766,28 +814,49 @@ public static class Program System.Console.WriteLine($"{"Fastest Round-trip",-20} │ {fastestRt.Name,-25} │ {fastestRt.AvgTime,15:F2} ms"); // Overall AcBinary Default vs MessagePack comparison - var msgPackAvgSer = results.Where(r => r.SerializerName == SerializerMessagePack && r.SerializeTimeMs > 0).Average(r => r.SerializeTimeMs); - var msgPackAvgDes = results.Where(r => r.SerializerName == SerializerMessagePack && r.DeserializeTimeMs > 0).Average(r => r.DeserializeTimeMs); - var msgPackAvgRt = results.Where(r => r.SerializerName == SerializerMessagePack && r.RoundTripTimeMs > 0).Average(r => r.RoundTripTimeMs); + var msgPackSerResults = results.Where(r => r.SerializerName == SerializerMessagePack && r.SerializeTimeMs > 0).ToList(); + var msgPackDesResults = results.Where(r => r.SerializerName == SerializerMessagePack && r.DeserializeTimeMs > 0).ToList(); + var msgPackRtResults = results.Where(r => r.SerializerName == SerializerMessagePack && r.RoundTripTimeMs > 0).ToList(); + + var acBinarySerResults = results.Where(r => r.SerializerName == SerializerAcBinaryDefault && r.SerializeTimeMs > 0).ToList(); + var acBinaryDesResults = results.Where(r => r.SerializerName == SerializerAcBinaryDefault && r.DeserializeTimeMs > 0).ToList(); + var acBinaryRtResults = results.Where(r => r.SerializerName == SerializerAcBinaryDefault && r.RoundTripTimeMs > 0).ToList(); + + // Skip comparison if no data available + if (msgPackRtResults.Count == 0 || acBinaryRtResults.Count == 0) + { + System.Console.WriteLine(); + System.Console.WriteLine($"── {SerializerAcBinaryDefault} vs {SerializerMessagePack} (Overall) ──"); + System.Console.WriteLine(" (Comparison requires both serialize and deserialize data)"); + return; + } + + var msgPackAvgSer = msgPackSerResults.Count > 0 ? msgPackSerResults.Average(r => r.SerializeTimeMs) : 0; + var msgPackAvgDes = msgPackDesResults.Average(r => r.DeserializeTimeMs); + var msgPackAvgRt = msgPackRtResults.Average(r => r.RoundTripTimeMs); var msgPackAvgSize = results.Where(r => r.SerializerName == SerializerMessagePack).Average(r => r.SerializedSize); - var acBinaryAvgSer = results.Where(r => r.SerializerName == SerializerAcBinaryDefault && r.SerializeTimeMs > 0).Average(r => r.SerializeTimeMs); - var acBinaryAvgDes = results.Where(r => r.SerializerName == SerializerAcBinaryDefault && r.DeserializeTimeMs > 0).Average(r => r.DeserializeTimeMs); - var acBinaryAvgRt = results.Where(r => r.SerializerName == SerializerAcBinaryDefault && r.RoundTripTimeMs > 0).Average(r => r.RoundTripTimeMs); + var acBinaryAvgSer = acBinarySerResults.Count > 0 ? acBinarySerResults.Average(r => r.SerializeTimeMs) : 0; + var acBinaryAvgDes = acBinaryDesResults.Average(r => r.DeserializeTimeMs); + var acBinaryAvgRt = acBinaryRtResults.Average(r => r.RoundTripTimeMs); var acBinaryAvgSize = results.Where(r => r.SerializerName == SerializerAcBinaryDefault).Average(r => r.SerializedSize); System.Console.WriteLine(); System.Console.WriteLine($"── {SerializerAcBinaryDefault} vs {SerializerMessagePack} (Overall) ──"); - var serPctAll = (acBinaryAvgSer / msgPackAvgSer - 1) * 100; + // Only show serialize comparison if data available + if (msgPackAvgSer > 0 && acBinaryAvgSer > 0) + { + var serPctAll = (acBinaryAvgSer / msgPackAvgSer - 1) * 100; + System.Console.ForegroundColor = serPctAll <= 0 ? ConsoleColor.Green : ConsoleColor.Red; + System.Console.WriteLine($" Serialize: {serPctAll:+0;-0}% ({acBinaryAvgSer:F2} ms vs {msgPackAvgSer:F2} ms)"); + System.Console.ResetColor(); + } + var desPctAll = (acBinaryAvgDes / msgPackAvgDes - 1) * 100; var rtPctAll = (acBinaryAvgRt / msgPackAvgRt - 1) * 100; var sizePctAll = (acBinaryAvgSize / msgPackAvgSize - 1) * 100; - System.Console.ForegroundColor = serPctAll <= 0 ? ConsoleColor.Green : ConsoleColor.Red; - System.Console.WriteLine($" Serialize: {serPctAll:+0;-0}% ({acBinaryAvgSer:F2} ms vs {msgPackAvgSer:F2} ms)"); - System.Console.ResetColor(); - System.Console.ForegroundColor = desPctAll <= 0 ? ConsoleColor.Green : ConsoleColor.Red; System.Console.WriteLine($" Deserialize: {desPctAll:+0;-0}% ({acBinaryAvgDes:F2} ms vs {msgPackAvgDes:F2} ms)"); System.Console.ResetColor(); @@ -806,9 +875,33 @@ public static class Program Directory.CreateDirectory(ResultsDirectory); var timestamp = DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss"); - var fileName = $"Console.FullBenchmark_{BuildConfiguration}_{timestamp}.log"; - var filePath = Path.Combine(ResultsDirectory, fileName); + var baseFileName = $"Console.FullBenchmark_{BuildConfiguration}_{timestamp}"; + var logFilePath = Path.Combine(ResultsDirectory, $"{baseFileName}.log"); + var outputFilePath = Path.Combine(ResultsDirectory, $"{baseFileName}.output"); + // Save binary output to separate .output file + var largeTestData = testDataSets.FirstOrDefault(t => t.Name.StartsWith("Large")); + if (largeTestData != null) + { + var outputSb = new StringBuilder(); + outputSb.AppendLine("╔══════════════════════════════════════════════════════════════════════════════════════════════════════╗"); + outputSb.AppendLine("║ SERIALIZED BINARY OUTPUT ║"); + outputSb.AppendLine($"║ Generated: {DateTime.Now:yyyy-MM-dd HH:mm:ss}".PadRight(100) + "║"); + outputSb.AppendLine("╚══════════════════════════════════════════════════════════════════════════════════════════════════════╝"); + outputSb.AppendLine(); + + outputSb.AppendLine("=== SERIALIZED BYTES: Large (5x5x5x10) - AcBinary (Default) ==="); + var serializedBytes = AcBinarySerializer.Serialize(largeTestData.Order, AcBinarySerializerOptions.Default); + outputSb.AppendLine($"Size: {serializedBytes.Length:N0} bytes"); + outputSb.AppendLine(); + outputSb.AppendLine("Hex dump:"); + outputSb.AppendLine(FormatHexDump(serializedBytes)); + + File.WriteAllText(outputFilePath, outputSb.ToString(), Utf8NoBom); + System.Console.WriteLine($"✓ Binary output saved to: {outputFilePath}"); + } + + // Save benchmark results to .log file var sb = new StringBuilder(); sb.AppendLine("╔══════════════════════════════════════════════════════════════════════════════════════════════════════╗"); sb.AppendLine("║ SERIALIZER BENCHMARK RESULTS ║"); @@ -818,25 +911,12 @@ public static class Program sb.AppendLine("╚══════════════════════════════════════════════════════════════════════════════════════════════════════╝"); sb.AppendLine(); - // Serialized bytes for Large test data (AcBinary Default) - var largeTestData = testDataSets.FirstOrDefault(t => t.Name.StartsWith("Large")); - if (largeTestData != null) - { - sb.AppendLine("=== SERIALIZED BYTES: Large (5x5x5x10) - AcBinary (Default) ==="); - var serializedBytes = AcBinarySerializer.Serialize(largeTestData.Order, AcBinarySerializerOptions.Default); - sb.AppendLine($"Size: {serializedBytes.Length:N0} bytes"); - sb.AppendLine(); - sb.AppendLine("Hex dump:"); - sb.AppendLine(FormatHexDump(serializedBytes)); - sb.AppendLine(); - } - // CSV-like data for easy import sb.AppendLine("=== RAW DATA (CSV) ==="); sb.AppendLine("TestData,Serializer,Size,SerializeMs,DeserializeMs,RoundTripMs"); foreach (var testData in testDataSets) { - var testResults = results.Where(r => r.TestDataName == testData.Name).ToList(); + var testResults = results.Where(r => r.TestDataName == testData.DisplayName).ToList(); foreach (var result in testResults) { sb.AppendLine($"{result.TestDataName},{result.SerializerName},{result.SerializedSize},{result.SerializeTimeMs:F2},{result.DeserializeTimeMs:F2},{result.RoundTripTimeMs:F2}"); @@ -851,12 +931,12 @@ public static class Program foreach (var testData in testDataSets) { - var testResults = results.Where(r => r.TestDataName == testData.Name).OrderBy(r => r.RoundTripTimeMs).ToList(); + var testResults = results.Where(r => r.TestDataName == testData.DisplayName).OrderBy(r => r.RoundTripTimeMs).ToList(); var msgPackResult = testResults.FirstOrDefault(r => r.SerializerName == SerializerMessagePack); var acBinaryResult = testResults.FirstOrDefault(r => r.SerializerName == SerializerAcBinaryDefault); sb.AppendLine(); - sb.AppendLine($"--- {testData.Name} ---"); + sb.AppendLine($"--- {testData.DisplayName} ---"); sb.AppendLine($"{"#",-4} {"Serializer",-26} {"Size",-12} {"Serialize",-14} {"Deserialize",-14} {"Round-trip",-14}"); sb.AppendLine(new string('-', 86)); @@ -890,23 +970,41 @@ public static class Program sb.AppendLine(); sb.AppendLine($"=== {SerializerAcBinaryDefault} vs {SerializerMessagePack} (Overall) ==="); - var msgPackAvgSer = results.Where(r => r.SerializerName == SerializerMessagePack && r.SerializeTimeMs > 0).Average(r => r.SerializeTimeMs); - var msgPackAvgDes = results.Where(r => r.SerializerName == SerializerMessagePack && r.DeserializeTimeMs > 0).Average(r => r.DeserializeTimeMs); - var msgPackAvgRt = results.Where(r => r.SerializerName == SerializerMessagePack && r.RoundTripTimeMs > 0).Average(r => r.RoundTripTimeMs); - var msgPackAvgSize = results.Where(r => r.SerializerName == SerializerMessagePack).Average(r => r.SerializedSize); + var msgPackSerResults2 = results.Where(r => r.SerializerName == SerializerMessagePack && r.SerializeTimeMs > 0).ToList(); + var msgPackDesResults2 = results.Where(r => r.SerializerName == SerializerMessagePack && r.DeserializeTimeMs > 0).ToList(); + var msgPackRtResults2 = results.Where(r => r.SerializerName == SerializerMessagePack && r.RoundTripTimeMs > 0).ToList(); - var acBinaryAvgSer = results.Where(r => r.SerializerName == SerializerAcBinaryDefault && r.SerializeTimeMs > 0).Average(r => r.SerializeTimeMs); - var acBinaryAvgDes = results.Where(r => r.SerializerName == SerializerAcBinaryDefault && r.DeserializeTimeMs > 0).Average(r => r.DeserializeTimeMs); - var acBinaryAvgRt = results.Where(r => r.SerializerName == SerializerAcBinaryDefault && r.RoundTripTimeMs > 0).Average(r => r.RoundTripTimeMs); - var acBinaryAvgSize = results.Where(r => r.SerializerName == SerializerAcBinaryDefault).Average(r => r.SerializedSize); + var acBinarySerResults2 = results.Where(r => r.SerializerName == SerializerAcBinaryDefault && r.SerializeTimeMs > 0).ToList(); + var acBinaryDesResults2 = results.Where(r => r.SerializerName == SerializerAcBinaryDefault && r.DeserializeTimeMs > 0).ToList(); + var acBinaryRtResults2 = results.Where(r => r.SerializerName == SerializerAcBinaryDefault && r.RoundTripTimeMs > 0).ToList(); - sb.AppendLine($" Serialize: {((acBinaryAvgSer / msgPackAvgSer - 1) * 100):+0;-0}% ({acBinaryAvgSer:F2} ms vs {msgPackAvgSer:F2} ms)"); - sb.AppendLine($" Deserialize: {((acBinaryAvgDes / msgPackAvgDes - 1) * 100):+0;-0}% ({acBinaryAvgDes:F2} ms vs {msgPackAvgDes:F2} ms)"); - sb.AppendLine($" Round-trip: {((acBinaryAvgRt / msgPackAvgRt - 1) * 100):+0;-0}% ({acBinaryAvgRt:F2} ms vs {msgPackAvgRt:F2} ms)"); - sb.AppendLine($" Size: {((acBinaryAvgSize / msgPackAvgSize - 1) * 100):+0;-0}% ({acBinaryAvgSize:F0} B vs {msgPackAvgSize:F0} B)"); + if (msgPackSerResults2.Count > 0 && acBinarySerResults2.Count > 0) + { + var msgPackAvgSer2 = msgPackSerResults2.Average(r => r.SerializeTimeMs); + var acBinaryAvgSer2 = acBinarySerResults2.Average(r => r.SerializeTimeMs); + sb.AppendLine($" Serialize: {((acBinaryAvgSer2 / msgPackAvgSer2 - 1) * 100):+0;-0}% ({acBinaryAvgSer2:F2} ms vs {msgPackAvgSer2:F2} ms)"); + } + + if (msgPackDesResults2.Count > 0 && acBinaryDesResults2.Count > 0) + { + var msgPackAvgDes2 = msgPackDesResults2.Average(r => r.DeserializeTimeMs); + var acBinaryAvgDes2 = acBinaryDesResults2.Average(r => r.DeserializeTimeMs); + sb.AppendLine($" Deserialize: {((acBinaryAvgDes2 / msgPackAvgDes2 - 1) * 100):+0;-0}% ({acBinaryAvgDes2:F2} ms vs {msgPackAvgDes2:F2} ms)"); + } + + if (msgPackRtResults2.Count > 0 && acBinaryRtResults2.Count > 0) + { + var msgPackAvgRt2 = msgPackRtResults2.Average(r => r.RoundTripTimeMs); + var acBinaryAvgRt2 = acBinaryRtResults2.Average(r => r.RoundTripTimeMs); + sb.AppendLine($" Round-trip: {((acBinaryAvgRt2 / msgPackAvgRt2 - 1) * 100):+0;-0}% ({acBinaryAvgRt2:F2} ms vs {msgPackAvgRt2:F2} ms)"); + } + + var msgPackAvgSize2 = results.Where(r => r.SerializerName == SerializerMessagePack).Average(r => r.SerializedSize); + var acBinaryAvgSize2 = results.Where(r => r.SerializerName == SerializerAcBinaryDefault).Average(r => r.SerializedSize); + sb.AppendLine($" Size: {((acBinaryAvgSize2 / msgPackAvgSize2 - 1) * 100):+0;-0}% ({acBinaryAvgSize2:F0} B vs {msgPackAvgSize2:F0} B)"); - File.WriteAllText(filePath, sb.ToString(), Utf8NoBom); - System.Console.WriteLine($"\n✓ Results saved to: {filePath}"); + File.WriteAllText(logFilePath, sb.ToString(), Utf8NoBom); + System.Console.WriteLine($"✓ Results saved to: {logFilePath}"); } /// diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs index 3bec675..558abf8 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs @@ -67,6 +67,11 @@ public static partial class AcBinarySerializer private Dictionary? _internedStrings; private List? _internedStringList; + // Single contiguous buffer for all interned string UTF8 bytes (reused across serializations) + private byte[]? _internedStringBuffer; + private int _internedStringBufferPos; + private List? _internedStringLengths; + private Dictionary? _propertyNames; private List? _propertyNameList; private int[]? _propertyIndexBuffer; @@ -125,7 +130,10 @@ public static partial class AcBinarySerializer _propertyNameList?.Clear(); _internedStringList?.Clear(); - _internedStringUtf8?.Clear(); + _internedStringLengths?.Clear(); + + // Reset intern buffer position (no deallocation - buffer is reused!) + _internedStringBufferPos = 0; // Reset cached property indices ResetCachedPropertyIndices(); @@ -170,25 +178,23 @@ public static partial class AcBinarySerializer ArrayPool.Shared.Return(_propertyStateBuffer); _propertyStateBuffer = null; } + + // _internedStringBuffer is a simple byte[] - no pool return needed, GC handles it + _internedStringBuffer = null; } #region String Interning - /// - /// Cached UTF8 bytes for interned strings to avoid re-encoding in FinalizeHeaderSections. - /// - private List? _internedStringUtf8; - /// /// Registers a string for interning. Returns the index of the string. - /// Uses CollectionsMarshal.GetValueRefOrAddDefault for single-operation lookup+add. + /// Uses single contiguous buffer for UTF8 bytes to minimize allocations. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public int RegisterInternedString(string value) { _internedStrings ??= new Dictionary(InitialInternCapacity, StringComparer.Ordinal); _internedStringList ??= new List(InitialInternCapacity); - _internedStringUtf8 ??= new List(InitialInternCapacity); + _internedStringLengths ??= new List(InitialInternCapacity); // Single operation: lookup + conditional add ref var index = ref CollectionsMarshal.GetValueRefOrAddDefault(_internedStrings, value, out var exists); @@ -197,28 +203,57 @@ public static partial class AcBinarySerializer return index; } - // New string - add to lists + // New string - add to list and write UTF8 to buffer index = _internedStringList.Count; _internedStringList.Add(value); - _internedStringUtf8.Add(GetUtf8BytesCached(value)); + + // Calculate UTF8 byte length + var utf8Length = Ascii.IsValid(value) ? value.Length : Utf8NoBom.GetByteCount(value); + + // Ensure intern buffer has capacity + EnsureInternBufferCapacity(utf8Length); + + // Write UTF8 bytes to contiguous buffer + if (Ascii.IsValid(value)) + { + Ascii.FromUtf16(value.AsSpan(), _internedStringBuffer.AsSpan(_internedStringBufferPos, utf8Length), out _); + } + else + { + Utf8NoBom.GetBytes(value.AsSpan(), _internedStringBuffer.AsSpan(_internedStringBufferPos, utf8Length)); + } + + _internedStringLengths.Add(utf8Length); + _internedStringBufferPos += utf8Length; + return index; } /// - /// Get UTF8 bytes for a string, optimized for ASCII strings. + /// Ensures the intern buffer has enough capacity for additional bytes. + /// Initial size is calculated from MaxStringInternLength * InitialInternCapacity. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static byte[] GetUtf8BytesCached(string value) + private void EnsureInternBufferCapacity(int additionalBytes) { - // Fast path for ASCII strings - direct char to byte conversion - if (Ascii.IsValid(value)) + var required = _internedStringBufferPos + additionalBytes; + + if (_internedStringBuffer == null) { - var bytes = new byte[value.Length]; - Ascii.FromUtf16(value.AsSpan(), bytes, out _); - return bytes; + // Initial size: MaxStringInternLength * InitialInternCapacity (e.g., 64 * 32 = 2048) + var initialSize = MaxStringInternLength * InitialInternCapacity; + _internedStringBuffer = new byte[Math.Max(initialSize, required)]; + return; } - // Standard path for multi-byte UTF8 - return Utf8NoBom.GetBytes(value); + + if (required <= _internedStringBuffer.Length) + { + return; + } + + // Grow buffer (double size) + var newSize = Math.Max(_internedStringBuffer.Length * 2, required); + Array.Resize(ref _internedStringBuffer, newSize); } #endregion @@ -989,19 +1024,21 @@ public static partial class AcBinarySerializer /// /// Writes interned strings to the footer (end of stream). - /// No shifting or estimation needed - just append. + /// Uses contiguous buffer - no re-encoding needed. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private void WriteFooterStrings() { WriteVarUInt((uint)_internedStringList!.Count); - // Use cached UTF8 bytes - no re-encoding needed! - for (var i = 0; i < _internedStringUtf8!.Count; i++) + // Write from contiguous buffer using stored lengths + var offset = 0; + for (var i = 0; i < _internedStringLengths!.Count; i++) { - var utf8Bytes = _internedStringUtf8[i]; - WriteVarUInt((uint)utf8Bytes.Length); - WriteBytes(utf8Bytes); + var length = _internedStringLengths[i]; + WriteVarUInt((uint)length); + WriteBytes(_internedStringBuffer.AsSpan(offset, length)); + offset += length; } } diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs index d8238cb..e5b41d7 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs @@ -135,10 +135,13 @@ public sealed class AcBinarySerializerOptions : AcSerializerOptions public static AcBinarySerializerOptions WithMaxDepth(byte maxDepth) => new() { MaxDepth = maxDepth }; /// - /// Creates options without reference handling. + /// Creates options without reference handling (and string interning disabled for speed). /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static AcBinarySerializerOptions WithoutReferenceHandling() => new() { ReferenceHandling = ReferenceHandlingMode.None }; + public static AcBinarySerializerOptions WithoutReferenceHandling() => new() + { + ReferenceHandling = ReferenceHandlingMode.None, + }; /// /// Creates options without metadata (faster but less flexible).