Benchmarks in .NET Core
Reading Time: 5 minutes
Why Benchmarking Matters
Performance discussions in .NET often start with good intentions and end with guesses.
Typical examples:
- “This LINQ query feels slower”
- “Span
should be faster here” - “I replaced this code with a manual loop and it seems better”
The problem is that performance intuition is often wrong.
Modern .NET has:
- JIT optimizations
- tiered compilation
- allocation effects
- CPU cache behavior
- runtime-specific optimizations
That means the only reliable way to compare two implementations is to measure them properly.
The Wrong Way to Benchmark
A lot of developers start with Stopwatch around ad hoc code.
var sw = Stopwatch.StartNew();
DoWork();
sw.Stop();
Console.WriteLine(sw.ElapsedMilliseconds);
This is usually not enough for trustworthy benchmarking because it ignores:
- warmup effects
- JIT compilation
- multiple iterations
- GC pressure
- process/environment noise
For quick diagnostics, Stopwatch can be useful. For real comparison work, use a benchmarking framework.
The Right Tool: BenchmarkDotNet
In the .NET ecosystem, BenchmarkDotNet is the standard tool for microbenchmarks.
It handles many important details for you:
- warmup
- repeated iteration
- statistical analysis
- memory allocation reporting
- runtime comparisons
- disassembly and diagnostics support
Basic Setup
Install the package:
dotnet add package BenchmarkDotNet
Minimal example
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
BenchmarkRunner.Run<StringBenchmarks>();
[MemoryDiagnoser]
public class StringBenchmarks
{
private readonly string[] _values = Enumerable.Range(1, 1000)
.Select(x => x.ToString())
.ToArray();
[Benchmark]
public string JoinWithStringJoin()
{
return string.Join(',', _values);
}
[Benchmark]
public string JoinWithBuilder()
{
var sb = new StringBuilder();
for (int i = 0; i < _values.Length; i++)
{
if (i > 0)
sb.Append(',');
sb.Append(_values[i]);
}
return sb.ToString();
}
}
Run the benchmark and BenchmarkDotNet will produce structured output showing timing and memory differences.
What BenchmarkDotNet Tells You
Typical metrics include:
- Mean: average execution time
- Error / StdDev: how stable the measurements are
- Allocated: memory usage per operation
- Ratio: relative speed compared to a baseline
This is much more useful than a single elapsed number from Stopwatch.
Add a Baseline
A useful technique is marking one implementation as the baseline.
[Benchmark(Baseline = true)]
public string OriginalImplementation()
{
return string.Join(',', _values);
}
[Benchmark]
public string NewImplementation()
{
return string.Concat(_values);
}
This makes it easier to compare the new code against the existing implementation.
Benchmarking Allocations Matters Too
Sometimes a method is only slightly faster, but allocates much more memory.
That matters in real applications because allocation pressure affects:
- GC frequency
- latency
- throughput under load
That is why [MemoryDiagnoser] is so valuable.
Common Benchmark Scenarios in .NET
Typical things worth benchmarking:
- string handling
- collection iteration
- LINQ vs manual loops
- parsing and serialization
- JSON libraries
- caching strategies
- object allocation patterns
Span<T>and buffer optimizations
The key is to benchmark something where a performance decision is actually being made.
Common Benchmarking Mistakes
1. Benchmarking unrealistic code
If the test input does not look like production input, results may be meaningless.
Bad example:
- benchmarking a parser with a 5-character string when production inputs are 20 KB payloads
2. Optimizing before identifying a bottleneck
Not everything needs a benchmark.
Benchmarking is most useful when:
- you already found a hotspot
- you are choosing between concrete implementations
- performance is a real requirement
3. Drawing conclusions from tiny differences
If one version is 1-2% faster but harder to read and maintain, that may not be a worthwhile tradeoff.
4. Ignoring allocations
A faster mean with much higher memory allocation can be worse in real systems.
5. Benchmarking too much inside one method
A benchmark should isolate the thing you want to compare.
If it includes setup, IO, logging, and unrelated work, it becomes harder to reason about the result.
Use Params for Realistic Comparisons
BenchmarkDotNet supports varying inputs with [Params].
[MemoryDiagnoser]
public class SearchBenchmarks
{
private int[] _data = Array.Empty<int>();
[Params(100, 1000, 100000)]
public int Size;
[GlobalSetup]
public void Setup()
{
_data = Enumerable.Range(1, Size).ToArray();
}
[Benchmark]
public bool LinqContains()
{
return _data.Contains(Size - 1);
}
[Benchmark]
public bool ManualLoop()
{
foreach (var value in _data)
{
if (value == Size - 1)
return true;
}
return false;
}
}
This helps avoid conclusions based on only one input size.
Use GlobalSetup for Shared Preparation
If the data preparation is not part of what you want to measure, move it into [GlobalSetup].
This keeps the benchmark focused on the actual operation.
[GlobalSetup]
public void Setup()
{
_items = CreateLargeDataset();
}
Comparing Runtimes
BenchmarkDotNet can also compare runtimes when needed.
This is useful when testing differences across:
- .NET 8
- .NET 9
- .NET 10 previews or newer runtimes
This helps answer questions like:
- Did the runtime itself get faster?
- Is this optimization still necessary on newer .NET?
Benchmarks vs Profiling
Benchmarks and profilers solve different problems.
Benchmarking
Use when you want to compare isolated implementations.
Profiling
Use when you want to find where time is going in a real application.
A practical workflow is:
- Profile the application to find hotspots
- Extract candidate code into benchmarkable units
- Benchmark competing implementations
- Apply the winning change only if it is worth it
Interpreting Results Pragmatically
The fastest code is not always the best code.
Ask:
- Is the improvement meaningful in the actual system?
- Does it reduce allocations or just shift them?
- Is the result stable across realistic inputs?
- Is the extra complexity justified?
Performance work should improve the system, not just the benchmark table.
Summary
Benchmarking in .NET Core is about making performance decisions with evidence instead of intuition.
The practical default is:
- use BenchmarkDotNet for microbenchmarks
- measure both execution time and allocations
- benchmark realistic workloads
- isolate the code you actually want to compare
- treat small wins skeptically if they add complexity
If performance matters, benchmark first, optimize second, and keep the result in context of the real application.
References
- BenchmarkDotNet documentation: https://benchmarkdotnet.org/
- .NET diagnostics overview: https://learn.microsoft.com/dotnet/core/diagnostics/
- Write microbenchmarks with BenchmarkDotNet: https://benchmarkdotnet.org/articles/guides/getting-started.html