Run benchmarks using multiple runtimes with BenchmarkDotNet
It's never been easier to run benchmarks against multiple runtimes with BenchmarkDotNet. Maybe you're checking your performance improvements benefit all runtimes targeted by your library code (that may support multiple target frameworks). Maybe you're curious to see if the relentless march of runtime improvements will benefit some of your existing code? Whatever the reason, with a couple of lines of code you can generate benchmarks like this:
I first saw this technique explained in the performance improvements in .NET 5 post which made it easier to understand than the toolchains documentation. The rest of this post doesn't introduce anything not already covered in the post/docs, but might be easier to digest.
Setup
We're going to compare the performance of String.StartsWith
to Span.StartsWith
under:
- .NET Framework 4.8 (download developer pack)
- .NET Core 3.1 (download SDK)
- .NET 5 (download SDK)
We'll then create a new console app for our benchmarks:
dotnet new console --name spanBench
This bootstraps a new console application in the spanBench
folder.
Project File
We're going to make three changes to our spanBench.csproj
file:
- Add the additional target frameworks (using the appropriate Target Framework Moniker for each, see target frameworks)
- Add the BenchmarkDotNet dependency
- Conditionally add the System.Memory package when targeting .NET Framework 4.8 (the
System.Span
type is included in the runtime from .NET Core 2.1)
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net5.0;netcoreapp3.1;net48</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.13.0" />
</ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' == 'net48' ">
<PackageReference Include="System.Memory" Version="4.5.4" />
</ItemGroup>
</Project>
Benchmark Code
The code is configured to use BenchmarkSwitcher, which allows you to specify which benchmarks to run via the command line, or interactively (if no filter
argument is passed). We've also added the MemoryDiagnoser
attribute to collect information about allocations. The benchmarks are fairly simple and will each be executed with two different arguments.
using System;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using BenchmarkDotNet.Diagnosers;
namespace SpanPerf
{
[MemoryDiagnoser]
public class Program
{
static void Main(string[] args) => BenchmarkSwitcher.FromAssemblies(new[] { typeof(Program).Assembly }).Run(args);
[Arguments("http://www.google.com")]
[Arguments("https://www.google.com")]
[Benchmark]
public bool String_StartsWith(string str)
{
return str.StartsWith("https://");
}
[Arguments("http://www.google.com")]
[Arguments("https://www.google.com")]
[Benchmark]
public BotDetails Span_StartsWith(string str)
{
return str.AsSpan().StartsWith("https://".AsSpan());
}
}
}
Running the benchmark
With everything in place the below command will run all 12 benchmarks (2 methods x 2 arguments x 3 runtimes):
dotnet run --configuration Release --framework net48 --runtimes net48 netcoreapp31 netcoreapp50 --filter * --join
The first two arguments are for the dotnet run
command:
- Use the release configuration (never benchmark with debug!)
- Build using the .NET Framework 4.8 surface area
The final three arguments are forwarded to BenchmarkDotNet's Run
method:
runtimes
specifies the runtimes we want to benchmark withfilter
in this case specifies every benchmark in the assemblyjoin
outputs all results at the end, rather than after each individual benchmark