Thursday 12 June 2008

Extending Stopwatch: Making it easier to time your code

If ever you look on the Project Euler forums (which, cleverly, you can't access until you've submitted your own answer to a problem), you'll notice a fair bit of bragging about how fast each contributor's solution is. Now I'm not aiming to arrive at the quickest solution (that's my excuse anyway!), but it would be interesting to know if any of my code comes anywhere close.

Since .Net 2.0, we've had the Stopwatch class, which makes it very easy to time code. No more totalTime = DateTime.Now - timeThen! Under the covers, Stopwatch can use native high-resolution performance counters, so it gives pretty accurate results.

You'd think that you couldn't get easier-to-use than the Stopwatch API: Stop(), Start(), Reset(), Elapsed give you pretty much everything you need. But now, thanks to Extension methods and lambda expressions, we can go one better. Here are some extensions I knocked up for Stopwatch which make it even easier to time code. The first one, Time is an example of what you might call a Polo method - a method with a hole in! The "hole" is the bit where your code goes - in this case, the code you want to time. The place of the mint is taken by the generic code around the hole that does the timing. Of course, the "hole" is implemented using delegates, in this case an Action delegate, that takes no arguments and returns no result.

public static class StopwatchExtensions
{
    /// <summary>
    /// Extends the Stopwatch class with a method to time an Action delegate over a specified number of iterations
    /// </summary>
    public static Stopwatch Time(this Stopwatch stopwatch, Action action, long numberOfIterations)
    {
        stopwatch.Reset();
        stopwatch.Start();

        for (int i = 0; i < numberOfIterations; i++)
        {
            action();
        }

        stopwatch.Stop();
        return stopwatch;
    }

    /// <summary>
    /// Extends the Stopwatch class with a method to time an Action delegate
    /// </summary>
    /// <param name=\"stopwatch\"></param>
    /// <param name=\"action\"></param>
    public static Stopwatch Time(this Stopwatch stopwatch, Action action)
    {
        return stopwatch.Time(action, 1);
    }

    public static double TimeInFractionalSeconds(this Stopwatch stopwatch)
    {
        // figure out how much of a second a Stopwatch tick represents
        double secondsPerTick = (double)1 / Stopwatch.Frequency;

        return stopwatch.ElapsedTicks*secondsPerTick;
    }
}

This makes timing a method as easy as:

double time = new Stopwatch().Time(MyMethod).TimeInFractionalSeconds();

or

double time = new Stopwatch().Time(()=> Math.Sqrt(int.MaxValue)).TimeInFractionalSeconds();

As you know, in order to get truly representative timings, it's usually necessary to repeat the measurement several times and then take an average - things like JITting the code, your computer getting distracted by a YouTube video, or the phase of the moon could all contribute to individual timings being out.

For my Project Euler project, I've put together a little helper method that takes care of the averaging over several iterations. The ActionTimer first runs a delegate once to get an idea for how quick it is. You can specify how long you think the code needs to run for in total to get an accurate time. If a single iteration takes longer than this it just reports that as the time - this is useful for long-running code where you assume that the actual run-time exceeds any overheads; otherwise it calculates how many iterations are needed to make the code run for the target time you specified. Once it has done those, it returns the average per iteration.

/// <summary>
/// Times how long a given delegate takes to run
/// </summary>
public static class ActionTimer
{
    /// <summary>
    /// Times the given delegate
    /// </summary>
    /// <param name=\"action\">The delegate to time</param>
    /// <param name=\"targetRunTime\">The target amount of time to average over (in seconds)</param>
    /// <returns>The running time, in fractional seconds</returns>
    public static double AverageTime(this Action action, double targetRunTime)
    {
        if (targetRunTime <= 0)
        {
            throw new ArgumentException(\"targetRunTime must be greater than 0.\", \"targetRunTime\");
        }

        Stopwatch timer = new Stopwatch();
      

        // time the action once to see how fast it is
        timer.Time(action);

        // if the action took more than half of the targetRunTime time, we\'re not going to
        // fit in a second iteration
        double firstIterationTime = timer.TimeInFractionalSeconds();
        if (firstIterationTime > targetRunTime/2)
        {
            return firstIterationTime;
        }

        // otherwise, repeat the action to get an accurate timing
        // aim for targetRunTime seconds of total running time
        long numberOfIterations = (long) (targetRunTime/firstIterationTime);

        // the number of iterations should be at least 1 because firstIterationTime is less than half of
        // targetRunTime
        Debug.Assert(numberOfIterations > 0);

        timer.Time(action, numberOfIterations);
      
        // calculate the length of time per iteration
        return timer.TimeInFractionalSeconds() / numberOfIterations ;
    }

    /// <summary>
    /// Times how long the given delegate takes to complete, averaging over up to 5 seconds of running time
    /// </summary>
    /// <param name=\"action\"></param>
    /// <returns></returns>
    public static double AverageTime(this Action action)
    {
        return AverageTime(action, 5);
    }
}

This is how you might use it, using lambda syntax:

double time = ActionTimer.Time(
    () => Enumerable.Range(1, 999)
            .Where(x => x % 5 == 0  x % 3 == 0)
            .Sum()
)

0 comments:

Post a Comment