Skip to content

Instantly share code, notes, and snippets.

@Sergio0694
Last active March 19, 2020 10:16
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Sergio0694/84c3ae8e7844adf1da723a4a720b0922 to your computer and use it in GitHub Desktop.
Save Sergio0694/84c3ae8e7844adf1da723a4a720b0922 to your computer and use it in GitHub Desktop.
A simple class that can run a benchmark and display its results with a markdown table
using System;
using System.Diagnostics;
using System.Diagnostics.Contracts;
using System.Runtime.CompilerServices;
using System.Text;
#nullable enable
namespace Profiling
{
/// <summary>
/// A simple <see langword="class"/> that runs a benchmark and returns a markdown table with the results.
/// </summary>
/// <remarks>
/// This is a very simplified version of what BenchmarkDotNet does, but with no additional
/// dependencies, so that it can also be used from UWP projects (on .NET Native too).
/// </remarks>
public static class BenchmarkRunner
{
/// <summary>
/// The number of benchmarking runs to run for each benchmark
/// </summary>
private const int NumberOfRuns = 10;
/// <summary>
/// Runs a given benchmark.
/// </summary>
/// <typeparam name="T">The type of benchmark to run.</typeparam>
[Pure]
[MethodImpl(MethodImplOptions.NoInlining)]
public static string Run<T>()
where T : struct, IBenchmark
{
StringBuilder builder = new StringBuilder();
builder.AppendLine("| Test | Mean | Min | Max |");
builder.AppendLine("|-------------:|-------------:|-------------:|-------------:|");
string
name = typeof(T).Name.PadLeft(14),
result = RunCore<T>();
builder.AppendLine($"|{name}|{result}|");
return builder.ToString();
}
/// <summary>
/// Runs a given benchmark.
/// </summary>
/// <typeparam name="T">The type of benchmark to run.</typeparam>
[Pure]
[MethodImpl(MethodImplOptions.NoInlining)]
private static string RunCore<T>()
where T : struct, IBenchmark
{
T runner = default;
GC.Collect();
// Warmup
runner.Run();
GC.Collect();
Span<TimeSpan> times = stackalloc TimeSpan[NumberOfRuns];
Stopwatch timer = new Stopwatch();
foreach (ref TimeSpan time in times)
{
timer.Restart();
runner.Run();
timer.Stop();
time = timer.Elapsed;
}
GC.Collect();
return GetStatisticsForRun(times);
}
/// <summary>
/// Creates a formatted representation of times from a given run result.
/// </summary>
/// <param name="times">The times for a benchmark run.</param>
[Pure]
[MethodImpl(MethodImplOptions.NoInlining)]
private static string GetStatisticsForRun(ReadOnlySpan<TimeSpan> times)
{
long
avg = 0,
min = long.MaxValue,
max = long.MinValue;
foreach (TimeSpan time in times)
{
avg += time.Ticks;
if (time.Ticks < min) min = time.Ticks;
if (time.Ticks > max) max = time.Ticks;
}
avg -= min;
avg -= max;
avg /= times.Length - 2;
string
first = TimeSpan.FromTicks(avg).ToString("m':'s'.'ffffff").PadLeft(14),
second = TimeSpan.FromTicks(min).ToString("m':'s'.'ffffff").PadLeft(14),
third = TimeSpan.FromTicks(max).ToString("m':'s'.'ffffff").PadLeft(14);
return string.Join('|', first, second, third);
}
}
/// <summary>
/// An <see langword="interface"/> for a script runner in a given configuration.
/// </summary>
public interface IBenchmark
{
/// <summary>
/// Runs a given benchmark.
/// </summary>
/// <remarks>You should mark this method as not inlined, for better results.</remarks>
void Run();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment