One codebase, three runtimes
Comparing performance across .NET versions to make data-driven upgrade decisions.
"Will upgrading to .NET 10 actually make our code faster?"
For most teams, the answer to this question is a guess based on a blog post or a release note. But for a performance-critical application, "it should be faster" is not a technical justification for an upgrade. You need an empirical answer.
Multi-runtime support allows you to run the exact same benchmark suite across multiple target frameworks - such as .net8.0, .net9.0, and .net10.0 - and compare the results side-by-side.
Setting up a multi-runtime project
To compare runtimes, you first need a project that can target all of them. In your .csproj file, replace the single <TargetFramework> tag with <TargetFrameworks>, listing the versions you want to compare:
<PropertyGroup>
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
</PropertyGroup>
Once your project is multi-targeted, NBenchmark can orchestrate the execution across the different installed runtimes on your machine.
Configuring the runtime sweep
Depending on your usage mode, there are different ways to trigger a runtime comparison.
In Harness mode
The simplest way is via the CLI. You can specify the runtimes you want to target using the --runtimes flag:
dotnet benchmark --runtimes net8,net9,net10
Alternatively, you can use the [Runtimes] attribute on your benchmark class to pin the target versions directly in the code.
In Suite mode
When building a suite programmatically, you use the WithRuntimes(...) method. However, because each runtime is its own process by definition, you cannot use a simple inline suite. Instead, you must use a [BenchmarkPlan] factory and RunPlanAsync.
This ensures that the engine can spawn a worker process for each specified runtime, apply the correct RuntimeProfile, and collect the results in a unified report.
Reading the cross-runtime report
The output of a multi-runtime run is grouped by runtime. You will see a separate results table for .net8.0, .net9.0, and .net10.0.
The most important rule of cross-runtime reporting is this: significance and ratios are only calculated within a single runtime.
You will see a Sig column for the comparisons within .net9.0, but you will never see a significance test that spans from .net8.0 to .net9.0. Why? Because the baseline shifted. The hardware might be the same, but the JIT compiler, the GC implementation, and the BCL internals have changed. Comparing a median from one runtime to another is like comparing temperatures in Celsius and Fahrenheit - the numbers are different, but they are measuring different things.
To make a decision, you look at the Relative Magnitude. If Implementation A is 20% faster than B in .net8.0, but only 5% faster in .net10.0, the new runtime has effectively erased your optimization.
A data-driven upgrade workflow
The most effective way to use multi-runtime support is as a three-step workflow:
- Pin the Suite: Select a representative set of benchmarks that cover your most critical hot paths.
- Sweep Runtimes: Run the suite across your current version and the target upgrade version.
- Analyze the Delta: Use the
SigandMagnitudecolumns to identify where the new runtime provides a win and where it introduces a regression.
This turns the upgrade conversation from "should we?" to "here is exactly how much faster we will be."
Go deeper: Most of the time, the built-in reporters and statistical tests are enough. But what if you need to stream your results to OpenTelemetry or implement a custom outlier detector for a specific hardware glitch?