[LOADED_DOCS: 3 files, no new loads]
Benchmark: multi-sample median timing & EH inlining docs Added BenchmarkSamples for multi-sample median timing in benchmarks, reducing variance and improving result stability. Updated output to show sample count. Refactored RunTimed to support multiple samples. Expanded documentation on JIT inlining barriers: clarified that EH regions (try/catch/finally/using) in hot-path and generated methods block inlining on .NET 9, and provided guidance for future generator features and stackalloc usage. Added audit requirements for EH and stackalloc in hot paths.
This commit is contained in:
parent
96a2f90535
commit
6f5c57af6a
|
|
@ -50,9 +50,11 @@ public static class Program
|
|||
#if DEBUG
|
||||
private static int WarmupIterations = 0;
|
||||
private static int TestIterations = 1;
|
||||
private static int BenchmarkSamples = 1; // Debug: single sample, fast iteration
|
||||
#else
|
||||
private static int WarmupIterations = 5000;
|
||||
private static int TestIterations = 1000;
|
||||
private static int BenchmarkSamples = 5; // Release: 5-sample median for stability (~±5% variance vs. ~±15% single-sample)
|
||||
|
||||
//private static int WarmupIterations = 5000;
|
||||
//private static int TestIterations = 2000;
|
||||
|
|
@ -69,6 +71,7 @@ public static class Program
|
|||
{
|
||||
WarmupIterations = 5;
|
||||
TestIterations = 100;
|
||||
BenchmarkSamples = 3;
|
||||
mode = "all";
|
||||
}
|
||||
|
||||
|
|
@ -85,7 +88,7 @@ public static class Program
|
|||
var allResults = new List<BenchmarkResult>();
|
||||
var testDataSets = BenchmarkTestDataProvider.CreateTestDataSets();
|
||||
|
||||
System.Console.WriteLine($"Mode: {mode} | Iterations: {TestIterations} | Warmup: {WarmupIterations}");
|
||||
System.Console.WriteLine($"Mode: {mode} | Iterations: {TestIterations} | Warmup: {WarmupIterations} | Samples: {BenchmarkSamples} (median)");
|
||||
System.Console.WriteLine($"Build: {BuildConfiguration} | .NET: {Environment.Version} | Test Type: {testDataSets.FirstOrDefault()?.TypeName ?? "unknown"}");
|
||||
System.Console.WriteLine();
|
||||
|
||||
|
|
@ -253,15 +256,37 @@ public static class Program
|
|||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs the action <paramref name="iterations"/> times for <see cref="BenchmarkSamples"/> independent samples,
|
||||
/// returning the median elapsed time. Multi-sample design reduces single-run variance from ~±15% to ~±5%
|
||||
/// by smoothing transient effects (background activity, thermal/turbo state, JIT tier-promotion timing).
|
||||
/// When <see cref="BenchmarkSamples"/> <= 1, falls back to single-sample timing (Debug / quick mode).
|
||||
/// </summary>
|
||||
private static double RunTimed(Action action, int iterations)
|
||||
{
|
||||
var sw = Stopwatch.StartNew();
|
||||
for (var i = 0; i < iterations; i++)
|
||||
var samples = BenchmarkSamples;
|
||||
if (samples <= 1)
|
||||
{
|
||||
action();
|
||||
// Single-sample fast path (Debug or trivial run) — no allocation, no sort.
|
||||
var sw = Stopwatch.StartNew();
|
||||
for (var i = 0; i < iterations; i++) action();
|
||||
sw.Stop();
|
||||
return sw.Elapsed.TotalMilliseconds;
|
||||
}
|
||||
sw.Stop();
|
||||
return sw.Elapsed.TotalMilliseconds;
|
||||
|
||||
var times = new double[samples];
|
||||
for (int s = 0; s < samples; s++)
|
||||
{
|
||||
var sw = Stopwatch.StartNew();
|
||||
for (var i = 0; i < iterations; i++) action();
|
||||
sw.Stop();
|
||||
times[s] = sw.Elapsed.TotalMilliseconds;
|
||||
}
|
||||
Array.Sort(times);
|
||||
// Median: middle value for odd sample counts, average of two middles for even counts.
|
||||
return samples % 2 == 1
|
||||
? times[samples / 2]
|
||||
: (times[samples / 2 - 1] + times[samples / 2]) / 2.0;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
|
@ -747,6 +772,7 @@ public static class Program
|
|||
sb.AppendLine($"║ Generated: {DateTime.Now:yyyy-MM-dd HH:mm:ss}".PadRight(100) + "║");
|
||||
sb.AppendLine($"║ Build: {BuildConfiguration}".PadRight(100) + "║");
|
||||
sb.AppendLine($"║ Iterations: {TestIterations}".PadRight(100) + "║");
|
||||
sb.AppendLine($"║ Samples: {BenchmarkSamples} (median)".PadRight(100) + "║");
|
||||
sb.AppendLine($"║ Test Type: {testDataSets.FirstOrDefault()?.TypeName ?? "unknown"}".PadRight(100) + "║");
|
||||
sb.AppendLine("╚══════════════════════════════════════════════════════════════════════════════════════════════════════╝");
|
||||
sb.AppendLine();
|
||||
|
|
@ -874,7 +900,7 @@ public static class Program
|
|||
var sb = new StringBuilder();
|
||||
var testTypeName = testDataSets.FirstOrDefault()?.TypeName ?? "unknown";
|
||||
sb.AppendLine($"# AcBinary Benchmark {BuildConfiguration} {DateTime.Now:yyyy-MM-dd HH:mm:ss}");
|
||||
sb.AppendLine($"Iterations: {TestIterations} | Warmup: {WarmupIterations} | .NET: {Environment.Version} | TestType: {testTypeName}");
|
||||
sb.AppendLine($"Iterations: {TestIterations} | Warmup: {WarmupIterations} | Samples: {BenchmarkSamples} (median) | .NET: {Environment.Version} | TestType: {testTypeName}");
|
||||
|
||||
// Options summary
|
||||
var optionsMap = results
|
||||
|
|
|
|||
|
|
@ -115,6 +115,14 @@ Two-phase:
|
|||
- **Cold:** multi-byte logic in separate `NoInlining` method (e.g. `WriteVarUIntMultiByteUnsafe`)
|
||||
- Keeps caller IL small, cache-friendly
|
||||
|
||||
**Inlining barriers — `[MethodImpl(AggressiveInlining)]` is silently ignored when:**
|
||||
|
||||
- **`try` / `catch` / `finally` / `using`** — any EH region in the method is a hard JIT rule (`inline.cpp` in CoreCLR). `using` statements desugar to `try/finally` and have the same effect. Move resource cleanup (`Pool.Return`, `ArrayPool.Return`, `Dispose`) into a separate cold method or keep the cleanup outside the hot caller. The Pool.Get → try/finally → Pool.Return pattern (Rule #5) is fine because it sits at the entry point of `Serialize<T>`, not on a per-property hot path.
|
||||
- **`stackalloc` with non-constant or large size** — small constant `stackalloc` (≤ ~1KB) is inlinable in .NET 6+, but the moment any other barrier (try/finally, complex control flow) is added the method becomes non-inlinable. When mixing `stackalloc` with `try/finally` (e.g. ArrayPool fallback + scratch buffer), expect the helper to always be a separate call frame — design accordingly (avoid inline-only assumptions in the caller).
|
||||
- **Method size / IL token count** — the JIT has IL-size and basic-block thresholds even with `AggressiveInlining`. For large generated methods (SGen `WriteProperties` for property-heavy types) the attribute is a hint, not a guarantee; see `BINARY_TODO.md#accore-bin-t-t5j8` for `AggressiveOptimization` as a complementary tool.
|
||||
|
||||
When adding a new helper to the hot path: read the method first for any of the above before placing `[MethodImpl(AggressiveInlining)]` on it. The attribute lies if the body has an EH region — silently.
|
||||
|
||||
### 4. SGen Root Fast Path
|
||||
|
||||
**Rule:** Root-level SGen types MUST skip `WriteValue`/`TryWritePrimitive`/`WriteValueNonPrimitive` dispatch chain.
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue