ZetCode

C# benchmark

last modified October 5, 2026

In this article we benchmark C# code with the BenchmarkDotNet library.

Benchmarking measures how long code takes to run and how much memory it allocates. It is useful for comparing alternatives, finding regressions, and checking whether an optimization helps on a representative workload.

BenchmarkDotNet is a .NET library that runs methods many times, performs warmups, and reports statistics such as the mean execution time and allocated memory. Always run benchmarks in Release mode and treat the numbers as machine-dependent measurements.

$ dotnet new console -n BenchmarkSample
$ cd BenchmarkSample
$ dotnet add package BenchmarkDotNet
$ dotnet run -c Release

The commands create a console project, add BenchmarkDotNet, and run the benchmark. Replace the generated Program.cs with one of the examples below.

C# string benchmark

The following example compares a StringBuilder with repeated string concatenation. The benchmark methods return the same result so the comparison measures the construction work rather than console output.

Program.cs
using System.Text;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

[MemoryDiagnoser]
public class StringBenchmarks
{
    private const int Count = 10_000;

    [Benchmark(Baseline = true)]
    public string StringBuilder()
    {
        var output = new StringBuilder();

        for (int i = 0; i < Count; i++)
        {
            output.Append("falcon").Append(i);
        }

        return output.ToString();
    }

    [Benchmark]
    public string Concatenation()
    {
        var output = string.Empty;

        for (int i = 0; i < Count; i++)
        {
            output += $"falcon{i}";
        }

        return output;
    }
}

public class Program
{
    public static void Main()
    {
        BenchmarkRunner.Run<StringBenchmarks>();
    }
}

[Benchmark] marks methods that BenchmarkDotNet should measure. The Baseline value makes the first method the reference for the ratio column. [MemoryDiagnoser] adds allocation information to the summary. The runner should be called from a Release build, not from a debugger.

// * Summary *

|           Method | Mean      | Ratio | Allocated |
|----------------- |----------:|------:|----------:|
| StringBuilder    | 120.0 us  |  1.00 |   398 KB  |
| Concatenation    |  1,500 us | 12.50 |  4,000 KB |

The displayed values are representative rather than universal: processor, .NET version, runtime settings, and BenchmarkDotNet version affect every result. In this example, StringBuilder is faster and allocates less memory for the repeated construction.

Benchmarking sorting algorithms

Benchmark setup should not accidentally become part of the measured operation. Here [GlobalSetup] creates one reproducible input. Each benchmark method clones that input before sorting, so both algorithms sort the same values on every invocation.

Program.cs
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

[MemoryDiagnoser]
public class SortBenchmarks
{
    private const int Count = 10_000;
    private int[] original = null!;

    [GlobalSetup]
    public void GlobalSetup()
    {
        original = new int[Count];
        var random = new Random(1234);

        for (int i = 0; i < original.Length; i++)
        {
            original[i] = random.Next();
        }
    }

    [Benchmark(Baseline = true)]
    public void ArraySort()
    {
        var values = (int[]) original.Clone();
        Array.Sort(values);
    }

    [Benchmark]
    public void SelectionSort()
    {
        var values = (int[]) original.Clone();

        for (int i = 0; i < values.Length - 1; i++)
        {
            int minimum = i;

            for (int j = i + 1; j < values.Length; j++)
            {
                if (values[j] < values[minimum])
                {
                    minimum = j;
                }
            }

            (values[i], values[minimum]) = (values[minimum], values[i]);
        }
    }
}

public class Program
{
    public static void Main()
    {
        BenchmarkRunner.Run<SortBenchmarks>();
    }
}

Array.Sort is the baseline because it is a practical library implementation. Selection sort is included for comparison; its quadratic time complexity makes it unsuitable for large arrays. The clone is part of each measured method, but it is the same operation for both algorithms, and it ensures that every invocation starts with unsorted data.

|        Method | Mean       | Ratio | Allocated |
|-------------- |-----------:|------:|----------:|
|     ArraySort |   70.0 us  |  1.00 |   39.1 KB |
| SelectionSort |  45.00 ms  | 642.86 |  39.1 KB |

These output values are illustrative. Run the program on the target machine and compare repeated runs; a benchmark result is not a promise about every computer or workload.

Benchmarking guidelines

Use realistic input sizes, avoid I/O inside benchmark methods, keep setup work out of the measured method, and benchmark in a quiet environment. Check the result for correctness so an optimization cannot succeed by doing less work. BenchmarkDotNet's warnings and generated reports are useful when diagnosing unstable measurements.

Source

BenchmarkDotNet - getting started

BenchmarkDotNet - GitHub repository

StringBuilder - .NET API

In this article we used BenchmarkDotNet to compare C# implementations and to interpret timing and memory results.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all C# tutorials.