Jimmy Boinembalome
Let's discover the BenchmarkDotNet package for tracking performance and sharing reproducible measurement experiments.
Benchmarking is a process used to evaluate performance measures. We can take advantage of benchmarking to compare performance between different methods or libraries and determine which areas of our code can be optimized.
In this article, we will create a benchmark in C# to compare the performance between 3 ways to retrieve a element by id in a object list:
foreach
loop.FirstOrDefault()
extension. (Linq)SingleOrDefault()
extension. (Linq)Before to start you will need:
Note: Of course, you can develop with your favourite IDE as well. ๐
BenchmarkDotNet is an open source library that can quickly transform our methods into benchmarks. BenchmarkDotNet does most of the analysis of performance data for us and presents the results in a user-friendly format. In addition to being extremely powerful, BenchmarkDotNet is compatible with applications using the .NET and .NET Core frameworks.
Now we will install BenchmarkDotNet.
dotnet new console --framework net6.0
dotnet add package BenchmarkDotNet --version 0.13.2
dotnet run
If all the steps were successful, you should see on your terminal our fabulous:
Hello, World!
Now that we have successfully created our project and installed the BenchmarkDotNet package, we can create our first benchmark.
As we want to evaluate the performance of 3 ways to get an element by its id, we will add a Person
class which will contain the properties Id
and Name
and a new class called GetByIdBenchmark
with a list of people.
namespace IntroductionToBenchmarkDotNet.Models
{
public class Person
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
}
}
using BenchmarkDotNet.Attributes;
using IntroductionToBenchmarkDotNet.Models;
namespace IntroductionToBenchmarkDotNet.Benchmarks
{
public class GetByIdBenchmark
{
readonly List<Person> people = new();
readonly int id = 1;
[Params(10, 50, 100)]
public int Iterations { get; set; }
[GlobalSetup]
public void GlobalSetup()
{
for (int i = 1; i <= Iterations; i++)
{
var person = new Person
{
Id = i,
Name = $"Name{i}"
};
people.Add(person);
}
}
}
}
Here we have used 2 attributes of BenchmarkDotNet, Params
and GlobalSetup
:
Params
attribute takes as a parameter a set of values. Each value passed as a parameter will be used in a benchmark.GlobalSetup
function allows us to perform an action before each benchmark. This is useful if we want to initialise a variable in the same way for each benchmark without duplicating code.Note: If needed, there is also a
GlobalCleanup
attribute to perform an action after each benchmark. E.g. To dispose an unmanaged resource.
Now let's add in the GetByIdBenchmark
class, the 3 ways to get an element by its id.
[Benchmark]
public Person? Foreach()
{
foreach (var person in people)
{
if (person.Id == id)
return person;
}
return null;
}
[Benchmark]
public Person? FirstOrDefault()
=> people.FirstOrDefault(x => x.Id == id);
[Benchmark]
public Person? SingleOrDefault()
=> people.SingleOrDefault(x => x.Id == id);
This time we have used the Benchmark
attribute. Benchmark
allows us to target the methods that will be run as a benchmark.
We will use the BenchmarkRunner
class to run all the benchmarks that are present in the GetByIdBenchmark
class. To do this, let's update our Program.cs file:
using BenchmarkDotNet.Running;
using IntroductionToBenchmarkDotNet.Benchmarks;
var summary = BenchmarkRunner.Run<GetByIdBenchmark>();
Finally, we will launch the project in release mode with the following command:
dotnet run -c Release
Note:
-c
or--configuration
, sets the build configuration. The default value is Debug. In the context of a benchmark, we use Release mode to improve the execution time of the benchmark, which can run 10 to 100 times slower in Debug mode.
If you did not get an error, you should get the result of the benchmarks according to your machine. For example, below is the result on my machine:
Legends:
If we look at the Mean column in the image above, retrieving an object by its id seems to be faster with a foreach
when our list has 10, 50 or 100 elements. From a technical point of view this makes sense:
SingleOrDefault
method looks through the list to check if the item is unique.FirstOrDefault
method returns the first item found.FirstOrDefault
and foreach
, FirstOrDefault
is slower because the method creates and invokes a delegate
which represents an additional cost.We can also run our benchmarks on multiple frameworks. To do this we need to modify the .csproj file to allow the project to target multiple frameworks.
For example, if we want to target the .NET 6.0 and .NET 7.0 frameworks we need to replace the line:
<TargetFramework>net6.0</TargetFramework>
by:
<TargetFrameworks>net6.0;net7.0</TargetFrameworks>
And add the SimpleJob
attribute above the GetByIdBenchmark
class:
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using IntroductionToBenchmarkDotNet.Models;
namespace IntroductionToBenchmarkDotNet.Benchmarks
{
[SimpleJob(RuntimeMoniker.Net70)]
[SimpleJob(RuntimeMoniker.Net60)]
public class GetByIdBenchmark
...
}
Note: You must also install the SDK if it is not already installed.
After updating your project, you can run it a second time in release mode with the command:
dotnet run -c Release --framework .net7.0
Note: This time we added the --framework .net7.0 parameter to host the console application with the .NET 7.0 framework. However, the application will run benchmarks for both frameworks. ๐
Below is the result on my machine:
BenchmarkDotNet is a package that allows us to easily measure the performance of our code and observe the results with a user-friendly interface. Over 11,000 projects use BenchmarkDotNet, including dotnet/performance (benchmarks for all .NET runtimes), dotnet/runtime (.NET runtime and libraries), Roslyn (C# and Visual Basic compiler), ASP.NET Core, Entity Framework Core, Serilog, Avalonia, RestSharp, MediatR and many others.
During this article, you have had an introduction to the BenchmarkDotNet package. There are many other features offered by the package. If you want to know more, here is the link to the official documentation: BenchmarkDotNet Doc
Source code of the article: Github source code
Thanks for reading! ๐