using System; using System.Collections.Generic; using System.Diagnostics; namespace Amphora.PerformanceTests { public struct Timing { public double MedianNanoseconds; public double MinimumNanoseconds; public double BytesPerCall; } public static class Measurement { public const double WarmUpMilliseconds = 100; public const double BatchMilliseconds = 5; /// /// Odd, so the median is a sample rather than an average of two. /// public const int Rounds = 31; public const int QuickRounds = 7; public static double NanosecondsFor(long ticks) { return ticks * 1e9 / Stopwatch.Frequency; } public static double Median(List samples) { var sorted = new List(samples); sorted.Sort(); return sorted[sorted.Count / 2]; } public static double Minimum(List samples) { double returnMe = double.MaxValue; foreach (double sample in samples) { returnMe = Math.Min(returnMe, sample); } return returnMe; } /// /// Runs the call long enough to JIT it and settle the caches, and returns how many calls fill one batch. /// public static int WarmUp(Action call) { long budget = (long)(WarmUpMilliseconds * Stopwatch.Frequency / 1000); long start = Stopwatch.GetTimestamp(); long calls = 0; long elapsed; do { call(); ++calls; elapsed = Stopwatch.GetTimestamp() - start; } while (elapsed < budget); double millisecondsPerCall = elapsed * 1000.0 / Stopwatch.Frequency / calls; return Math.Max(1, (int)(BatchMilliseconds / millisecondsPerCall)); } public static double NanosecondsPerCall(Action call, int calls) { long start = Stopwatch.GetTimestamp(); for (int i = 0; i < calls; ++i) { call(); } return NanosecondsFor(Stopwatch.GetTimestamp() - start) / calls; } public static double BytesPerCall(Action call, int calls) { long before = GC.GetAllocatedBytesForCurrentThread(); for (int i = 0; i < calls; ++i) { call(); } return (double)(GC.GetAllocatedBytesForCurrentThread() - before) / calls; } /// /// Times each call once warm, one batch at a time, taking turns. /// public static Timing[] TimeTakingTurns(IReadOnlyList calls, int rounds) { var callsPerBatch = new int[calls.Count]; var samples = new List[calls.Count]; for (int call = 0; call < calls.Count; ++call) { callsPerBatch[call] = WarmUp(calls[call]); samples[call] = new List(rounds); } // Each round starts from a different call, so drift in clock speed as the CPU heats up lands on every call // alike rather than on whichever ran last. for (int round = 0; round < rounds; ++round) { for (int turn = 0; turn < calls.Count; ++turn) { int call = (round + turn) % calls.Count; samples[call].Add(NanosecondsPerCall(calls[call], callsPerBatch[call])); } } var returnMe = new Timing[calls.Count]; for (int call = 0; call < calls.Count; ++call) { returnMe[call] = new Timing { MedianNanoseconds = Median(samples[call]), MinimumNanoseconds = Minimum(samples[call]), BytesPerCall = BytesPerCall(calls[call], callsPerBatch[call]), }; } return returnMe; } } }