[LOADED_DOCS: 2 files, no new loads]
Refactor output, allocation, and summary logic in Program - Switched if/else and range checks to C# switch expressions for clarity. - Improved console progress display with cleaner line updates. - Added Thread.Sleep after JIT pre-warmup for stable benchmarking. - Enhanced allocation measurement for serializer/deserializer setup. - Made options and summary output conditional and more consistent. - Standardized string outputs and comparison headers. - Improved comments, XML docs, and code style for maintainability. - No changes to core algorithms; all changes are quality-of-life and output improvements.
This commit is contained in:
parent
969fa550b5
commit
46b26b7238
|
|
@ -109,7 +109,7 @@ public static class Program
|
||||||
// - AOT mode (NativeAOT publish): no dynamic compilation happens; the sleep is pure noise.
|
// - AOT mode (NativeAOT publish): no dynamic compilation happens; the sleep is pure noise.
|
||||||
// 250ms (vs the historical 3000ms) is sufficient for a few-method working set under .NET 9's
|
// 250ms (vs the historical 3000ms) is sufficient for a few-method working set under .NET 9's
|
||||||
// tiered JIT — empirically the queue drains in <100ms for the bench's hot path.
|
// tiered JIT — empirically the queue drains in <100ms for the bench's hot path.
|
||||||
private static int JitSleep => System.Runtime.CompilerServices.RuntimeFeature.IsDynamicCodeCompiled ? 250 : 0;
|
private static int JitSleep => RuntimeFeature.IsDynamicCodeCompiled ? 250 : 0;
|
||||||
|
|
||||||
// OptionsPreset values are passed per-instance (constructor argument), not constants —
|
// OptionsPreset values are passed per-instance (constructor argument), not constants —
|
||||||
// each CreateSerializers call line specifies its own preset name (e.g. "FastMode", "NoIntern").
|
// each CreateSerializers call line specifies its own preset name (e.g. "FastMode", "NoIntern").
|
||||||
|
|
@ -161,6 +161,7 @@ public static class Program
|
||||||
// tagged type has the feature enabled, false = at least one type opted out via
|
// tagged type has the feature enabled, false = at least one type opted out via
|
||||||
// [AcBinarySerializable(enablePropertyFilterFeature: false)] → SGen-emit + Runtime hot-loop both gate).
|
// [AcBinarySerializable(enablePropertyFilterFeature: false)] → SGen-emit + Runtime hot-loop both gate).
|
||||||
var propFilterOpt = options.PropertyFilter == null ? "None" : "Set";
|
var propFilterOpt = options.PropertyFilter == null ? "None" : "Set";
|
||||||
|
|
||||||
return $"WireMode={options.WireMode}, " +
|
return $"WireMode={options.WireMode}, " +
|
||||||
$"RefHandling={options.ReferenceHandling}(opt) | {_attrFlags.refHandling} (attr), " +
|
$"RefHandling={options.ReferenceHandling}(opt) | {_attrFlags.refHandling} (attr), " +
|
||||||
$"Interning={options.UseStringInterning}(opt) | {_attrFlags.internString} (attr), " +
|
$"Interning={options.UseStringInterning}(opt) | {_attrFlags.internString} (attr), " +
|
||||||
|
|
@ -196,6 +197,7 @@ public static class Program
|
||||||
/// — only its sample noise grows). Symmetric with the already-per-op <c>*AllocBytesPerOp</c> fields.
|
/// — only its sample noise grows). Symmetric with the already-per-op <c>*AllocBytesPerOp</c> fields.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Converts a total-time (in ms across <paramref name="iterations"/>) into per-operation microseconds.
|
/// Converts a total-time (in ms across <paramref name="iterations"/>) into per-operation microseconds.
|
||||||
/// Per-op µs is the iter-independent unit: 1000 iter and 50000 iter of the same operation should
|
/// Per-op µs is the iter-independent unit: 1000 iter and 50000 iter of the same operation should
|
||||||
|
|
@ -231,10 +233,7 @@ public static class Program
|
||||||
/// geo/median variants — a cell where AcBinary or MemPack is missing is dropped from all stats.
|
/// geo/median variants — a cell where AcBinary or MemPack is missing is dropped from all stats.
|
||||||
/// Returns null when no paired cell has a valid value.
|
/// Returns null when no paired cell has a valid value.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static OverallStats? ComputeOverallStats(
|
private static OverallStats? ComputeOverallStats(List<BenchmarkResult> acResults, List<BenchmarkResult> mpResults, Func<BenchmarkResult, double> getValue)
|
||||||
List<BenchmarkResult> acResults,
|
|
||||||
List<BenchmarkResult> mpResults,
|
|
||||||
Func<BenchmarkResult, double> getValue)
|
|
||||||
{
|
{
|
||||||
if (acResults.Count == 0 || mpResults.Count == 0) return null;
|
if (acResults.Count == 0 || mpResults.Count == 0) return null;
|
||||||
|
|
||||||
|
|
@ -283,9 +282,11 @@ public static class Program
|
||||||
// No range data (single-sample fast path) — surface as bare median, identical to the prior format.
|
// No range data (single-sample fast path) — surface as bare median, identical to the prior format.
|
||||||
if (minMs <= 0 && maxMs <= 0) return med.ToString("F2", inv);
|
if (minMs <= 0 && maxMs <= 0) return med.ToString("F2", inv);
|
||||||
if (minMs >= medianMs && maxMs <= medianMs) return med.ToString("F2", inv);
|
if (minMs >= medianMs && maxMs <= medianMs) return med.ToString("F2", inv);
|
||||||
|
|
||||||
var min = ToPerOpMicros(minMs, iterations);
|
var min = ToPerOpMicros(minMs, iterations);
|
||||||
var max = ToPerOpMicros(maxMs, iterations);
|
var max = ToPerOpMicros(maxMs, iterations);
|
||||||
var range = $"{med.ToString("F2", inv)} ({min.ToString("F2", inv)}..{max.ToString("F2", inv)})";
|
var range = $"{med.ToString("F2", inv)} ({min.ToString("F2", inv)}..{max.ToString("F2", inv)})";
|
||||||
|
|
||||||
// CV (coefficient of variation = stddev / mean) — flag rows above the unstable threshold so a
|
// CV (coefficient of variation = stddev / mean) — flag rows above the unstable threshold so a
|
||||||
// small inter-engine delta on a high-CV row is easy to discount as noise.
|
// small inter-engine delta on a high-CV row is easy to discount as noise.
|
||||||
if (medianMs > 0 && stdDevMs > 0)
|
if (medianMs > 0 && stdDevMs > 0)
|
||||||
|
|
@ -297,6 +298,7 @@ public static class Program
|
||||||
return $"{range} ⚠️{cvPct}%";
|
return $"{range} ⚠️{cvPct}%";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return range;
|
return range;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -323,6 +325,7 @@ public static class Program
|
||||||
{
|
{
|
||||||
if (!TryParseCliArgs(args, out var layer, out var opMode, out var serializerMode))
|
if (!TryParseCliArgs(args, out var layer, out var opMode, out var serializerMode))
|
||||||
return; // profiler mode (already ran) or invalid args
|
return; // profiler mode (already ran) or invalid args
|
||||||
|
|
||||||
RunBenchmark(layer, opMode, serializerMode);
|
RunBenchmark(layer, opMode, serializerMode);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -476,6 +479,7 @@ public static class Program
|
||||||
if (BenchmarkSamples > 1) // skip in DEBUG (single-sample fast iteration)
|
if (BenchmarkSamples > 1) // skip in DEBUG (single-sample fast iteration)
|
||||||
{
|
{
|
||||||
System.Console.WriteLine($"Global JIT pre-warmup ({testDataSets.Count} cells × all serializers, light pass)...");
|
System.Console.WriteLine($"Global JIT pre-warmup ({testDataSets.Count} cells × all serializers, light pass)...");
|
||||||
|
|
||||||
foreach (var testData in testDataSets)
|
foreach (var testData in testDataSets)
|
||||||
{
|
{
|
||||||
var preSerializers = CreateSerializers(testData, serializerMode);
|
var preSerializers = CreateSerializers(testData, serializerMode);
|
||||||
|
|
@ -494,6 +498,7 @@ public static class Program
|
||||||
foreach (var s in preSerializers) (s as IDisposable)?.Dispose();
|
foreach (var s in preSerializers) (s as IDisposable)?.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Let background tiered-JIT compilation drain before we begin measuring.
|
// Let background tiered-JIT compilation drain before we begin measuring.
|
||||||
if (JitSleep > 0) Thread.Sleep(JitSleep);
|
if (JitSleep > 0) Thread.Sleep(JitSleep);
|
||||||
System.Console.WriteLine("✓ Global pre-warmup complete.\n");
|
System.Console.WriteLine("✓ Global pre-warmup complete.\n");
|
||||||
|
|
@ -960,6 +965,7 @@ public static class Program
|
||||||
GC.Collect();
|
GC.Collect();
|
||||||
GC.WaitForPendingFinalizers();
|
GC.WaitForPendingFinalizers();
|
||||||
GC.Collect();
|
GC.Collect();
|
||||||
|
|
||||||
var pilotSw = Stopwatch.StartNew();
|
var pilotSw = Stopwatch.StartNew();
|
||||||
RunWithProgress(action, iterations, progressLabel, samples + 1, sampleIndex: 0);
|
RunWithProgress(action, iterations, progressLabel, samples + 1, sampleIndex: 0);
|
||||||
pilotSw.Stop();
|
pilotSw.Stop();
|
||||||
|
|
@ -986,6 +992,7 @@ public static class Program
|
||||||
var maxMs = double.MinValue;
|
var maxMs = double.MinValue;
|
||||||
var sum = 0.0;
|
var sum = 0.0;
|
||||||
var sumSq = 0.0;
|
var sumSq = 0.0;
|
||||||
|
|
||||||
for (var i = 0; i < times.Length; i++)
|
for (var i = 0; i < times.Length; i++)
|
||||||
{
|
{
|
||||||
var t = times[i];
|
var t = times[i];
|
||||||
|
|
@ -1005,6 +1012,7 @@ public static class Program
|
||||||
// Median: middle value for odd sample counts, average of two middles for even counts.
|
// Median: middle value for odd sample counts, average of two middles for even counts.
|
||||||
var medianMs = samples % 2 == 1 ? times[samples / 2] : (times[samples / 2 - 1] + times[samples / 2]) / 2.0;
|
var medianMs = samples % 2 == 1 ? times[samples / 2] : (times[samples / 2 - 1] + times[samples / 2]) / 2.0;
|
||||||
EndProgress(progressLabel, medianMs);
|
EndProgress(progressLabel, medianMs);
|
||||||
|
|
||||||
return (medianMs, minMs, maxMs, stdDevMs);
|
return (medianMs, minMs, maxMs, stdDevMs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1039,9 +1047,12 @@ public static class Program
|
||||||
// Round UP to nearest 1000 — keeps numbers human-readable in the markdown output.
|
// Round UP to nearest 1000 — keeps numbers human-readable in the markdown output.
|
||||||
var rounded = ((raw + 999) / 1000) * 1000;
|
var rounded = ((raw + 999) / 1000) * 1000;
|
||||||
|
|
||||||
if (rounded < 1000) return 1000;
|
return rounded switch
|
||||||
if (rounded > 200_000) return 200_000;
|
{
|
||||||
return rounded;
|
< 1000 => 1000,
|
||||||
|
> 200_000 => 200_000,
|
||||||
|
_ => rounded
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -1122,10 +1133,13 @@ public static class Program
|
||||||
var line = samples > 1
|
var line = samples > 1
|
||||||
? $" > {label} sample {sampleIndex + 1}/{samples} {pct,3}% ({i + 1}/{iterations})"
|
? $" > {label} sample {sampleIndex + 1}/{samples} {pct,3}% ({i + 1}/{iterations})"
|
||||||
: $" > {label} {pct,3}% ({i + 1}/{iterations})";
|
: $" > {label} {pct,3}% ({i + 1}/{iterations})";
|
||||||
|
|
||||||
System.Console.Write('\r');
|
System.Console.Write('\r');
|
||||||
System.Console.Write(line);
|
System.Console.Write(line);
|
||||||
|
|
||||||
if (line.Length < _progressLastLineLen)
|
if (line.Length < _progressLastLineLen)
|
||||||
System.Console.Write(new string(' ', _progressLastLineLen - line.Length));
|
System.Console.Write(new string(' ', _progressLastLineLen - line.Length));
|
||||||
|
|
||||||
_progressLastLineLen = line.Length;
|
_progressLastLineLen = line.Length;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1139,10 +1153,13 @@ public static class Program
|
||||||
{
|
{
|
||||||
if (label is null) return;
|
if (label is null) return;
|
||||||
var done = $" > {label} done in {elapsedMs,7:F1} ms";
|
var done = $" > {label} done in {elapsedMs,7:F1} ms";
|
||||||
|
|
||||||
System.Console.Write('\r');
|
System.Console.Write('\r');
|
||||||
System.Console.Write(done);
|
System.Console.Write(done);
|
||||||
|
|
||||||
if (done.Length < _progressLastLineLen)
|
if (done.Length < _progressLastLineLen)
|
||||||
System.Console.Write(new string(' ', _progressLastLineLen - done.Length));
|
System.Console.Write(new string(' ', _progressLastLineLen - done.Length));
|
||||||
|
|
||||||
System.Console.WriteLine();
|
System.Console.WriteLine();
|
||||||
_progressLastLineLen = 0;
|
_progressLastLineLen = 0;
|
||||||
}
|
}
|
||||||
|
|
@ -1301,13 +1318,17 @@ public static class Program
|
||||||
private static string GetCurrentCharsetName()
|
private static string GetCurrentCharsetName()
|
||||||
{
|
{
|
||||||
var s = BenchmarkTestDataProvider.LongStringSuffix;
|
var s = BenchmarkTestDataProvider.LongStringSuffix;
|
||||||
if (s == CharsetSuffixes.Latin1FixAscii) return "Latin1FixAscii";
|
|
||||||
if (s == CharsetSuffixes.Latin1Short) return "Latin1Short";
|
return s switch
|
||||||
if (s == CharsetSuffixes.Latin1Long) return "Latin1Long";
|
{
|
||||||
if (s == CharsetSuffixes.CjkBmp) return "CjkBmp";
|
CharsetSuffixes.Latin1FixAscii => "Latin1FixAscii",
|
||||||
if (s == CharsetSuffixes.Cyrillic) return "Cyrillic";
|
CharsetSuffixes.Latin1Short => "Latin1Short",
|
||||||
if (s == CharsetSuffixes.Mixed) return "Mixed";
|
CharsetSuffixes.Latin1Long => "Latin1Long",
|
||||||
return "Custom";
|
CharsetSuffixes.CjkBmp => "CjkBmp",
|
||||||
|
CharsetSuffixes.Cyrillic => "Cyrillic",
|
||||||
|
CharsetSuffixes.Mixed => "Mixed",
|
||||||
|
_ => "Custom"
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ShowCharsetSettingsMenu()
|
private static void ShowCharsetSettingsMenu()
|
||||||
|
|
@ -1424,9 +1445,12 @@ public static class Program
|
||||||
private static int PromptInt(string name, int currentValue, int min)
|
private static int PromptInt(string name, int currentValue, int min)
|
||||||
{
|
{
|
||||||
System.Console.Write($" {name} [{currentValue}]: ");
|
System.Console.Write($" {name} [{currentValue}]: ");
|
||||||
|
|
||||||
var input = System.Console.ReadLine()?.Trim() ?? "";
|
var input = System.Console.ReadLine()?.Trim() ?? "";
|
||||||
if (input.Length == 0) return currentValue;
|
if (input.Length == 0) return currentValue;
|
||||||
|
|
||||||
if (int.TryParse(input, out var newValue) && newValue >= min) return newValue;
|
if (int.TryParse(input, out var newValue) && newValue >= min) return newValue;
|
||||||
|
|
||||||
System.Console.WriteLine($" ! Invalid value (need int ≥ {min}) — keeping {currentValue}");
|
System.Console.WriteLine($" ! Invalid value (need int ≥ {min}) — keeping {currentValue}");
|
||||||
return currentValue;
|
return currentValue;
|
||||||
}
|
}
|
||||||
|
|
@ -1838,15 +1862,18 @@ public static class Program
|
||||||
// pipe-pair (server + client) + connect handshake + writer-side PipeWriter wrapper.
|
// pipe-pair (server + client) + connect handshake + writer-side PipeWriter wrapper.
|
||||||
GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect();
|
GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect();
|
||||||
var beforeSer = GC.GetAllocatedBytesForCurrentThread();
|
var beforeSer = GC.GetAllocatedBytesForCurrentThread();
|
||||||
|
|
||||||
_pipeServer = new NamedPipeServerStream(pipeName, PipeDirection.In, 1, PipeTransmissionMode.Byte,
|
_pipeServer = new NamedPipeServerStream(pipeName, PipeDirection.In, 1, PipeTransmissionMode.Byte,
|
||||||
System.IO.Pipes.PipeOptions.Asynchronous,
|
System.IO.Pipes.PipeOptions.Asynchronous,
|
||||||
inBufferSize: _options.BufferWriterChunkSize,
|
inBufferSize: _options.BufferWriterChunkSize,
|
||||||
outBufferSize: _options.BufferWriterChunkSize);
|
outBufferSize: _options.BufferWriterChunkSize);
|
||||||
|
|
||||||
_pipeClient = new NamedPipeClientStream(".", pipeName, PipeDirection.Out, System.IO.Pipes.PipeOptions.Asynchronous);
|
_pipeClient = new NamedPipeClientStream(".", pipeName, PipeDirection.Out, System.IO.Pipes.PipeOptions.Asynchronous);
|
||||||
|
|
||||||
var serverWait = _pipeServer.WaitForConnectionAsync();
|
var serverWait = _pipeServer.WaitForConnectionAsync();
|
||||||
_pipeClient.Connect();
|
_pipeClient.Connect();
|
||||||
serverWait.GetAwaiter().GetResult();
|
serverWait.GetAwaiter().GetResult();
|
||||||
|
|
||||||
_pipeWriter = PipeWriter.Create(_pipeClient);
|
_pipeWriter = PipeWriter.Create(_pipeClient);
|
||||||
var afterSer = GC.GetAllocatedBytesForCurrentThread();
|
var afterSer = GC.GetAllocatedBytesForCurrentThread();
|
||||||
SetupSerializeAllocBytes = afterSer - beforeSer;
|
SetupSerializeAllocBytes = afterSer - beforeSer;
|
||||||
|
|
@ -1857,17 +1884,21 @@ public static class Program
|
||||||
// kernel pipe into input; consumer drives Deserialize<T>(input) per iter on signal.
|
// kernel pipe into input; consumer drives Deserialize<T>(input) per iter on signal.
|
||||||
GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect();
|
GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect();
|
||||||
var beforeDes = GC.GetAllocatedBytesForCurrentThread();
|
var beforeDes = GC.GetAllocatedBytesForCurrentThread();
|
||||||
|
|
||||||
_pipeReader = PipeReader.Create(_pipeServer);
|
_pipeReader = PipeReader.Create(_pipeServer);
|
||||||
_input = new AsyncPipeReaderInput(_options.BufferWriterChunkSize * 2, multiMessage: true);
|
_input = new AsyncPipeReaderInput(_options.BufferWriterChunkSize * 2, multiMessage: true);
|
||||||
_cts = new CancellationTokenSource();
|
_cts = new CancellationTokenSource();
|
||||||
|
|
||||||
// Drain task: pumps PipeReader → input.Feed forever (or until cancel). Single Task.Run for
|
// Drain task: pumps PipeReader → input.Feed forever (or until cancel). Single Task.Run for
|
||||||
// the full benchmark lifetime — its overhead is amortised across all messages.
|
// the full benchmark lifetime — its overhead is amortised across all messages.
|
||||||
_drainTask = Task.Run(() => _input.DrainFromAsync(_pipeReader, _cts.Token));
|
_drainTask = Task.Run(() => _input.DrainFromAsync(_pipeReader, _cts.Token));
|
||||||
|
|
||||||
// Consumer task: per-iter Deserialize<T>(input) loop. Started here once; signaled per-iter via
|
// Consumer task: per-iter Deserialize<T>(input) loop. Started here once; signaled per-iter via
|
||||||
// _consumeRequest. Enables Ser↔Des streaming overlap — calling thread runs SerializeChunkedFramed
|
// _consumeRequest. Enables Ser↔Des streaming overlap — calling thread runs SerializeChunkedFramed
|
||||||
// while THIS task simultaneously runs Deserialize<T>, both consuming/producing through the
|
// while THIS task simultaneously runs Deserialize<T>, both consuming/producing through the
|
||||||
// sliding-window buffer pipelined by the drain task.
|
// sliding-window buffer pipelined by the drain task.
|
||||||
_consumerTask = Task.Run(ConsumeLoop);
|
_consumerTask = Task.Run(ConsumeLoop);
|
||||||
|
|
||||||
var afterDes = GC.GetAllocatedBytesForCurrentThread();
|
var afterDes = GC.GetAllocatedBytesForCurrentThread();
|
||||||
SetupDeserializeAllocBytes = afterDes - beforeDes;
|
SetupDeserializeAllocBytes = afterDes - beforeDes;
|
||||||
}
|
}
|
||||||
|
|
@ -2055,11 +2086,13 @@ public static class Program
|
||||||
// consumer task scaffolding. Identical to the NamedPipe variant on the receive side.
|
// consumer task scaffolding. Identical to the NamedPipe variant on the receive side.
|
||||||
GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect();
|
GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect();
|
||||||
var beforeDes = GC.GetAllocatedBytesForCurrentThread();
|
var beforeDes = GC.GetAllocatedBytesForCurrentThread();
|
||||||
|
|
||||||
_pipeReader = _pipe.Reader;
|
_pipeReader = _pipe.Reader;
|
||||||
_input = new AsyncPipeReaderInput(_options.BufferWriterChunkSize * 2, multiMessage: true);
|
_input = new AsyncPipeReaderInput(_options.BufferWriterChunkSize * 2, multiMessage: true);
|
||||||
_cts = new CancellationTokenSource();
|
_cts = new CancellationTokenSource();
|
||||||
_drainTask = Task.Run(() => _input.DrainFromAsync(_pipeReader, _cts.Token));
|
_drainTask = Task.Run(() => _input.DrainFromAsync(_pipeReader, _cts.Token));
|
||||||
_consumerTask = Task.Run(ConsumeLoop);
|
_consumerTask = Task.Run(ConsumeLoop);
|
||||||
|
|
||||||
var afterDes = GC.GetAllocatedBytesForCurrentThread();
|
var afterDes = GC.GetAllocatedBytesForCurrentThread();
|
||||||
SetupDeserializeAllocBytes = afterDes - beforeDes;
|
SetupDeserializeAllocBytes = afterDes - beforeDes;
|
||||||
}
|
}
|
||||||
|
|
@ -2668,6 +2701,7 @@ public static class Program
|
||||||
{
|
{
|
||||||
_bufferWriter.ResetWrittenCount();
|
_bufferWriter.ResetWrittenCount();
|
||||||
AcBinarySerializer.Serialize(_order, _bufferWriter, _options);
|
AcBinarySerializer.Serialize(_order, _bufferWriter, _options);
|
||||||
|
|
||||||
var roundTripped = AcBinaryDeserializer.Deserialize<TestOrder>(new ReadOnlySequence<byte>(_bufferWriter.WrittenMemory), _options);
|
var roundTripped = AcBinaryDeserializer.Deserialize<TestOrder>(new ReadOnlySequence<byte>(_bufferWriter.WrittenMemory), _options);
|
||||||
return DeepEqualsViaJson(_order, roundTripped);
|
return DeepEqualsViaJson(_order, roundTripped);
|
||||||
}
|
}
|
||||||
|
|
@ -2875,6 +2909,7 @@ public static class Program
|
||||||
.Select(r => (r.SerializerName, r.OptionsDescription!))
|
.Select(r => (r.SerializerName, r.OptionsDescription!))
|
||||||
.Distinct()
|
.Distinct()
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
if (optionsMap.Count > 0)
|
if (optionsMap.Count > 0)
|
||||||
{
|
{
|
||||||
System.Console.WriteLine();
|
System.Console.WriteLine();
|
||||||
|
|
@ -2914,6 +2949,7 @@ public static class Program
|
||||||
// The AcBinary Byte[]+Runtime variant is shown unhighlighted — it's contextual (SGen speed-up reference), not the headline.
|
// The AcBinary Byte[]+Runtime variant is shown unhighlighted — it's contextual (SGen speed-up reference), not the headline.
|
||||||
var isHighlighted = (result.Engine == EngineMemoryPack && result.IoMode == IoByteArray)
|
var isHighlighted = (result.Engine == EngineMemoryPack && result.IoMode == IoByteArray)
|
||||||
|| (result.Engine == EngineAcBinary && result.IoMode == IoByteArray && result.DispatchMode == ModeSGen);
|
|| (result.Engine == EngineAcBinary && result.IoMode == IoByteArray && result.DispatchMode == ModeSGen);
|
||||||
|
|
||||||
var prefix = isHighlighted ? "│►" : "│ ";
|
var prefix = isHighlighted ? "│►" : "│ ";
|
||||||
var suffix = isHighlighted ? "◄│" : " │";
|
var suffix = isHighlighted ? "◄│" : " │";
|
||||||
|
|
||||||
|
|
@ -3030,6 +3066,7 @@ public static class Program
|
||||||
.Select(g => new { Name = g.Key, AvgPerOp = g.Average(r => SerPerOp(r)) })
|
.Select(g => new { Name = g.Key, AvgPerOp = g.Average(r => SerPerOp(r)) })
|
||||||
.OrderBy(x => x.AvgPerOp)
|
.OrderBy(x => x.AvgPerOp)
|
||||||
.FirstOrDefault();
|
.FirstOrDefault();
|
||||||
|
|
||||||
if (fastestSer != null)
|
if (fastestSer != null)
|
||||||
System.Console.WriteLine($"{"Fastest Serialize",-20} │ {fastestSer.Name,-40} │ {fastestSer.AvgPerOp,12:F2} µs/op");
|
System.Console.WriteLine($"{"Fastest Serialize",-20} │ {fastestSer.Name,-40} │ {fastestSer.AvgPerOp,12:F2} µs/op");
|
||||||
|
|
||||||
|
|
@ -3039,6 +3076,7 @@ public static class Program
|
||||||
.Select(g => new { Name = g.Key, AvgPerOp = g.Average(r => DesPerOp(r)) })
|
.Select(g => new { Name = g.Key, AvgPerOp = g.Average(r => DesPerOp(r)) })
|
||||||
.OrderBy(x => x.AvgPerOp)
|
.OrderBy(x => x.AvgPerOp)
|
||||||
.FirstOrDefault();
|
.FirstOrDefault();
|
||||||
|
|
||||||
if (fastestDes != null)
|
if (fastestDes != null)
|
||||||
System.Console.WriteLine($"{"Fastest Deserialize",-20} │ {fastestDes.Name,-40} │ {fastestDes.AvgPerOp,12:F2} µs/op");
|
System.Console.WriteLine($"{"Fastest Deserialize",-20} │ {fastestDes.Name,-40} │ {fastestDes.AvgPerOp,12:F2} µs/op");
|
||||||
|
|
||||||
|
|
@ -3048,6 +3086,7 @@ public static class Program
|
||||||
.Select(g => new { Name = g.Key, AvgSize = g.Average(r => r.SerializedSize) })
|
.Select(g => new { Name = g.Key, AvgSize = g.Average(r => r.SerializedSize) })
|
||||||
.OrderBy(x => x.AvgSize)
|
.OrderBy(x => x.AvgSize)
|
||||||
.FirstOrDefault();
|
.FirstOrDefault();
|
||||||
|
|
||||||
if (smallestSize != null)
|
if (smallestSize != null)
|
||||||
System.Console.WriteLine($"{"Smallest Size",-20} │ {smallestSize.Name,-40} │ {smallestSize.AvgSize,15:F0} B");
|
System.Console.WriteLine($"{"Smallest Size",-20} │ {smallestSize.Name,-40} │ {smallestSize.AvgSize,15:F0} B");
|
||||||
|
|
||||||
|
|
@ -3057,6 +3096,7 @@ public static class Program
|
||||||
.Select(g => new { Name = g.Key, AvgPerOp = g.Average(r => RtPerOp(r)) })
|
.Select(g => new { Name = g.Key, AvgPerOp = g.Average(r => RtPerOp(r)) })
|
||||||
.OrderBy(x => x.AvgPerOp)
|
.OrderBy(x => x.AvgPerOp)
|
||||||
.FirstOrDefault();
|
.FirstOrDefault();
|
||||||
|
|
||||||
if (fastestRt != null)
|
if (fastestRt != null)
|
||||||
System.Console.WriteLine($"{"Fastest Round-trip",-20} │ {fastestRt.Name,-40} │ {fastestRt.AvgPerOp,12:F2} µs/op");
|
System.Console.WriteLine($"{"Fastest Round-trip",-20} │ {fastestRt.Name,-40} │ {fastestRt.AvgPerOp,12:F2} µs/op");
|
||||||
|
|
||||||
|
|
@ -3075,7 +3115,7 @@ public static class Program
|
||||||
if (memPackRtResults.Count == 0 || acBinaryRtResults.Count == 0)
|
if (memPackRtResults.Count == 0 || acBinaryRtResults.Count == 0)
|
||||||
{
|
{
|
||||||
System.Console.WriteLine();
|
System.Console.WriteLine();
|
||||||
System.Console.WriteLine($"── {"AcBinary (Byte[], SGen)"} vs {"MemoryPack (Byte[])"} (Overall) ──");
|
System.Console.WriteLine("── AcBinary (Byte[], SGen) vs MemoryPack (Byte[]) (Overall) ──");
|
||||||
System.Console.WriteLine(" (Comparison requires both serialize and deserialize data)");
|
System.Console.WriteLine(" (Comparison requires both serialize and deserialize data)");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -3099,7 +3139,7 @@ public static class Program
|
||||||
var desAllocStats = ComputeOverallStats(acBinaryDesResults, memPackDesResults, r => r.DeserializeAllocBytesPerOp);
|
var desAllocStats = ComputeOverallStats(acBinaryDesResults, memPackDesResults, r => r.DeserializeAllocBytesPerOp);
|
||||||
|
|
||||||
System.Console.WriteLine();
|
System.Console.WriteLine();
|
||||||
System.Console.WriteLine($"── {"AcBinary (Byte[], SGen)"} vs {"MemoryPack (Byte[])"} (Overall) ──");
|
System.Console.WriteLine("── AcBinary (Byte[], SGen) vs MemoryPack (Byte[]) (Overall) ──");
|
||||||
|
|
||||||
WriteOverallLine("Serialize", "µs/op", serStats);
|
WriteOverallLine("Serialize", "µs/op", serStats);
|
||||||
WriteOverallLine("Deserialize", "µs/op", desStats);
|
WriteOverallLine("Deserialize", "µs/op", desStats);
|
||||||
|
|
@ -3123,6 +3163,7 @@ public static class Program
|
||||||
private static void WriteOverallLine(string label, string unit, OverallStats? stats, string fmt = "F2")
|
private static void WriteOverallLine(string label, string unit, OverallStats? stats, string fmt = "F2")
|
||||||
{
|
{
|
||||||
if (stats == null) return;
|
if (stats == null) return;
|
||||||
|
|
||||||
// Color follows geo-mean (the magnitude-neutral signal). The arith-mean column may show a
|
// Color follows geo-mean (the magnitude-neutral signal). The arith-mean column may show a
|
||||||
// different sign when a single big cell dominates — that's exactly the signal we want to surface.
|
// different sign when a single big cell dominates — that's exactly the signal we want to surface.
|
||||||
System.Console.ForegroundColor = stats.GeoMeanPct <= 0 ? ConsoleColor.Green : ConsoleColor.Red;
|
System.Console.ForegroundColor = stats.GeoMeanPct <= 0 ? ConsoleColor.Green : ConsoleColor.Red;
|
||||||
|
|
@ -3201,6 +3242,7 @@ public static class Program
|
||||||
// CSV-like data for easy import — keeps raw byte integers (no KB rounding) so external tools can compute precisely.
|
// CSV-like data for easy import — keeps raw byte integers (no KB rounding) so external tools can compute precisely.
|
||||||
sb.AppendLine("=== RAW DATA (CSV) ===");
|
sb.AppendLine("=== RAW DATA (CSV) ===");
|
||||||
sb.AppendLine("TestData,Engine,IO,Mode,Options,Size,SerializeMicrosPerOp,DeserializeMicrosPerOp,RoundTripMicrosPerOp,SerializeAllocBytesPerOp,DeserializeAllocBytesPerOp,RoundTripAllocBytesPerOp,SetupSerializeAllocBytes,SetupDeserializeAllocBytes");
|
sb.AppendLine("TestData,Engine,IO,Mode,Options,Size,SerializeMicrosPerOp,DeserializeMicrosPerOp,RoundTripMicrosPerOp,SerializeAllocBytesPerOp,DeserializeAllocBytesPerOp,RoundTripAllocBytesPerOp,SetupSerializeAllocBytes,SetupDeserializeAllocBytes");
|
||||||
|
|
||||||
foreach (var testData in testDataSets)
|
foreach (var testData in testDataSets)
|
||||||
{
|
{
|
||||||
var testResults = results.Where(r => r.TestDataName == testData.DisplayName).ToList();
|
var testResults = results.Where(r => r.TestDataName == testData.DisplayName).ToList();
|
||||||
|
|
@ -3213,7 +3255,7 @@ public static class Program
|
||||||
|
|
||||||
// Formatted results
|
// Formatted results
|
||||||
sb.AppendLine("=== FORMATTED RESULTS BY TEST DATA ===");
|
sb.AppendLine("=== FORMATTED RESULTS BY TEST DATA ===");
|
||||||
sb.AppendLine($"(►) = Highlighted: {"MemoryPack (Byte[])"} (baseline) and {"AcBinary (Byte[])"}");
|
sb.AppendLine("(►) = Highlighted: MemoryPack (Byte[]) (baseline) and AcBinary (Byte[])");
|
||||||
sb.AppendLine();
|
sb.AppendLine();
|
||||||
|
|
||||||
foreach (var testData in testDataSets)
|
foreach (var testData in testDataSets)
|
||||||
|
|
@ -3256,7 +3298,7 @@ public static class Program
|
||||||
var desPct = DesPerOp(memPackResult) > 0 ? (DesPerOp(acBinaryResult) / DesPerOp(memPackResult) - 1) * 100 : 0;
|
var desPct = DesPerOp(memPackResult) > 0 ? (DesPerOp(acBinaryResult) / DesPerOp(memPackResult) - 1) * 100 : 0;
|
||||||
var rtPct = RtPerOp(memPackResult) > 0 ? (RtPerOp(acBinaryResult) / RtPerOp(memPackResult) - 1) * 100 : 0;
|
var rtPct = RtPerOp(memPackResult) > 0 ? (RtPerOp(acBinaryResult) / RtPerOp(memPackResult) - 1) * 100 : 0;
|
||||||
|
|
||||||
sb.AppendLine($" {"AcBinary (Byte[])"} vs {"MemoryPack (Byte[])"}: Size {sizePct:+0;-0}% │ Ser {serPct:+0;-0}% │ Des {desPct:+0;-0}% │ RT {rtPct:+0;-0}%");
|
sb.AppendLine($" AcBinary (Byte[]) vs MemoryPack (Byte[]): Size {sizePct:+0;-0}% │ Ser {serPct:+0;-0}% │ Des {desPct:+0;-0}% │ RT {rtPct:+0;-0}%");
|
||||||
}
|
}
|
||||||
|
|
||||||
//sb.AppendLine($"GrowBufferCount: {AcBinarySerializer.GrowBufferCount}");
|
//sb.AppendLine($"GrowBufferCount: {AcBinarySerializer.GrowBufferCount}");
|
||||||
|
|
@ -3268,7 +3310,7 @@ public static class Program
|
||||||
// Restrict AcBinary side to SGen — the SGen vs Runtime variants are shown side-by-side
|
// Restrict AcBinary side to SGen — the SGen vs Runtime variants are shown side-by-side
|
||||||
// in the per-test fancy table; the headline should compare apples-to-apples (both source-generated).
|
// in the per-test fancy table; the headline should compare apples-to-apples (both source-generated).
|
||||||
sb.AppendLine();
|
sb.AppendLine();
|
||||||
sb.AppendLine($"=== {"AcBinary (Byte[], SGen)"} vs {"MemoryPack (Byte[])"} (Overall) ===");
|
sb.AppendLine("=== AcBinary (Byte[], SGen) vs MemoryPack (Byte[]) (Overall) ===");
|
||||||
|
|
||||||
var memPackSerResults2 = results.Where(r => (r.Engine == EngineMemoryPack && r.IoMode == IoByteArray) && r.SerializeTimeMs > 0).ToList();
|
var memPackSerResults2 = results.Where(r => (r.Engine == EngineMemoryPack && r.IoMode == IoByteArray) && r.SerializeTimeMs > 0).ToList();
|
||||||
var memPackDesResults2 = results.Where(r => (r.Engine == EngineMemoryPack && r.IoMode == IoByteArray) && r.DeserializeTimeMs > 0).ToList();
|
var memPackDesResults2 = results.Where(r => (r.Engine == EngineMemoryPack && r.IoMode == IoByteArray) && r.DeserializeTimeMs > 0).ToList();
|
||||||
|
|
@ -3318,7 +3360,7 @@ public static class Program
|
||||||
var testTypeName = testDataSets.FirstOrDefault()?.TypeName ?? "unknown";
|
var testTypeName = testDataSets.FirstOrDefault()?.TypeName ?? "unknown";
|
||||||
sb.AppendLine($"# AcBinary Benchmark {BuildConfiguration} {DateTime.Now:yyyy-MM-dd HH:mm:ss}");
|
sb.AppendLine($"# AcBinary Benchmark {BuildConfiguration} {DateTime.Now:yyyy-MM-dd HH:mm:ss}");
|
||||||
sb.AppendLine($"Charset: {GetCurrentCharsetName()} | Iterations: per-cell adaptive (target ~{TargetSampleMs} ms/sample) | Warmup: {WarmupIterations} per phase (Ser/Des isolated) | Samples: {BenchmarkSamples} (median) + 1 pilot discarded | .NET: {Environment.Version} | TestType: {testTypeName} | UnstableCV threshold: {UnstableCVThreshold * 100:F0}%");
|
sb.AppendLine($"Charset: {GetCurrentCharsetName()} | Iterations: per-cell adaptive (target ~{TargetSampleMs} ms/sample) | Warmup: {WarmupIterations} per phase (Ser/Des isolated) | Samples: {BenchmarkSamples} (median) + 1 pilot discarded | .NET: {Environment.Version} | TestType: {testTypeName} | UnstableCV threshold: {UnstableCVThreshold * 100:F0}%");
|
||||||
sb.AppendLine($"Baseline: {"MemoryPack (Byte[])"} (SOTA reference) | Verified: round-trip correctness checked once per cell before warmup");
|
sb.AppendLine("Baseline: MemoryPack (Byte[]) (SOTA reference) | Verified: round-trip correctness checked once per cell before warmup");
|
||||||
|
|
||||||
// Options summary
|
// Options summary
|
||||||
var optionsMap = results
|
var optionsMap = results
|
||||||
|
|
@ -3349,7 +3391,7 @@ public static class Program
|
||||||
var testResults = results
|
var testResults = results
|
||||||
.Where(r => r.TestDataName == testData.DisplayName)
|
.Where(r => r.TestDataName == testData.DisplayName)
|
||||||
// Per-op µs (iter-independent) ordering — mixing iter counts within a cell is now expected.
|
// Per-op µs (iter-independent) ordering — mixing iter counts within a cell is now expected.
|
||||||
.OrderBy(r => RtPerOp(r))
|
.OrderBy(RtPerOp)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
foreach (var r in testResults)
|
foreach (var r in testResults)
|
||||||
|
|
@ -3368,10 +3410,12 @@ public static class Program
|
||||||
? FormatMicrosWithRange(r.RoundTripTimeMs, r.RoundTripTimeMinMs, r.RoundTripTimeMaxMs, r.RoundTripTimeStdDevMs, r.RoundTripIterations, inv)
|
? FormatMicrosWithRange(r.RoundTripTimeMs, r.RoundTripTimeMinMs, r.RoundTripTimeMaxMs, r.RoundTripTimeStdDevMs, r.RoundTripIterations, inv)
|
||||||
: RtPerOp(r).ToString("F2", inv))
|
: RtPerOp(r).ToString("F2", inv))
|
||||||
: "-";
|
: "-";
|
||||||
|
|
||||||
var serAlloc = r.SerializeTimeMs > 0 ? ToKilobytes(r.SerializeAllocBytesPerOp).ToString("F2", inv) : "-";
|
var serAlloc = r.SerializeTimeMs > 0 ? ToKilobytes(r.SerializeAllocBytesPerOp).ToString("F2", inv) : "-";
|
||||||
var desAlloc = r.DeserializeTimeMs > 0 ? ToKilobytes(r.DeserializeAllocBytesPerOp).ToString("F2", inv) : "-";
|
var desAlloc = r.DeserializeTimeMs > 0 ? ToKilobytes(r.DeserializeAllocBytesPerOp).ToString("F2", inv) : "-";
|
||||||
var rtAlloc = r.RoundTripAllocBytesPerOp > 0 ? ToKilobytes(r.RoundTripAllocBytesPerOp).ToString("F2", inv) : "-";
|
var rtAlloc = r.RoundTripAllocBytesPerOp > 0 ? ToKilobytes(r.RoundTripAllocBytesPerOp).ToString("F2", inv) : "-";
|
||||||
var setupAlloc = $"{ToKilobytes(r.SetupSerializeAllocBytes).ToString("F2", inv)} / {ToKilobytes(r.SetupDeserializeAllocBytes).ToString("F2", inv)}";
|
var setupAlloc = $"{ToKilobytes(r.SetupSerializeAllocBytes).ToString("F2", inv)} / {ToKilobytes(r.SetupDeserializeAllocBytes).ToString("F2", inv)}";
|
||||||
|
|
||||||
// Iter Ser/Des column — per-row adaptive iter counts. RT-only rows show Iter for RT.
|
// Iter Ser/Des column — per-row adaptive iter counts. RT-only rows show Iter for RT.
|
||||||
var iterCol = r.IsRoundTripOnly
|
var iterCol = r.IsRoundTripOnly
|
||||||
? r.RoundTripIterations.ToString(inv)
|
? r.RoundTripIterations.ToString(inv)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue