|
| 1 | +--- |
| 2 | +title: .NET benchmarks |
| 3 | +description: Have you ever wanted to test if a solution or algorithm you've written or refactored is performing faster than the previous iteration? In this post, we'll take a look at how you can use the BenchmarkDotNet library to write benchmarks for your C# code. |
| 4 | +author: bart |
| 5 | +layout: post |
| 6 | +image: assets/images/c-sharp/benchmarks.jpeg |
| 7 | +caption: This image is generated using Dall-E |
| 8 | +prompt: Generate an image of a computer screen with multiple graphs being displayed in a minimalistic flat style |
| 9 | +mermaid: false |
| 10 | +date: 2024-10-16 |
| 11 | +categories: [ c-sharp, benchmark, dotnet ] |
| 12 | +permalink: csharp/benchmarks |
| 13 | +tags: [ .NET, Microsoft, C#, BenchmarkDotNet, Benchmark ] |
| 14 | +related: csharp |
| 15 | +related_to: [ csharp, dotnet ] |
| 16 | +--- |
| 17 | + |
| 18 | +Recently I came across the [BenchmarkDotNet](https://benchmarkdotnet.org/) library, which is an open source library |
| 19 | +maintained by the .NET foundation. |
| 20 | + |
| 21 | +This library allows us to write benchmarks for our own C# code, just by lookin at the readme on Github, it seems like an |
| 22 | +easy to use library. So let's check it out. |
| 23 | + |
| 24 | +## Set up the project |
| 25 | + |
| 26 | +For this post, I'll be using .NET 9 with C# 13 because that's what I've installed on my machine. |
| 27 | +This will, however, also work on other .NET versions or different C# versions. |
| 28 | + |
| 29 | +To get started, we create a new project called `DotNetBenchmarks`. |
| 30 | + |
| 31 | +```shell |
| 32 | +$ dotnet new console -n DotNetBenchmarks |
| 33 | +$ cd DotNetBenchmarks |
| 34 | +``` |
| 35 | + |
| 36 | +You can also find this project |
| 37 | +on [github.com/bartkessels/dotnet-benchmarks](https://github.com/bartkessels/dotnet-benchmarks). |
| 38 | + |
| 39 | +## Add the BenchmarkDotNet package |
| 40 | + |
| 41 | +Next, we can add the [BenchmarkDotNet](https://www.nuget.org/packages/BenchmarkDotNet/) Nuget package to our project. |
| 42 | + |
| 43 | +```shell |
| 44 | +$ dotnet add package BenchmarkDotNet |
| 45 | +``` |
| 46 | + |
| 47 | +And that's all for the installation part. |
| 48 | + |
| 49 | +## Writing our first benchmark |
| 50 | + |
| 51 | +Before we create our first benchmark, let's first think of a use case where we can benchmark multiple implementations. |
| 52 | +Let's create a small method that adds multiple numbers. |
| 53 | +Yes, I hear you think how is this usefull for two methods? Well, we'll make one method that uses a for loop with a |
| 54 | +counter and another method that uses LINQ. |
| 55 | + |
| 56 | +```csharp |
| 57 | +internal class Calculator |
| 58 | +{ |
| 59 | + internal int AddUsingLinq(int[] numbers) |
| 60 | + { |
| 61 | + return numbers.Sum(); |
| 62 | + } |
| 63 | + |
| 64 | + internal int AddUsingForLoop(int[] numbers) |
| 65 | + { |
| 66 | + var sum = 0; |
| 67 | + |
| 68 | + for (var i = 0; i < numbers.Length; i++) |
| 69 | + { |
| 70 | + sum += numbers[i]; |
| 71 | + } |
| 72 | + |
| 73 | + return sum; |
| 74 | + } |
| 75 | +} |
| 76 | +``` |
| 77 | + |
| 78 | +To be completely honest, I don't have any expectations on which method will be faster, or what the actual difference between the two will be. |
| 79 | +Let's just try it out. |
| 80 | + |
| 81 | +## Benchmarking the methods |
| 82 | + |
| 83 | +Now that our methods have been written, we simply add the `[Benchmark]` attribute to the methods we want to benchmark. |
| 84 | + |
| 85 | +```csharp |
| 86 | +internal class CalculatorBenchmark |
| 87 | +{ |
| 88 | + private readonly Calculator _calculator = new(); |
| 89 | + |
| 90 | + [Benchmark] |
| 91 | + internal void AddUsingLinq() |
| 92 | + { |
| 93 | + int[] numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; |
| 94 | + _calculator.AddUsingLinq(numbers); |
| 95 | + } |
| 96 | + |
| 97 | + [Benchmark] |
| 98 | + internal void AddUsingForLoop() |
| 99 | + { |
| 100 | + int[] numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; |
| 101 | + _calculator.AddUsingForLoop(numbers); |
| 102 | + } |
| 103 | +} |
| 104 | +``` |
| 105 | + |
| 106 | +Next we need to register our benchmark class to the benchmark runner in our `Program.cs` file. |
| 107 | + |
| 108 | +```csharp |
| 109 | +BenchmarkRunner.Run<CalculatorBenchmark>(); |
| 110 | +``` |
| 111 | + |
| 112 | +Now, let's dive into the code behind the `BenchmarkRunner.Run` method and see how it knows what methods we have declared as benchmarks. |
| 113 | + |
| 114 | +Let's start by opening the `BenchmarkRunner.Run` method in Github, after a little digging I found in the [BenchmarkRunnerDirty](https://github.com/dotnet/BenchmarkDotNet/blob/9040e40187f2bbecea4aec724f995fde378f608b/src/BenchmarkDotNet/Running/BenchmarkRunnerDirty.cs#L21) file. |
| 115 | +All this method does, is actually calling the `BenchmarkRunnerClean ` method, which in turn uses the `BenchMarkConverter` to retrieve all methods that have the `Benchmark` attribute, see [BenchmarkConverter.cs; line35](https://github.com/dotnet/BenchmarkDotNet/blob/9040e40187f2bbecea4aec724f995fde378f608b/src/BenchmarkDotNet/Running/BenchmarkConverter.cs#L35). |
| 116 | + |
| 117 | +Then for each method it finds, it will execute it and temporarily save the results, see [BenchmarkRunnerClean.cs; line 121](https://github.com/dotnet/BenchmarkDotNet/blob/9040e40187f2bbecea4aec724f995fde378f608b/src/BenchmarkDotNet/Running/BenchmarkRunnerClean.cs#L121) |
| 118 | + |
| 119 | +### Running the benchmarks |
| 120 | + |
| 121 | +Now we know a bit more on how the benchmarks are run, let's actually run our own benchmarks. It's important to know that it's highly recommended to run the benchmarks in release mode, as debug mode probably impacts the runtime more than you'd like [(Dotnet Foundation and members, n.d.)](https://benchmarkdotnet.org/articles/guides/good-practices.html#use-the-release-build-without-an-attached-debugger). |
| 122 | +If for some reason, you run it using debug mode, you get the following message and your benchmarks won't run. |
| 123 | + |
| 124 | +``` |
| 125 | +// Validating benchmarks: |
| 126 | +// * Assembly DotNetBenchmarks which defines benchmarks is non-optimized |
| 127 | +Benchmark was built without optimization enabled (most probably a DEBUG configuration). Please, build it in RELEASE. |
| 128 | +If you want to debug the benchmarks, please see https://benchmarkdotnet.org/articles/guides/troubleshooting.html#debugging-benchmarks. |
| 129 | +``` |
| 130 | + |
| 131 | +To run our benchmarks, we just call `dotnet run` and set the configuration flag to `Release`. |
| 132 | + |
| 133 | +```shell |
| 134 | +$ dotnet run -c Release |
| 135 | +``` |
| 136 | + |
| 137 | +After less than two minutes, I got the following result (this may differ for your machine). |
| 138 | + |
| 139 | +``` |
| 140 | +| Method | Mean | Error | StdDev | |
| 141 | +|---------------- |---------:|---------:|---------:| |
| 142 | +| AddUsingLinq | 14.50 ns | 0.203 ns | 0.169 ns | |
| 143 | +| AddUsingForLoop | 15.01 ns | 0.365 ns | 1.054 ns | |
| 144 | +``` |
| 145 | + |
| 146 | +As a small extra, we not only know how to write benchmarks for our own classes or methods, we also know that LINQ is faster in summing up numbers than writing your own for-loop. |
0 commit comments