# How to Configure BenchmarkDotNet for Accurate Microbenchmarking in .NET

> Master accurate .NET microbenchmarking with BenchmarkDotNet. Learn console project setup, entry points, value returning, and configuration for reliable performance measurements.

- Repository: [.NET Platform/skills](https://github.com/dotnet/skills)
- Tags: how-to-guide
- Published: 2026-07-07

---

**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`](https://github.com/dotnet/skills/blob/main/writing-benchmarks.md), [`project-setup-and-running.md`](https://github.com/dotnet/skills/blob/main/project-setup-and-running.md), and [`bdn-internals-and-tuning.md`](https://github.com/dotnet/skills/blob/main/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.

```bash
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>`:

```xml
<TargetFrameworks>net8.0;net9.0</TargetFrameworks>

```

This multi-targeting approach is documented in [`project-setup-and-running.md`](https://github.com/dotnet/skills/blob/main/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:

```csharp
using BenchmarkDotNet.Running;

BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args);

```

**Use `BenchmarkRunner`** only when you explicitly forward arguments, otherwise it ignores CLI flags:

```csharp
BenchmarkRunner.Run<MyBenchmark>(args: args);

```

As noted in [`project-setup-and-running.md`](https://github.com/dotnet/skills/blob/main/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`](https://github.com/dotnet/skills/blob/main/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:

```csharp
// 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:

```csharp
// 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:

```csharp
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`](https://github.com/dotnet/skills/blob/main/writing-benchmarks.md) (lines 33-45):

- **`[Params]`** – Field or property parameters with static values
- **`[ParamsSource]`** – Values from a property or method returning an `IEnumerable`
- **`[ParamsAllValues]`** – All values of an `enum` or `bool`
- **`[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:

```csharp
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:

```csharp
[Config(typeof(MyConfig))]
public class MyBenchmark { /* ... */ }

// Or programmatically:
BenchmarkRunner.Run<MyBenchmark>(new MyConfig());

```

Configuration alternatives are documented in [`project-setup-and-running.md`](https://github.com/dotnet/skills/blob/main/project-setup-and-running.md) (lines 27-52).

## Execute Benchmarks Reliably

Always run benchmarks in **Release** configuration to ensure JIT optimizations are enabled:

```bash
dotnet build -c Release
dotnet run -c Release --no-build -- \
    --filter "*" \
    --noOverwrite \
    > benchmark.log 2>&1

```

**Key flags explained:**
- `--filter "*"` avoids the interactive prompt of `BenchmarkSwitcher`
- `--noOverwrite` forces a timestamped subdirectory under `BenchmarkDotNet.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:

```bash
dotnet run -c Release --no-build -- \
    --filter "*" \
    --job Dry \
    --noOverwrite

```

This dry-run workflow is recommended in [`project-setup-and-running.md`](https://github.com/dotnet/skills/blob/main/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:

```bash
--exporters json --exporters markdown

```

Exporter options are listed in [`project-setup-and-running.md`](https://github.com/dotnet/skills/blob/main/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`](https://github.com/dotnet/skills/blob/main/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 `BenchmarkDotNet` package, using `<TargetFrameworks>` for multi-runtime comparisons.
- Use `BenchmarkSwitcher.FromAssembly(...).Run(args)` to respect CLI configuration flags.
- Return values from benchmark methods or use `Consumer` to prevent dead-code elimination.
- Avoid manual loops; let BDN handle invocation counting via `OperationsPerInvoke` only 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 `ManualConfig` for custom runtime comparisons and memory diagnostics.
- Execute with `--filter "*"` and `--noOverwrite` in 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`](https://github.com/dotnet/skills/blob/main/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`](https://github.com/dotnet/skills/blob/main/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.