Suite mode: BenchmarkSuite
Advanced features: Parameterized benchmarks, categories, isolated runs, multi-runtime comparison, and multiple launches are covered in the Features section.
BenchmarkSuite is a fluent builder for running several benchmarks in the same run and comparing them. It handles run ordering, significance testing, setup and teardown, and reporter output automatically.
Minimal example
using NBenchmark;
using NBenchmark.Reporters.Console;
var results = await new BenchmarkSuite("sorting")
.Add("bubble", () =>
{
var arr = Enumerable.Range(0, 100).Reverse().ToArray();
Array.Sort(arr);
})
.Add("linq", () =>
{
_ = Enumerable.Range(0, 100).Reverse().OrderBy(x => x).ToArray();
})
.WithBaseline("bubble")
.WithReporter(new ConsoleReporter())
.RunAsync();Adding benchmarks
Synchronous
suite.Add("name", () => DoWork());
// Return a value - prevents dead-code elimination
suite.Add("name", () => ComputeHash(data));Async
suite.Add("name", async () => await FetchDataAsync());
// Async with return value
suite.Add("name", async () => await ComputeAsync(input));With per-benchmark setup and teardown
The optional setup and teardown callbacks run before and after each iteration:
suite.Add(
name: "db query",
action: () => db.Execute("SELECT 1"),
setup: () => db.BeginTransaction(),
teardown: () => db.Rollback()
);WARNING
Setup and teardown time is not included in the measurement. Only the action is timed.
With categories
Tag a benchmark with categories and filter the suite before running. See Categories for the full filtering model.
var results = await new BenchmarkSuite("sorting")
.Add("bubble", () => { }, categories: ["Classic"])
.Add("linq", () => { }, categories: ["Modern"])
.WithCategoryFilter(include: ["Classic"])
.WithReporter(new ConsoleReporter())
.RunAsync();Use .WithCategories(params string[]) to apply categories to every subsequent .Add call:
await new BenchmarkSuite("string")
.WithCategories("String")
.Add("concat", () => "a" + "b")
.Add("interpolate", () => $"a { "b" }")
.RunAsync();Benchmark names must be unique
Each name within a suite must be distinct. The significance test keys raw samples by name, so duplicates would corrupt the results.
// This throws ArgumentException:
suite.Add("sort", SortA).Add("sort", SortB);Fluent configuration
All configuration methods return this, so they can be chained:
await new BenchmarkSuite("name")
.Add(...)
.Add(...)
.WithBaseline("name") // which benchmark is the 1.00x reference
.WithParameter("size", 10, 100) // expand parameterized benchmarks across values
.WithIterations(200) // pin measured samples (default: auto)
.WithWarmup(25) // pin warmup samples (default: auto)
.WithLaunchCount(3) // repeat each benchmark 3 times as separate launches (default: 1)
.WithAllocations() // enable allocation tracking
.WithOutlierMode(OutlierMode.IqrFence) // default
.WithOutlierDetector(new MyDetector()) // custom IOutlierDetector (overrides WithOutlierMode)
.WithConfidenceLevel(0.99) // default: 0.95
.WithSignificanceLevel(0.05) // alpha for the significance test; default: 0.05
.WithSignificance(false) // disable significance testing
.WithSignificanceTest(new MyTest()) // custom ISignificanceTest
.WithRunOrder(RunOrder.Declaration) // default: RunOrder.Random
.WithSuiteSetup(() => { }) // runs once before all benchmarks
.WithSuiteTeardown(() => { }) // runs once after all benchmarks
.WithIsolation() // run the whole suite in one clean child process
.WithReporter(new ConsoleReporter())
.WithReporter(new MarkdownReporter("results/"))
.WithProgress(new ConsoleBenchmarkProgress())
.RunAsync();See Configuration for details on every option.
Custom statistics
The suite uses the same pluggable statistics as the rest of the engine. By default it trims outliers with the IQR fence and tests significance with DefaultSignificanceTest - Mann-Whitney U for two benchmarks, the Kruskal-Wallis omnibus test (followed by post-hoc pairwise Mann-Whitney U with Holm-Bonferroni correction) for three or more. Override either when your data needs it:
using NBenchmark.Stats;
await new BenchmarkSuite("latency")
.Add("a", RunA)
.Add("b", RunB)
.Add("c", RunC)
.WithOutlierDetector(new KeepFastestDetector(0.90)) // custom trimming
.WithSignificanceTest(new MedianRatioSignificanceTest(25)) // custom significance rule
.RunAsync();WithOutlierDetector takes priority over WithOutlierMode. See Custom outlier detectors and Custom significance tests for the interfaces and contracts.
Setting a baseline
Call WithBaseline("name") to designate one benchmark as the reference point. The Ratio column in the output shows how fast each other benchmark is relative to the baseline, and significance is tested against it.
If no baseline is set, NBenchmark uses the benchmark with the lowest median as the implicit baseline for ratio calculations.
Suite setup and teardown
WithSuiteSetup and WithSuiteTeardown run once around the entire suite - useful for starting a server, opening a connection, or initialising shared state:
await new BenchmarkSuite("http")
.WithSuiteSetup(() => server.Start())
.WithSuiteTeardown(() => server.Stop())
.Add("get", async () => await httpClient.GetStringAsync("/"))
.Add("post", async () => await httpClient.PostAsync("/", content))
.RunAsync();Once suite setup has succeeded, suite teardown is guaranteed to run - even when the run is cancelled through a CancellationToken - so resources opened in setup are always released.
Multi-runtime comparison
Use WithRuntimes to run the same benchmarks across multiple .NET runtimes and compare results side-by-side. See Multi-runtime comparison for the full guide.
Process isolation
Call WithIsolation() to run the entire suite in a single freshly spawned child process, so runtime state (JIT warmup, GC pressure, thread-pool state) from the host can't bias the measurements:
await new BenchmarkSuite("sorting")
.Add("bubble", () => BubbleSort(data))
.Add("array", () => Array.Sort(data))
.WithBaseline("bubble")
.WithIsolation() // whole suite runs in one clean child process
.RunAsync();WithIsolation(false) is the default (in-process). The child rebuilds the suite from your own Main, so custom IOutlierDetector / ISignificanceTest instances and suite setup/teardown are preserved. See Isolated Runs for the full model.
Multiple launches
Use WithLaunchCount(n) to run each benchmark in the suite N times as independent launches. See Multiple launches for the full guide.
Parameterized benchmarks
Use WithParameter and typed Add overloads to run the same benchmark body across multiple input values. Each parameter combination produces a separate benchmark entry with a distinct name like "sort(size=10)". See Parameterized benchmarks: Suite mode for the full guide.
var results = await new BenchmarkSuite("sorting")
.WithParameter("size", 10, 100, 1000)
.Add("sort", (int size) =>
{
var arr = Enumerable.Range(0, size).Reverse().ToArray();
Array.Sort(arr);
})
.WithRunOrder(RunOrder.Declaration)
.RunAsync();Run order
By default benchmarks run in a random order (Fisher-Yates shuffle). This guards against systematic bias where the first benchmark always benefits from a warm CPU cache.
.WithRunOrder(RunOrder.Declaration) // run in the order Add() was called
.WithRunOrder(RunOrder.Random) // defaultMultiple reporters
You can attach any number of reporters. They all receive the same results:
suite
.WithReporter(new ConsoleReporter())
.WithReporter(new MarkdownReporter("results/"))
.WithReporter(new CsvReporter("results/"))Progress display
ConsoleBenchmarkProgress (from NBenchmark.Reporters.Console) shows warmup and measurement progress for each benchmark:
.WithProgress(new ConsoleBenchmarkProgress())Pass the same values you gave to WithIterations and WithWarmup so the progress display is accurate.
Return value
RunAsync() returns IReadOnlyList<BenchmarkResult>. You can process the results programmatically after the run:
var results = await suite.RunAsync();
foreach (var result in results.Where(r => !r.Errored))
Console.WriteLine($"{result.Name}: {result.Median:F0} ns median");Errored benchmarks have result.Errored == true and a message in result.ErrorMessage. They are included in the list so reporters can display them.
Next steps
- Parameterized benchmarks: Suite mode - run benchmarks across multiple input values
- Multi-runtime comparison - compare across .NET runtimes
- Multiple launches - measure run-to-run variance
- Isolated runs - run in a clean child process
- Harness mode: BenchmarkHarness - attribute-based discovery and CLI control
- Configuration - full options reference
- Reporters - all available reporters
