Boosting C# Performance: A Guide to BenchmarkDotNet

Evaluating C# code performance

In the fast-paced world of software development, the need to assess the performance of algorithms or compare the efficiency of libraries is a recurring challenge. Developers often find themselves racing against tight deadlines to deliver a certain feature or product on time, so benchmarking is a thing occasionally overlooked. However, investing some extra time might pay off in the future. Therefore, benchmarking can be a valuable addition to our toolkit, especially in scenarios where performance is critical.

In C#, we have BenchmarkDotNet to accomplish this task very precisely. Here is how to use it.

Example

Let's start comparing string concatenation algorithms. As it is well known, in .NET it is always preferred to use the StringBuilder class. Let's see if that is true.

Project setup

First, let's create a Console project and install the required Nuget package.

dotnet add package BenchmarkDotNet

Please, keep in mind that BenchmarkDotNet works only with Console Apps.

Next, we will create our benchmarking class like this:

using System.Text;
using BenchmarkDotNet.Attributes;

namespace Benchmarking;

public class StringConcatenationBenchmark
{
    private const int NumberOfConcatenations = 100;

    [Benchmark]
    public string AdditionAssignment()
    {
        var result = "";
        for (var i = 0; i < NumberOfConcatenations; i++)
        {
            result += i;
        }
        return result;
    }

    [Benchmark]
    public string StringConcat()
    {
        var result = "";
        for (var i = 0; i < NumberOfConcatenations; i++)
        {
            result = string.Concat(result, i);
        }
        return result;
    }

    [Benchmark]
    public string StringBuilder()
    {
        var builder = new StringBuilder();
        for (var i = 0; i < NumberOfConcatenations; i++)
        {
            builder.Append(i);
        }
        return builder.ToString();
    }

    [Benchmark]
    public string StringJoin()
    {
        var list = new List<string>();
        for (var i = 0; i < NumberOfConcatenations; i++)
        {
            list.Add(i.ToString());
        }
        return string.Join("", list);
    }
}

There, we have 4 algorithms to concatenate strings. Note that there is a [Benchmark] annotation in each of them. According to their good practices, it is recommended to use the result of the calculation to avoid dead code elimination by JIT. That is why I am using a string result type.

Finally, in our Program.cs file, we will add the following lines to run the benchmarking:

using BenchmarkDotNet.Running;
using Benchmarking;

var summary = BenchmarkRunner.Run<StringConcatenationBenchmark>();

There are more ways to run it, but the most important to make this work correctly is to build the application in Release mode and not attach the debugger during the benchmarking.

Running the benchmarking

Let's start the project and see what we get in the Console Window.

.NET SDK 8.0.100
  [Host]     : .NET 8.0.0 (8.0.23.53103), X64 RyuJIT AVX2
  DefaultJob : .NET 8.0.0 (8.0.23.53103), X64 RyuJIT AVX2


| Method             | Mean       | Error    | StdDev   |
|------------------- |-----------:|---------:|---------:|
| AdditionAssignment | 1,436.3 ns | 28.58 ns | 34.02 ns |
| StringConcat       | 1,678.7 ns | 32.37 ns | 44.31 ns |
| StringBuilder      |   389.3 ns |  4.66 ns |  4.36 ns |
| StringJoin         |   882.1 ns | 17.22 ns | 17.68 ns |

Alright! Sort of what we expected. The StringBuilder class was the fastest according to the statistics.

Now, it would be helpful to know how they perform in terms of memory allocation. This feature is not enabled by default, but we can do that by adding the [MemoryDiagnoser] annotation to the class.

[MemoryDiagnoser]
public class StringConcatenationBenchmark
{
  ...

Let's run this again.

| Method             | Mean       | Error    | StdDev   | Gen0   | Allocated |
|------------------- |-----------:|---------:|---------:|-------:|----------:|
| AdditionAssignment | 1,429.6 ns | 25.21 ns | 30.96 ns | 1.6613 |  20.37 KB |
| StringConcat       | 1,668.8 ns | 33.09 ns | 50.54 ns | 1.8520 |  22.71 KB |
| StringBuilder      |   383.6 ns |  3.81 ns |  3.56 ns | 0.1016 |   1.25 KB |
| StringJoin         |   889.4 ns | 14.97 ns | 16.64 ns | 0.2069 |   2.54 KB |

Great! StringBuilder also wins in GC and memory allocation.

Final thoughts

BenchmarkDotNet proves to be an invaluable tool to diagnose performance issues in our C# code from different metrics and points of view like execution time and memory allocation. The examples provided are just the tip of the iceberg; BenchmarkDotNet offers numerous features to delve deeper into performance evaluations. Take the time to explore their documentation and unlock the full potential of this powerful performance-testing tool for your C# projects.