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
This commit is contained in:
parent
6df5c53937
commit
145cc0a493
|
|
@ -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,11 +55,18 @@ 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 ║");
|
||||
System.Console.WriteLine("╚══════════════════════════════════════════════════════════════════════╝");
|
||||
|
|
@ -91,6 +98,47 @@ public static class Program
|
|||
System.Console.WriteLine("\n✓ Benchmark complete!");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Profiler mode: warmup only, then EXIT immediately.
|
||||
/// Usage: dotnet run -- profiler
|
||||
/// </summary>
|
||||
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
|
||||
|
||||
private static List<TestDataSet> CreateTestDataSets()
|
||||
|
|
@ -766,27 +814,48 @@ 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) ──");
|
||||
|
||||
// Only show serialize comparison if data available
|
||||
if (msgPackAvgSer > 0 && acBinaryAvgSer > 0)
|
||||
{
|
||||
var serPctAll = (acBinaryAvgSer / msgPackAvgSer - 1) * 100;
|
||||
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();
|
||||
}
|
||||
|
||||
var desPctAll = (acBinaryAvgDes / msgPackAvgDes - 1) * 100;
|
||||
var rtPctAll = (acBinaryAvgRt / msgPackAvgRt - 1) * 100;
|
||||
var sizePctAll = (acBinaryAvgSize / msgPackAvgSize - 1) * 100;
|
||||
|
||||
System.Console.ForegroundColor = desPctAll <= 0 ? ConsoleColor.Green : ConsoleColor.Red;
|
||||
System.Console.WriteLine($" Deserialize: {desPctAll:+0;-0}% ({acBinaryAvgDes:F2} ms vs {msgPackAvgDes:F2} ms)");
|
||||
|
|
@ -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)");
|
||||
}
|
||||
|
||||
File.WriteAllText(filePath, sb.ToString(), Utf8NoBom);
|
||||
System.Console.WriteLine($"\n✓ Results saved to: {filePath}");
|
||||
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(logFilePath, sb.ToString(), Utf8NoBom);
|
||||
System.Console.WriteLine($"✓ Results saved to: {logFilePath}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -67,6 +67,11 @@ public static partial class AcBinarySerializer
|
|||
private Dictionary<string, int>? _internedStrings;
|
||||
private List<string>? _internedStringList;
|
||||
|
||||
// Single contiguous buffer for all interned string UTF8 bytes (reused across serializations)
|
||||
private byte[]? _internedStringBuffer;
|
||||
private int _internedStringBufferPos;
|
||||
private List<int>? _internedStringLengths;
|
||||
|
||||
private Dictionary<string, int>? _propertyNames;
|
||||
private List<string>? _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<byte>.Shared.Return(_propertyStateBuffer);
|
||||
_propertyStateBuffer = null;
|
||||
}
|
||||
|
||||
// _internedStringBuffer is a simple byte[] - no pool return needed, GC handles it
|
||||
_internedStringBuffer = null;
|
||||
}
|
||||
|
||||
#region String Interning
|
||||
|
||||
/// <summary>
|
||||
/// Cached UTF8 bytes for interned strings to avoid re-encoding in FinalizeHeaderSections.
|
||||
/// </summary>
|
||||
private List<byte[]>? _internedStringUtf8;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public int RegisterInternedString(string value)
|
||||
{
|
||||
_internedStrings ??= new Dictionary<string, int>(InitialInternCapacity, StringComparer.Ordinal);
|
||||
_internedStringList ??= new List<string>(InitialInternCapacity);
|
||||
_internedStringUtf8 ??= new List<byte[]>(InitialInternCapacity);
|
||||
_internedStringLengths ??= new List<int>(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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[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
|
|||
|
||||
/// <summary>
|
||||
/// Writes interned strings to the footer (end of stream).
|
||||
/// No shifting or estimation needed - just append.
|
||||
/// Uses contiguous buffer - no re-encoding needed.
|
||||
/// </summary>
|
||||
[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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -135,10 +135,13 @@ public sealed class AcBinarySerializerOptions : AcSerializerOptions
|
|||
public static AcBinarySerializerOptions WithMaxDepth(byte maxDepth) => new() { MaxDepth = maxDepth };
|
||||
|
||||
/// <summary>
|
||||
/// Creates options without reference handling.
|
||||
/// Creates options without reference handling (and string interning disabled for speed).
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static AcBinarySerializerOptions WithoutReferenceHandling() => new() { ReferenceHandling = ReferenceHandlingMode.None };
|
||||
public static AcBinarySerializerOptions WithoutReferenceHandling() => new()
|
||||
{
|
||||
ReferenceHandling = ReferenceHandlingMode.None,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Creates options without metadata (faster but less flexible).
|
||||
|
|
|
|||
Loading…
Reference in New Issue