Environment.TickCount vs DateTime.Now

Viewed 84600

Is it ever OK to use Environment.TickCountto calculate time spans?

int start = Environment.TickCount;
// Do stuff
int duration = Environment.TickCount - start;
Console.WriteLine("That took " + duration " ms");

Because TickCount is signed and will rollover after 25 days (it takes 50 days to hit all 32 bits, but you have to scrap the signed bit if you want to make any sense of the math), it seems like it's too risky to be useful.

I'm using DateTime.Now instead. Is this the best way to do this?

DateTime start = DateTime.Now;
// Do stuff
TimeSpan duration = DateTime.Now - start;
Console.WriteLine("That took " + duration.TotalMilliseconds + " ms");
14 Answers

Why are you worried about rollover? As long as the duration you are measuring is under 24.9 days and you calculate the relative duration, you're fine. It doesn't matter how long the system has been running, as long as you only concern yourself with your portion of that running time (as opposed to directly performing less-than or greater-than comparisons on the begin and end points). I.e. this:

 int before_rollover = Int32.MaxValue - 5;
 int after_rollover = Int32.MinValue + 7;
 int duration = after_rollover - before_rollover;
 Console.WriteLine("before_rollover: " + before_rollover.ToString());
 Console.WriteLine("after_rollover: " + after_rollover.ToString());
 Console.WriteLine("duration: " + duration.ToString());

correctly prints:

 before_rollover: 2147483642
 after_rollover: -2147483641
 duration: 13

You don't have to worry about the sign bit. C#, like C, lets the CPU handle this.

This is a common situation I've run into before with time counts in embedded systems. I would never compare beforerollover < afterrollover directly, for instance. I would always perform the subtraction to find the duration that takes rollover into account, and then base any other calculations on the duration.

Use

System.Diagnostics.Stopwatch

It has a property called

EllapsedMilliseconds

I use Environment.TickCount because:

  1. The Stopwatch class is not in the Compact Framework.
  2. Stopwatch uses the same underlying timing mechanism as TickCount, so the results won't be any more or less accurate.
  3. The wrap-around problem with TickCount is cosmically unlikely to be hit (you'd have to leave your computer running for 27 days and then try to measure a time that just happens to span the wrap-around moment), and even if you did hit it the result would be a huge negative time span (so it would kind of stand out).

That being said, I would also recommend using Stopwatch, if it's available to you. Or you could take about 1 minute and write a Stopwatch-like class that wraps Environment.TickCount.

BTW, I see nothing in the Stopwatch documentation that mentions the wrap-around problem with the underlying timer mechanism, so I wouldn't be surprised at all to find that Stopwatch suffers from the same problem. But again, I wouldn't spend any time worrying about it.

I was going to say wrap it into a stopwatch class, but Grzenio already said the right thing, so I will give him an uptick. Such encapsulation factors out the decision as to which way is better, and this can change in time. I remember being shocked at how expensive it can be getting the time on some systems, so having one place that can implement the best technique can be very important.

For one-shot timing, it's even simpler to write

Stopwatch stopWatch = Stopwatch.StartNew();
...dostuff...
Debug.WriteLine(String.Format("It took {0} milliseconds",
                              stopWatch.EllapsedMilliseconds)));

I'd guess the cosmically unlikely wraparound in TickCount is even less of a concern for StopWatch, given that the ElapsedTicks field is a long. On my machine, StopWatch is high resolution, at 2.4e9 ticks per second. Even at that rate, it would take over 121 years to overflow the ticks field. Of course, I don't know what's going on under the covers, so take that with a grain of salt. However, I notice that the documentation for StopWatch doesn't even mention the wraparound issue, while the doc for TickCount does.

Overflow compensation

As said before, rollover may happen after 24.9 days, or, if you use an uint cast, after 49.8 days. Because I did not want to pInvoke GetTickCount64, I wrote this overflow compensation. The sample code is using 'byte' to keep the numbers handy. Please have a look at it, it still might contain errors:

using System;
namespace ConsoleApp1 {
    class Program {
        //
        static byte Lower = byte.MaxValue / 3;
        static byte Upper = 2 * byte.MaxValue / 3;
        //
        ///<summary>Compute delta between two TickCount values reliably, because TickCount might wrap after 49.8 days.</summary>
        static short Delta( byte next, byte ticks ) {
            if ( next < Lower ) {
                if ( ticks > Upper ) {
                    return (short) ( ticks - ( byte.MaxValue + 1 + next ) );
                }
            }
            if ( next > Upper ) {
                if ( ticks < Lower ) {
                    return (short) ( ( ticks + byte.MaxValue + 1 ) - next );
                }
            }
            return (short) ( ticks - next );
        }
        //
        static void Main( string[] args ) {
            // Init
            Random rnd = new Random();
            int max = 0;
            byte last = 0;
            byte wait = 3;
            byte next = (byte) ( last + wait );
            byte step = 0;
            // Loop tick
            for ( byte tick = 0; true; ) {
                //
                short delta = Delta( next, tick );
                if ( delta >= 0 ) {
                    Console.WriteLine( "RUN: last: {0} next: {1} tick: {2} delta: {3}", last, next, tick, delta );
                    last = tick;
                    next = (byte) ( last + wait );
                }
                // Will overflow to 0 automatically
                step = (byte) rnd.Next( 0, 11 );
                tick += step;
                max++; if ( max > 99999 ) break;
            }
        }
    }
}

TickCount64

Doing some quick measurements on this new function, I found (optimized, release 64-bit, 1000mio loops):

Environment.TickCount: 2265
Environment.TickCount64: 2531
DateTime.UtcNow.Ticks: 69016

The measurements for not-optimized code were similar. Test code:

static void Main( string[] args ) {
    long start, end, length = 1000 * 1000 * 1000;
    start = Environment.TickCount64;
    for ( int i = 0; i < length; i++ ) {
        var a = Environment.TickCount;
    }
    end = Environment.TickCount64;
    Console.WriteLine( "Environment.TickCount: {0}", end - start );
    start = Environment.TickCount64;
    for ( int i = 0; i < length; i++ ) {
        var a = Environment.TickCount64;
    }
    end = Environment.TickCount64;
    Console.WriteLine( "Environment.TickCount64: {0}", end - start );
    start = Environment.TickCount64;
    for ( int i = 0; i < length; i++ ) {
        var a = DateTime.UtcNow.Ticks;
    }
    end = Environment.TickCount64;
    Console.WriteLine( "DateTime.UtcNow.Ticks: {0}", end - start );
}
Related