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.
Another nice feature 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>
/// The total amount of time taken for all the benchmarks. (Does not include statistic calculation time, or result verification time.)
/// </summary>
/// <remarks>
/// Depending on the number of rounds and time taken for each, this value may not be entirely representful of the actual result, and may have rounded over. It should be used with caution on long-running methods that are run for long amounts of time, though that likely won't be a problem as that would result in the programmer having to wait for it to run. (It would take around 29,247 years for it to wrap around.)
/// </remarks>
public TimeSpan TotalTime { 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;
long totalTime = 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);
totalTime += sw.ElapsedTicks;
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,
TotalTime = new TimeSpan(totalTime),
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;
long totalTime = 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);
totalTime += sw.ElapsedTicks;
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,
TotalTime = new TimeSpan(totalTime),
Variance = new TimeSpan(Convert.ToInt64(variance))
};
}
}
It's literally a one-liner:
var result = BenchmarkResult.Benchmark((ulong)1e7, () => 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
.