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 readonly UTF8Encoding Utf8NoBom = new(encoderShouldEmitUTF8Identifier: false);
|
||||||
|
|
||||||
private static int WarmupIterations = 10;
|
private static int WarmupIterations = 5;
|
||||||
private static int TestIterations = 1000;
|
private static int TestIterations = 1000;
|
||||||
|
|
||||||
public static void Main(string[] args)
|
public static void Main(string[] args)
|
||||||
|
|
@ -55,11 +55,18 @@ public static class Program
|
||||||
|
|
||||||
if (mode == "quick")
|
if (mode == "quick")
|
||||||
{
|
{
|
||||||
WarmupIterations = 10;
|
WarmupIterations = 5;
|
||||||
TestIterations = 100;
|
TestIterations = 100;
|
||||||
mode = "all";
|
mode = "all";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Profiler mode: warmup only, then exit (for memory profiler analysis)
|
||||||
|
if (mode == "profiler")
|
||||||
|
{
|
||||||
|
RunProfilerMode();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
System.Console.WriteLine("╔══════════════════════════════════════════════════════════════════════╗");
|
System.Console.WriteLine("╔══════════════════════════════════════════════════════════════════════╗");
|
||||||
System.Console.WriteLine("║ COMPREHENSIVE SERIALIZER BENCHMARK SUITE ║");
|
System.Console.WriteLine("║ COMPREHENSIVE SERIALIZER BENCHMARK SUITE ║");
|
||||||
System.Console.WriteLine("╚══════════════════════════════════════════════════════════════════════╝");
|
System.Console.WriteLine("╚══════════════════════════════════════════════════════════════════════╝");
|
||||||
|
|
@ -91,6 +98,47 @@ public static class Program
|
||||||
System.Console.WriteLine("\n✓ Benchmark complete!");
|
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
|
#region Test Data Creation
|
||||||
|
|
||||||
private static List<TestDataSet> CreateTestDataSets()
|
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");
|
System.Console.WriteLine($"{"Fastest Round-trip",-20} │ {fastestRt.Name,-25} │ {fastestRt.AvgTime,15:F2} ms");
|
||||||
|
|
||||||
// Overall AcBinary Default vs MessagePack comparison
|
// Overall AcBinary Default vs MessagePack comparison
|
||||||
var msgPackAvgSer = results.Where(r => r.SerializerName == SerializerMessagePack && r.SerializeTimeMs > 0).Average(r => r.SerializeTimeMs);
|
var msgPackSerResults = results.Where(r => r.SerializerName == SerializerMessagePack && r.SerializeTimeMs > 0).ToList();
|
||||||
var msgPackAvgDes = results.Where(r => r.SerializerName == SerializerMessagePack && r.DeserializeTimeMs > 0).Average(r => r.DeserializeTimeMs);
|
var msgPackDesResults = results.Where(r => r.SerializerName == SerializerMessagePack && r.DeserializeTimeMs > 0).ToList();
|
||||||
var msgPackAvgRt = results.Where(r => r.SerializerName == SerializerMessagePack && r.RoundTripTimeMs > 0).Average(r => r.RoundTripTimeMs);
|
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 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 acBinaryAvgSer = acBinarySerResults.Count > 0 ? acBinarySerResults.Average(r => r.SerializeTimeMs) : 0;
|
||||||
var acBinaryAvgDes = results.Where(r => r.SerializerName == SerializerAcBinaryDefault && r.DeserializeTimeMs > 0).Average(r => r.DeserializeTimeMs);
|
var acBinaryAvgDes = acBinaryDesResults.Average(r => r.DeserializeTimeMs);
|
||||||
var acBinaryAvgRt = results.Where(r => r.SerializerName == SerializerAcBinaryDefault && r.RoundTripTimeMs > 0).Average(r => r.RoundTripTimeMs);
|
var acBinaryAvgRt = acBinaryRtResults.Average(r => r.RoundTripTimeMs);
|
||||||
var acBinaryAvgSize = results.Where(r => r.SerializerName == SerializerAcBinaryDefault).Average(r => r.SerializedSize);
|
var acBinaryAvgSize = results.Where(r => r.SerializerName == SerializerAcBinaryDefault).Average(r => r.SerializedSize);
|
||||||
|
|
||||||
System.Console.WriteLine();
|
System.Console.WriteLine();
|
||||||
System.Console.WriteLine($"── {SerializerAcBinaryDefault} vs {SerializerMessagePack} (Overall) ──");
|
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 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.ForegroundColor = serPctAll <= 0 ? ConsoleColor.Green : ConsoleColor.Red;
|
||||||
System.Console.WriteLine($" Serialize: {serPctAll:+0;-0}% ({acBinaryAvgSer:F2} ms vs {msgPackAvgSer:F2} ms)");
|
System.Console.WriteLine($" Serialize: {serPctAll:+0;-0}% ({acBinaryAvgSer:F2} ms vs {msgPackAvgSer:F2} ms)");
|
||||||
System.Console.ResetColor();
|
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.ForegroundColor = desPctAll <= 0 ? ConsoleColor.Green : ConsoleColor.Red;
|
||||||
System.Console.WriteLine($" Deserialize: {desPctAll:+0;-0}% ({acBinaryAvgDes:F2} ms vs {msgPackAvgDes:F2} ms)");
|
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);
|
Directory.CreateDirectory(ResultsDirectory);
|
||||||
|
|
||||||
var timestamp = DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss");
|
var timestamp = DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss");
|
||||||
var fileName = $"Console.FullBenchmark_{BuildConfiguration}_{timestamp}.log";
|
var baseFileName = $"Console.FullBenchmark_{BuildConfiguration}_{timestamp}";
|
||||||
var filePath = Path.Combine(ResultsDirectory, fileName);
|
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();
|
var sb = new StringBuilder();
|
||||||
sb.AppendLine("╔══════════════════════════════════════════════════════════════════════════════════════════════════════╗");
|
sb.AppendLine("╔══════════════════════════════════════════════════════════════════════════════════════════════════════╗");
|
||||||
sb.AppendLine("║ SERIALIZER BENCHMARK RESULTS ║");
|
sb.AppendLine("║ SERIALIZER BENCHMARK RESULTS ║");
|
||||||
|
|
@ -818,25 +911,12 @@ public static class Program
|
||||||
sb.AppendLine("╚══════════════════════════════════════════════════════════════════════════════════════════════════════╝");
|
sb.AppendLine("╚══════════════════════════════════════════════════════════════════════════════════════════════════════╝");
|
||||||
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
|
// CSV-like data for easy import
|
||||||
sb.AppendLine("=== RAW DATA (CSV) ===");
|
sb.AppendLine("=== RAW DATA (CSV) ===");
|
||||||
sb.AppendLine("TestData,Serializer,Size,SerializeMs,DeserializeMs,RoundTripMs");
|
sb.AppendLine("TestData,Serializer,Size,SerializeMs,DeserializeMs,RoundTripMs");
|
||||||
foreach (var testData in testDataSets)
|
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)
|
foreach (var result in testResults)
|
||||||
{
|
{
|
||||||
sb.AppendLine($"{result.TestDataName},{result.SerializerName},{result.SerializedSize},{result.SerializeTimeMs:F2},{result.DeserializeTimeMs:F2},{result.RoundTripTimeMs:F2}");
|
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)
|
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 msgPackResult = testResults.FirstOrDefault(r => r.SerializerName == SerializerMessagePack);
|
||||||
var acBinaryResult = testResults.FirstOrDefault(r => r.SerializerName == SerializerAcBinaryDefault);
|
var acBinaryResult = testResults.FirstOrDefault(r => r.SerializerName == SerializerAcBinaryDefault);
|
||||||
|
|
||||||
sb.AppendLine();
|
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($"{"#",-4} {"Serializer",-26} {"Size",-12} {"Serialize",-14} {"Deserialize",-14} {"Round-trip",-14}");
|
||||||
sb.AppendLine(new string('-', 86));
|
sb.AppendLine(new string('-', 86));
|
||||||
|
|
||||||
|
|
@ -890,23 +970,41 @@ public static class Program
|
||||||
sb.AppendLine();
|
sb.AppendLine();
|
||||||
sb.AppendLine($"=== {SerializerAcBinaryDefault} vs {SerializerMessagePack} (Overall) ===");
|
sb.AppendLine($"=== {SerializerAcBinaryDefault} vs {SerializerMessagePack} (Overall) ===");
|
||||||
|
|
||||||
var msgPackAvgSer = results.Where(r => r.SerializerName == SerializerMessagePack && r.SerializeTimeMs > 0).Average(r => r.SerializeTimeMs);
|
var msgPackSerResults2 = results.Where(r => r.SerializerName == SerializerMessagePack && r.SerializeTimeMs > 0).ToList();
|
||||||
var msgPackAvgDes = results.Where(r => r.SerializerName == SerializerMessagePack && r.DeserializeTimeMs > 0).Average(r => r.DeserializeTimeMs);
|
var msgPackDesResults2 = results.Where(r => r.SerializerName == SerializerMessagePack && r.DeserializeTimeMs > 0).ToList();
|
||||||
var msgPackAvgRt = results.Where(r => r.SerializerName == SerializerMessagePack && r.RoundTripTimeMs > 0).Average(r => r.RoundTripTimeMs);
|
var msgPackRtResults2 = results.Where(r => r.SerializerName == SerializerMessagePack && r.RoundTripTimeMs > 0).ToList();
|
||||||
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 acBinarySerResults2 = results.Where(r => r.SerializerName == SerializerAcBinaryDefault && r.SerializeTimeMs > 0).ToList();
|
||||||
var acBinaryAvgDes = results.Where(r => r.SerializerName == SerializerAcBinaryDefault && r.DeserializeTimeMs > 0).Average(r => r.DeserializeTimeMs);
|
var acBinaryDesResults2 = results.Where(r => r.SerializerName == SerializerAcBinaryDefault && r.DeserializeTimeMs > 0).ToList();
|
||||||
var acBinaryAvgRt = results.Where(r => r.SerializerName == SerializerAcBinaryDefault && r.RoundTripTimeMs > 0).Average(r => r.RoundTripTimeMs);
|
var acBinaryRtResults2 = results.Where(r => r.SerializerName == SerializerAcBinaryDefault && r.RoundTripTimeMs > 0).ToList();
|
||||||
var acBinaryAvgSize = results.Where(r => r.SerializerName == SerializerAcBinaryDefault).Average(r => r.SerializedSize);
|
|
||||||
|
|
||||||
sb.AppendLine($" Serialize: {((acBinaryAvgSer / msgPackAvgSer - 1) * 100):+0;-0}% ({acBinaryAvgSer:F2} ms vs {msgPackAvgSer:F2} ms)");
|
if (msgPackSerResults2.Count > 0 && acBinarySerResults2.Count > 0)
|
||||||
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)");
|
var msgPackAvgSer2 = msgPackSerResults2.Average(r => r.SerializeTimeMs);
|
||||||
sb.AppendLine($" Size: {((acBinaryAvgSize / msgPackAvgSize - 1) * 100):+0;-0}% ({acBinaryAvgSize:F0} B vs {msgPackAvgSize:F0} B)");
|
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);
|
if (msgPackDesResults2.Count > 0 && acBinaryDesResults2.Count > 0)
|
||||||
System.Console.WriteLine($"\n✓ Results saved to: {filePath}");
|
{
|
||||||
|
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>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,11 @@ public static partial class AcBinarySerializer
|
||||||
private Dictionary<string, int>? _internedStrings;
|
private Dictionary<string, int>? _internedStrings;
|
||||||
private List<string>? _internedStringList;
|
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 Dictionary<string, int>? _propertyNames;
|
||||||
private List<string>? _propertyNameList;
|
private List<string>? _propertyNameList;
|
||||||
private int[]? _propertyIndexBuffer;
|
private int[]? _propertyIndexBuffer;
|
||||||
|
|
@ -125,7 +130,10 @@ public static partial class AcBinarySerializer
|
||||||
|
|
||||||
_propertyNameList?.Clear();
|
_propertyNameList?.Clear();
|
||||||
_internedStringList?.Clear();
|
_internedStringList?.Clear();
|
||||||
_internedStringUtf8?.Clear();
|
_internedStringLengths?.Clear();
|
||||||
|
|
||||||
|
// Reset intern buffer position (no deallocation - buffer is reused!)
|
||||||
|
_internedStringBufferPos = 0;
|
||||||
|
|
||||||
// Reset cached property indices
|
// Reset cached property indices
|
||||||
ResetCachedPropertyIndices();
|
ResetCachedPropertyIndices();
|
||||||
|
|
@ -170,25 +178,23 @@ public static partial class AcBinarySerializer
|
||||||
ArrayPool<byte>.Shared.Return(_propertyStateBuffer);
|
ArrayPool<byte>.Shared.Return(_propertyStateBuffer);
|
||||||
_propertyStateBuffer = null;
|
_propertyStateBuffer = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// _internedStringBuffer is a simple byte[] - no pool return needed, GC handles it
|
||||||
|
_internedStringBuffer = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
#region String Interning
|
#region String Interning
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Cached UTF8 bytes for interned strings to avoid re-encoding in FinalizeHeaderSections.
|
|
||||||
/// </summary>
|
|
||||||
private List<byte[]>? _internedStringUtf8;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Registers a string for interning. Returns the index of the string.
|
/// 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>
|
/// </summary>
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public int RegisterInternedString(string value)
|
public int RegisterInternedString(string value)
|
||||||
{
|
{
|
||||||
_internedStrings ??= new Dictionary<string, int>(InitialInternCapacity, StringComparer.Ordinal);
|
_internedStrings ??= new Dictionary<string, int>(InitialInternCapacity, StringComparer.Ordinal);
|
||||||
_internedStringList ??= new List<string>(InitialInternCapacity);
|
_internedStringList ??= new List<string>(InitialInternCapacity);
|
||||||
_internedStringUtf8 ??= new List<byte[]>(InitialInternCapacity);
|
_internedStringLengths ??= new List<int>(InitialInternCapacity);
|
||||||
|
|
||||||
// Single operation: lookup + conditional add
|
// Single operation: lookup + conditional add
|
||||||
ref var index = ref CollectionsMarshal.GetValueRefOrAddDefault(_internedStrings, value, out var exists);
|
ref var index = ref CollectionsMarshal.GetValueRefOrAddDefault(_internedStrings, value, out var exists);
|
||||||
|
|
@ -197,28 +203,57 @@ public static partial class AcBinarySerializer
|
||||||
return index;
|
return index;
|
||||||
}
|
}
|
||||||
|
|
||||||
// New string - add to lists
|
// New string - add to list and write UTF8 to buffer
|
||||||
index = _internedStringList.Count;
|
index = _internedStringList.Count;
|
||||||
_internedStringList.Add(value);
|
_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;
|
return index;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
private static byte[] GetUtf8BytesCached(string value)
|
private void EnsureInternBufferCapacity(int additionalBytes)
|
||||||
{
|
{
|
||||||
// Fast path for ASCII strings - direct char to byte conversion
|
var required = _internedStringBufferPos + additionalBytes;
|
||||||
if (Ascii.IsValid(value))
|
|
||||||
|
if (_internedStringBuffer == null)
|
||||||
{
|
{
|
||||||
var bytes = new byte[value.Length];
|
// Initial size: MaxStringInternLength * InitialInternCapacity (e.g., 64 * 32 = 2048)
|
||||||
Ascii.FromUtf16(value.AsSpan(), bytes, out _);
|
var initialSize = MaxStringInternLength * InitialInternCapacity;
|
||||||
return bytes;
|
_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
|
#endregion
|
||||||
|
|
@ -989,19 +1024,21 @@ public static partial class AcBinarySerializer
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Writes interned strings to the footer (end of stream).
|
/// Writes interned strings to the footer (end of stream).
|
||||||
/// No shifting or estimation needed - just append.
|
/// Uses contiguous buffer - no re-encoding needed.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
private void WriteFooterStrings()
|
private void WriteFooterStrings()
|
||||||
{
|
{
|
||||||
WriteVarUInt((uint)_internedStringList!.Count);
|
WriteVarUInt((uint)_internedStringList!.Count);
|
||||||
|
|
||||||
// Use cached UTF8 bytes - no re-encoding needed!
|
// Write from contiguous buffer using stored lengths
|
||||||
for (var i = 0; i < _internedStringUtf8!.Count; i++)
|
var offset = 0;
|
||||||
|
for (var i = 0; i < _internedStringLengths!.Count; i++)
|
||||||
{
|
{
|
||||||
var utf8Bytes = _internedStringUtf8[i];
|
var length = _internedStringLengths[i];
|
||||||
WriteVarUInt((uint)utf8Bytes.Length);
|
WriteVarUInt((uint)length);
|
||||||
WriteBytes(utf8Bytes);
|
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 };
|
public static AcBinarySerializerOptions WithMaxDepth(byte maxDepth) => new() { MaxDepth = maxDepth };
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates options without reference handling.
|
/// Creates options without reference handling (and string interning disabled for speed).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public static AcBinarySerializerOptions WithoutReferenceHandling() => new() { ReferenceHandling = ReferenceHandlingMode.None };
|
public static AcBinarySerializerOptions WithoutReferenceHandling() => new()
|
||||||
|
{
|
||||||
|
ReferenceHandling = ReferenceHandlingMode.None,
|
||||||
|
};
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates options without metadata (faster but less flexible).
|
/// Creates options without metadata (faster but less flexible).
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue