How to Configure BenchmarkDotNet for Accurate Microbenchmarking in .NET
BenchmarkDotNet requires a console project with the latest NuGet package, proper entry points using BenchmarkSwitcher, and carefully designed benchmark methods that return values to prevent dead-code elimination, alongside explicit configuration via ManualConfig or CLI flags to generate statistically reliable measurements.
The dotnet/skills repository maintains authoritative guidance for microbenchmarking in the plugins/dotnet-diag/skills/microbenchmarking directory. According to the internal references—including writing-benchmarks.md, project-setup-and-running.md, and bdn-internals-and-tuning.md—accurate BenchmarkDotNet configuration involves project setup, entry point selection, benchmark method design, and runtime execution parameters. The following sections distill the repository's best practices into a complete configuration workflow.
Create a Clean Console Project
Start with a minimal console application to isolate benchmark dependencies from production code.
dotnet new console -n MyBenchmarks
cd MyBenchmarks
dotnet add package BenchmarkDotNet
If you need to compare multiple runtimes, modify the project file to use plural <TargetFrameworks>:
<TargetFrameworks>net8.0;net9.0</TargetFrameworks>
This multi-targeting approach is documented in project-setup-and-running.md under "Creating a new benchmark project."
Select the Correct Entry Point
BenchmarkDotNet offers two primary entry points, and choosing the wrong one silently ignores CLI arguments.
Use BenchmarkSwitcher for automation and CI/CD pipelines. It respects CLI flags like --filter and --job automatically:
using BenchmarkDotNet.Running;
BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args);
Use BenchmarkRunner only when you explicitly forward arguments, otherwise it ignores CLI flags:
BenchmarkRunner.Run<MyBenchmark>(args: args);
As noted in project-setup-and-running.md (lines 5-23), always prefer BenchmarkSwitcher for automation workflows to ensure configuration via command-line arguments takes effect.
Write Reliable Benchmark Methods
Accurate microbenchmarking depends on methods that the JIT compiler cannot optimize away. According to writing-benchmarks.md (lines 5-165), avoid these common pitfalls:
Prevent dead-code elimination by returning a value from your benchmark method. Void methods returning nothing may be optimized away entirely:
// Good: returns a value
[Benchmark]
public int Parse() => int.Parse("12345");
// Bad: void return may be eliminated
[Benchmark]
public void ParseVoid() => int.Parse("12345");
Avoid manual loops inside benchmark methods. BDN handles invocation counting; manual loops add overhead that obscures per-operation costs. Use OperationsPerInvoke only when you must manually repeat operations.
Materialize deferred execution before returning. Returning IEnumerable<T> measures only object creation, not enumeration:
// Bad: measures only iterator creation
[Benchmark]
public IEnumerable<int> GetItems() => Enumerable.Range(0, 100);
// Good: forces enumeration
[Benchmark]
public List<int> GetItemsList() => Enumerable.Range(0, 100).ToList();
Prevent constant folding by storing inputs in fields or using [Params]. Literal values allow the JIT to pre-compute results at compile time:
private string _input = "12345";
[Benchmark]
public int ParseInput() => int.Parse(_input);
Reset state properly using [GlobalSetup] for immutable state and [IterationSetup] only when necessary. [IterationSetup] forces InvocationCount = 1 and adds significant overhead, so prefer [GlobalSetup] unless the benchmark mutates state each iteration.
Parameterize Benchmarks Correctly
BenchmarkDotNet provides several attributes for parameterization, each serving different scopes as detailed in writing-benchmarks.md (lines 33-45):
[Params]– Field or property parameters with static values[ParamsSource]– Values from a property or method returning anIEnumerable[ParamsAllValues]– All values of anenumorbool[Arguments]– Method-level parameters[ArgumentsSource]– Method parameters from a source property[GenericTypeArguments]– Type arguments for generic benchmarks
Choose the attribute that matches the desired scope to avoid unnecessary benchmark permutations.
Configure the Runtime and Diagnostics
Create a custom configuration by inheriting from ManualConfig to specify runtimes, diagnosers, and output columns:
using BenchmarkDotNet.Columns;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Diagnosers;
public class MyConfig : ManualConfig
{
public MyConfig()
{
AddJob(Job.Default.WithRuntime(CoreRuntime.Core90));
AddDiagnoser(MemoryDiagnoser.Default);
AddColumn(TargetMethodColumn.Instance);
}
}
Apply the configuration using the [Config] attribute or pass it at runtime:
[Config(typeof(MyConfig))]
public class MyBenchmark { /* ... */ }
// Or programmatically:
BenchmarkRunner.Run<MyBenchmark>(new MyConfig());
Configuration alternatives are documented in project-setup-and-running.md (lines 27-52).
Execute Benchmarks Reliably
Always run benchmarks in Release configuration to ensure JIT optimizations are enabled:
dotnet build -c Release
dotnet run -c Release --no-build -- \
--filter "*" \
--noOverwrite \
> benchmark.log 2>&1
Key flags explained:
--filter "*"avoids the interactive prompt ofBenchmarkSwitcher--noOverwriteforces a timestamped subdirectory underBenchmarkDotNet.Artifacts/- Output redirection captures verbose logs for later analysis
Perform a dry-run first to catch compilation or runtime errors without waiting for full execution:
dotnet run -c Release --no-build -- \
--filter "*" \
--job Dry \
--noOverwrite
This dry-run workflow is recommended in project-setup-and-running.md (lines 71-78).
Analyze Results and Export Formats
BenchmarkDotNet generates reports in multiple formats under BenchmarkDotNet.Artifacts/results/:
- Markdown report:
<Benchmark>-report-github.md(optimized for GitHub rendering) - CSV:
<Benchmark>-report.csv(for spreadsheet analysis)
Export additional formats using CLI flags:
--exporters json --exporters markdown
Exporter options are listed in project-setup-and-running.md (lines 62-68).
Understanding BDN Internals and Common Pitfalls
Several internal behaviors affect measurement accuracy, as documented in bdn-internals-and-tuning.md:
Jobs and runtimes execute independent pilot, warm-up, and measurement phases. Changing runtimes via --runtimes net8.0 net9.0 requires a multi-target project file.
MemoryRandomization changes execution behavior significantly. When enabled, [GlobalSetup] and [GlobalCleanup] run for every iteration, drastically increasing total execution time.
IterationSetup overhead forces single-invocation measurement (InvocationCount = 1). Use [GlobalSetup] unless the benchmark method mutates shared state that must be reset between iterations.
Summary
- Create a dedicated console project with the latest
BenchmarkDotNetpackage, using<TargetFrameworks>for multi-runtime comparisons. - Use
BenchmarkSwitcher.FromAssembly(...).Run(args)to respect CLI configuration flags. - Return values from benchmark methods or use
Consumerto prevent dead-code elimination. - Avoid manual loops; let BDN handle invocation counting via
OperationsPerInvokeonly when necessary. - Use
[GlobalSetup]for immutable state and reserve[IterationSetup]for state-mutating benchmarks. - Parameterize using
[Params],[ParamsSource], or[ArgumentsSource]based on scope requirements. - Configure jobs and diagnosers via
ManualConfigfor custom runtime comparisons and memory diagnostics. - Execute with
--filter "*"and--noOverwritein Release configuration, performing dry-runs first to validate code. - Inspect results in the generated Markdown and CSV reports under
BenchmarkDotNet.Artifacts/.
Frequently Asked Questions
How do I prevent the JIT compiler from optimizing away my benchmark code?
Return a value from the benchmark method or pass the result to Consumer. The dotnet/skills documentation in writing-benchmarks.md emphasizes that void methods with no side effects are candidates for dead-code elimination. Consume the result by assigning it to a field or returning it to ensure the operation is measured.
What is the difference between BenchmarkSwitcher and BenchmarkRunner?
BenchmarkSwitcher respects CLI arguments like --filter and --job automatically, while BenchmarkRunner requires explicit forwarding of the args parameter to recognize command-line flags. For automation and CI/CD pipelines, BenchmarkSwitcher is the recommended entry point according to project-setup-and-running.md.
When should I use IterationSetup instead of GlobalSetup?
Use [GlobalSetup] for initialization that can run once per benchmark method, such as creating test data or warming up caches. Use [IterationSetup] only when the benchmark method mutates shared state that must be reset between each invocation. Note that [IterationSetup] forces InvocationCount = 1 and increases measurement overhead significantly.
How do I run benchmarks against multiple .NET runtimes?
Specify multiple target frameworks in the project file using <TargetFrameworks>net8.0;net9.0</TargetFrameworks>, then pass the --runtimes flag to the CLI. BenchmarkDotNet will execute the benchmarks against each specified runtime independently, generating separate results for comparison.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →