Skip to main content
1 of 5
Der Kommissar
  • 20.1k
  • 4
  • 68
  • 158

Benchmarking things in C#

I needed a better way to benchmark code, because, well, rewriting the same benchmarking code every time I need it is just...well...unpleasant.

So, here's a class which does just that, it runs an Action over a specific number of rounds, and calculates certain stats on it.

The nice thing about this is that it doesn't store the run times as it calculates the stats. So you can literally supply any value for rounds and it should work. (Not tested for rounds values greater than 10,000,000.)

The latest version is on GitHub.

It's pretty simple. Two static methods on a simple class that run the benchmark.

The nice thing about this class is it includes a version for Func<T> as well, which will also verify the output of the function. This means you can benchmark and verify your code at the same time, to make sure that nothing weird happens.

/// <summary>
/// Represents the result of a benchmarking session.
/// </summary>
public class BenchmarkResult
{
    /// <summary>
    /// The total number of rounds ran.
    /// </summary>
    public ulong RoundsRun { get; set; }

    /// <summary>
    /// The average time for all the rounds.
    /// </summary>
    public TimeSpan AverageTime { get; set; }

    /// <summary>
    /// The maximum time taken for a single round.
    /// </summary>
    public TimeSpan MaxTime { get; set; }

    /// <summary>
    /// The minimum time taken for a single round.
    /// </summary>
    public TimeSpan MinTime { get; set; }

    /// <summary>
    /// The variance (standard deviation) of all the rounds.
    /// </summary>
    public TimeSpan Variance { get; set; }

    /// <summary>
    /// The number of rounds that passed testing. (Always equivalent to <see cref="RoundsRun"/> for <see cref="Benchmark(ulong, Action)"/>.)
    /// </summary>
    public ulong RoundsPassed { get; set; }
    
    /// <summary>
    /// Runs a benchmark of a method.
    /// </summary>
    /// <param name="rounds">The number of rounds to run.</param>
    /// <param name="method">The code to run.</param>
    /// <returns>A <see cref="BenchmarkResult"/> representing the result of the session.</returns>
    public static BenchmarkResult Benchmark(ulong rounds, Action method)
    {
        var sw = new Stopwatch();

        double m2 = 0;
        double averageTicks = 0;
        double totalValues = 0;
        long maxTicks = 0;
        long minTicks = 0;

        for (ulong i = 0; i < rounds; i++)
        {
            sw.Start();
            method.Invoke();
            sw.Stop();

            if (totalValues == 0)
            {
                maxTicks = sw.ElapsedTicks;
                minTicks = sw.ElapsedTicks;
            }

            totalValues++;

            maxTicks = Math.Max(sw.ElapsedTicks, maxTicks);
            minTicks = Math.Min(sw.ElapsedTicks, minTicks);

            // We need to store `delta` here as the `averageTicks` will change on the next calculation, and we need this previous `delta` for the calculation after it.
            double delta = sw.ElapsedTicks - averageTicks;
            averageTicks = averageTicks + delta / totalValues;
            m2 += delta * (sw.ElapsedTicks - averageTicks);

            sw.Reset();
        }

        double variance = m2 / (totalValues - 1);

        return new BenchmarkResult
        {
            AverageTime = new TimeSpan(Convert.ToInt64(averageTicks)),
            MaxTime = new TimeSpan(maxTicks),
            MinTime = new TimeSpan(minTicks),
            RoundsPassed = rounds,
            RoundsRun = rounds,
            Variance = new TimeSpan(Convert.ToInt64(variance))
        };
    }

    /// <summary>
    /// Runs a benchmark of a function and returns the results of the session.
    /// </summary>
    /// <typeparam name="T">The type of the output of the function.</typeparam>
    /// <param name="rounds">The number of rounds to run.</param>
    /// <param name="method">The code to run.</param>
    /// <param name="expectedResult">The expected result of the function. This will be compared to the actual result and used for <see cref="BenchmarkResult.RoundsPassed"/>. This uses the default <code>object.Equals(object)</code> method.</param>
    /// <returns>A <see cref="BenchmarkResult"/> representing the result of the session.</returns>
    public static BenchmarkResult Benchmark<T>(ulong rounds, Func<T> method, T expectedResult)
    {
        var sw = new Stopwatch();

        double m2 = 0;
        double averageTicks = 0;
        double totalValues = 0;
        long maxTicks = 0;
        long minTicks = 0;
        ulong roundsPassed = 0;

        for (ulong i = 0; i < rounds; i++)
        {
            sw.Start();
            var result = method.Invoke();
            sw.Stop();

            if (expectedResult.Equals(result))
            {
                roundsPassed++;
            }

            if (totalValues == 0)
            {
                maxTicks = sw.ElapsedTicks;
                minTicks = sw.ElapsedTicks;
            }

            totalValues++;

            maxTicks = Math.Max(sw.ElapsedTicks, maxTicks);
            minTicks = Math.Min(sw.ElapsedTicks, minTicks);

            // We need to store `delta` here as the `averageTicks` will change on the next calculation, and we need this previous `delta` for the calculation after it.
            double delta = sw.ElapsedTicks - averageTicks;
            averageTicks = averageTicks + delta / totalValues;
            m2 += delta * (sw.ElapsedTicks - averageTicks);

            sw.Reset();
        }

        double variance = m2 / (totalValues - 1);

        return new BenchmarkResult
        {
            AverageTime = new TimeSpan(Convert.ToInt64(averageTicks)),
            MaxTime = new TimeSpan(maxTicks),
            MinTime = new TimeSpan(minTicks),
            RoundsPassed = roundsPassed,
            RoundsRun = rounds,
            Variance = new TimeSpan(Convert.ToInt64(variance))
        };
    }
}

It's literally a one-liner:

var result = BenchmarkResult.Benchmark(10000000, () => Thread.Sleep(0));
result = BenchmarkResult.Benchmark(10000000, () => true, true);

That will benchmark the Thread.Sleep(0) method 10,000,000 times, and the return true method 10,000,000 times while verifying it always returns true.

Der Kommissar
  • 20.1k
  • 4
  • 68
  • 158