[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.
|
||||
// 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.
|
||||
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 —
|
||||
// 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
|
||||
// [AcBinarySerializable(enablePropertyFilterFeature: false)] → SGen-emit + Runtime hot-loop both gate).
|
||||
var propFilterOpt = options.PropertyFilter == null ? "None" : "Set";
|
||||
|
||||
return $"WireMode={options.WireMode}, " +
|
||||
$"RefHandling={options.ReferenceHandling}(opt) | {_attrFlags.refHandling} (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.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
|
|
@ -231,10 +233,7 @@ public static class Program
|
|||
/// 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.
|
||||
/// </summary>
|
||||
private static OverallStats? ComputeOverallStats(
|
||||
List<BenchmarkResult> acResults,
|
||||
List<BenchmarkResult> mpResults,
|
||||
Func<BenchmarkResult, double> getValue)
|
||||
private static OverallStats? ComputeOverallStats(List<BenchmarkResult> acResults, List<BenchmarkResult> mpResults, Func<BenchmarkResult, double> getValue)
|
||||
{
|
||||
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.
|
||||
if (minMs <= 0 && maxMs <= 0) return med.ToString("F2", inv);
|
||||
if (minMs >= medianMs && maxMs <= medianMs) return med.ToString("F2", inv);
|
||||
|
||||
var min = ToPerOpMicros(minMs, iterations);
|
||||
var max = ToPerOpMicros(maxMs, iterations);
|
||||
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
|
||||
// small inter-engine delta on a high-CV row is easy to discount as noise.
|
||||
if (medianMs > 0 && stdDevMs > 0)
|
||||
|
|
@ -297,6 +298,7 @@ public static class Program
|
|||
return $"{range} ⚠️{cvPct}%";
|
||||
}
|
||||
}
|
||||
|
||||
return range;
|
||||
}
|
||||
|
||||
|
|
@ -323,6 +325,7 @@ public static class Program
|
|||
{
|
||||
if (!TryParseCliArgs(args, out var layer, out var opMode, out var serializerMode))
|
||||
return; // profiler mode (already ran) or invalid args
|
||||
|
||||
RunBenchmark(layer, opMode, serializerMode);
|
||||
return;
|
||||
}
|
||||
|
|
@ -476,6 +479,7 @@ public static class Program
|
|||
if (BenchmarkSamples > 1) // skip in DEBUG (single-sample fast iteration)
|
||||
{
|
||||
System.Console.WriteLine($"Global JIT pre-warmup ({testDataSets.Count} cells × all serializers, light pass)...");
|
||||
|
||||
foreach (var testData in testDataSets)
|
||||
{
|
||||
var preSerializers = CreateSerializers(testData, serializerMode);
|
||||
|
|
@ -494,6 +498,7 @@ public static class Program
|
|||
foreach (var s in preSerializers) (s as IDisposable)?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
// Let background tiered-JIT compilation drain before we begin measuring.
|
||||
if (JitSleep > 0) Thread.Sleep(JitSleep);
|
||||
System.Console.WriteLine("✓ Global pre-warmup complete.\n");
|
||||
|
|
@ -960,6 +965,7 @@ public static class Program
|
|||
GC.Collect();
|
||||
GC.WaitForPendingFinalizers();
|
||||
GC.Collect();
|
||||
|
||||
var pilotSw = Stopwatch.StartNew();
|
||||
RunWithProgress(action, iterations, progressLabel, samples + 1, sampleIndex: 0);
|
||||
pilotSw.Stop();
|
||||
|
|
@ -986,6 +992,7 @@ public static class Program
|
|||
var maxMs = double.MinValue;
|
||||
var sum = 0.0;
|
||||
var sumSq = 0.0;
|
||||
|
||||
for (var i = 0; i < times.Length; 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.
|
||||
var medianMs = samples % 2 == 1 ? times[samples / 2] : (times[samples / 2 - 1] + times[samples / 2]) / 2.0;
|
||||
EndProgress(progressLabel, medianMs);
|
||||
|
||||
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.
|
||||
var rounded = ((raw + 999) / 1000) * 1000;
|
||||
|
||||
if (rounded < 1000) return 1000;
|
||||
if (rounded > 200_000) return 200_000;
|
||||
return rounded;
|
||||
return rounded switch
|
||||
{
|
||||
< 1000 => 1000,
|
||||
> 200_000 => 200_000,
|
||||
_ => rounded
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -1122,10 +1133,13 @@ public static class Program
|
|||
var line = samples > 1
|
||||
? $" > {label} sample {sampleIndex + 1}/{samples} {pct,3}% ({i + 1}/{iterations})"
|
||||
: $" > {label} {pct,3}% ({i + 1}/{iterations})";
|
||||
|
||||
System.Console.Write('\r');
|
||||
System.Console.Write(line);
|
||||
|
||||
if (line.Length < _progressLastLineLen)
|
||||
System.Console.Write(new string(' ', _progressLastLineLen - line.Length));
|
||||
|
||||
_progressLastLineLen = line.Length;
|
||||
}
|
||||
}
|
||||
|
|
@ -1139,10 +1153,13 @@ public static class Program
|
|||
{
|
||||
if (label is null) return;
|
||||
var done = $" > {label} done in {elapsedMs,7:F1} ms";
|
||||
|
||||
System.Console.Write('\r');
|
||||
System.Console.Write(done);
|
||||
|
||||
if (done.Length < _progressLastLineLen)
|
||||
System.Console.Write(new string(' ', _progressLastLineLen - done.Length));
|
||||
|
||||
System.Console.WriteLine();
|
||||
_progressLastLineLen = 0;
|
||||
}
|
||||
|
|
@ -1301,13 +1318,17 @@ public static class Program
|
|||
private static string GetCurrentCharsetName()
|
||||
{
|
||||
var s = BenchmarkTestDataProvider.LongStringSuffix;
|
||||
if (s == CharsetSuffixes.Latin1FixAscii) return "Latin1FixAscii";
|
||||
if (s == CharsetSuffixes.Latin1Short) return "Latin1Short";
|
||||
if (s == CharsetSuffixes.Latin1Long) return "Latin1Long";
|
||||
if (s == CharsetSuffixes.CjkBmp) return "CjkBmp";
|
||||
if (s == CharsetSuffixes.Cyrillic) return "Cyrillic";
|
||||
if (s == CharsetSuffixes.Mixed) return "Mixed";
|
||||
return "Custom";
|
||||
|
||||
return s switch
|
||||
{
|
||||
CharsetSuffixes.Latin1FixAscii => "Latin1FixAscii",
|
||||
CharsetSuffixes.Latin1Short => "Latin1Short",
|
||||
CharsetSuffixes.Latin1Long => "Latin1Long",
|
||||
CharsetSuffixes.CjkBmp => "CjkBmp",
|
||||
CharsetSuffixes.Cyrillic => "Cyrillic",
|
||||
CharsetSuffixes.Mixed => "Mixed",
|
||||
_ => "Custom"
|
||||
};
|
||||
}
|
||||
|
||||
private static void ShowCharsetSettingsMenu()
|
||||
|
|
@ -1424,9 +1445,12 @@ public static class Program
|
|||
private static int PromptInt(string name, int currentValue, int min)
|
||||
{
|
||||
System.Console.Write($" {name} [{currentValue}]: ");
|
||||
|
||||
var input = System.Console.ReadLine()?.Trim() ?? "";
|
||||
if (input.Length == 0) return currentValue;
|
||||
|
||||
if (int.TryParse(input, out var newValue) && newValue >= min) return newValue;
|
||||
|
||||
System.Console.WriteLine($" ! Invalid value (need int ≥ {min}) — keeping {currentValue}");
|
||||
return currentValue;
|
||||
}
|
||||
|
|
@ -1838,15 +1862,18 @@ public static class Program
|
|||
// pipe-pair (server + client) + connect handshake + writer-side PipeWriter wrapper.
|
||||
GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect();
|
||||
var beforeSer = GC.GetAllocatedBytesForCurrentThread();
|
||||
|
||||
_pipeServer = new NamedPipeServerStream(pipeName, PipeDirection.In, 1, PipeTransmissionMode.Byte,
|
||||
System.IO.Pipes.PipeOptions.Asynchronous,
|
||||
inBufferSize: _options.BufferWriterChunkSize,
|
||||
outBufferSize: _options.BufferWriterChunkSize);
|
||||
|
||||
_pipeClient = new NamedPipeClientStream(".", pipeName, PipeDirection.Out, System.IO.Pipes.PipeOptions.Asynchronous);
|
||||
|
||||
var serverWait = _pipeServer.WaitForConnectionAsync();
|
||||
_pipeClient.Connect();
|
||||
serverWait.GetAwaiter().GetResult();
|
||||
|
||||
_pipeWriter = PipeWriter.Create(_pipeClient);
|
||||
var afterSer = GC.GetAllocatedBytesForCurrentThread();
|
||||
SetupSerializeAllocBytes = afterSer - beforeSer;
|
||||
|
|
@ -1857,17 +1884,21 @@ public static class Program
|
|||
// kernel pipe into input; consumer drives Deserialize<T>(input) per iter on signal.
|
||||
GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect();
|
||||
var beforeDes = GC.GetAllocatedBytesForCurrentThread();
|
||||
|
||||
_pipeReader = PipeReader.Create(_pipeServer);
|
||||
_input = new AsyncPipeReaderInput(_options.BufferWriterChunkSize * 2, multiMessage: true);
|
||||
_cts = new CancellationTokenSource();
|
||||
|
||||
// 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.
|
||||
_drainTask = Task.Run(() => _input.DrainFromAsync(_pipeReader, _cts.Token));
|
||||
|
||||
// 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
|
||||
// while THIS task simultaneously runs Deserialize<T>, both consuming/producing through the
|
||||
// sliding-window buffer pipelined by the drain task.
|
||||
_consumerTask = Task.Run(ConsumeLoop);
|
||||
|
||||
var afterDes = GC.GetAllocatedBytesForCurrentThread();
|
||||
SetupDeserializeAllocBytes = afterDes - beforeDes;
|
||||
}
|
||||
|
|
@ -2055,11 +2086,13 @@ public static class Program
|
|||
// consumer task scaffolding. Identical to the NamedPipe variant on the receive side.
|
||||
GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect();
|
||||
var beforeDes = GC.GetAllocatedBytesForCurrentThread();
|
||||
|
||||
_pipeReader = _pipe.Reader;
|
||||
_input = new AsyncPipeReaderInput(_options.BufferWriterChunkSize * 2, multiMessage: true);
|
||||
_cts = new CancellationTokenSource();
|
||||
_drainTask = Task.Run(() => _input.DrainFromAsync(_pipeReader, _cts.Token));
|
||||
_consumerTask = Task.Run(ConsumeLoop);
|
||||
|
||||
var afterDes = GC.GetAllocatedBytesForCurrentThread();
|
||||
SetupDeserializeAllocBytes = afterDes - beforeDes;
|
||||
}
|
||||
|
|
@ -2668,6 +2701,7 @@ public static class Program
|
|||
{
|
||||
_bufferWriter.ResetWrittenCount();
|
||||
AcBinarySerializer.Serialize(_order, _bufferWriter, _options);
|
||||
|
||||
var roundTripped = AcBinaryDeserializer.Deserialize<TestOrder>(new ReadOnlySequence<byte>(_bufferWriter.WrittenMemory), _options);
|
||||
return DeepEqualsViaJson(_order, roundTripped);
|
||||
}
|
||||
|
|
@ -2875,6 +2909,7 @@ public static class Program
|
|||
.Select(r => (r.SerializerName, r.OptionsDescription!))
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
if (optionsMap.Count > 0)
|
||||
{
|
||||
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.
|
||||
var isHighlighted = (result.Engine == EngineMemoryPack && result.IoMode == IoByteArray)
|
||||
|| (result.Engine == EngineAcBinary && result.IoMode == IoByteArray && result.DispatchMode == ModeSGen);
|
||||
|
||||
var prefix = isHighlighted ? "│►" : "│ ";
|
||||
var suffix = isHighlighted ? "◄│" : " │";
|
||||
|
||||
|
|
@ -3030,6 +3066,7 @@ public static class Program
|
|||
.Select(g => new { Name = g.Key, AvgPerOp = g.Average(r => SerPerOp(r)) })
|
||||
.OrderBy(x => x.AvgPerOp)
|
||||
.FirstOrDefault();
|
||||
|
||||
if (fastestSer != null)
|
||||
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)) })
|
||||
.OrderBy(x => x.AvgPerOp)
|
||||
.FirstOrDefault();
|
||||
|
||||
if (fastestDes != null)
|
||||
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) })
|
||||
.OrderBy(x => x.AvgSize)
|
||||
.FirstOrDefault();
|
||||
|
||||
if (smallestSize != null)
|
||||
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)) })
|
||||
.OrderBy(x => x.AvgPerOp)
|
||||
.FirstOrDefault();
|
||||
|
||||
if (fastestRt != null)
|
||||
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)
|
||||
{
|
||||
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)");
|
||||
return;
|
||||
}
|
||||
|
|
@ -3099,7 +3139,7 @@ public static class Program
|
|||
var desAllocStats = ComputeOverallStats(acBinaryDesResults, memPackDesResults, r => r.DeserializeAllocBytesPerOp);
|
||||
|
||||
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("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")
|
||||
{
|
||||
if (stats == null) return;
|
||||
|
||||
// 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.
|
||||
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.
|
||||
sb.AppendLine("=== RAW DATA (CSV) ===");
|
||||
sb.AppendLine("TestData,Engine,IO,Mode,Options,Size,SerializeMicrosPerOp,DeserializeMicrosPerOp,RoundTripMicrosPerOp,SerializeAllocBytesPerOp,DeserializeAllocBytesPerOp,RoundTripAllocBytesPerOp,SetupSerializeAllocBytes,SetupDeserializeAllocBytes");
|
||||
|
||||
foreach (var testData in testDataSets)
|
||||
{
|
||||
var testResults = results.Where(r => r.TestDataName == testData.DisplayName).ToList();
|
||||
|
|
@ -3213,7 +3255,7 @@ public static class Program
|
|||
|
||||
// Formatted results
|
||||
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();
|
||||
|
||||
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 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}");
|
||||
|
|
@ -3268,7 +3310,7 @@ public static class Program
|
|||
// 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).
|
||||
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 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";
|
||||
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($"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
|
||||
var optionsMap = results
|
||||
|
|
@ -3349,7 +3391,7 @@ public static class Program
|
|||
var testResults = results
|
||||
.Where(r => r.TestDataName == testData.DisplayName)
|
||||
// Per-op µs (iter-independent) ordering — mixing iter counts within a cell is now expected.
|
||||
.OrderBy(r => RtPerOp(r))
|
||||
.OrderBy(RtPerOp)
|
||||
.ToList();
|
||||
|
||||
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)
|
||||
: RtPerOp(r).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 rtAlloc = r.RoundTripAllocBytesPerOp > 0 ? ToKilobytes(r.RoundTripAllocBytesPerOp).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.
|
||||
var iterCol = r.IsRoundTripOnly
|
||||
? r.RoundTripIterations.ToString(inv)
|
||||
|
|
|
|||
Loading…
Reference in New Issue