The first rule of performance testing is to measure, then measure again, then measure one more time just to be sure. NET 2.0 adds a handy Diagnostics.Stopwatch which is perfect for this kind of ad-hoc precision timing.
A year ago I created a Stopwatch class which was eerily similar to the one that Microsoft ended up shipping in .NET 2.0. I went ahead and made the minor modifications necessary to make my Stopwatch class identical to Microsoft's. This way you can use the object in .NET 1.1 and port your code as-is to .NET 2.0 with only a namespace change.
using System;
/// <summary>
/// Provides a set of methods and properties that you can use to accurately
/// measure elapsed time.
/// </summary>
public class Stopwatch
{
[System.Runtime.InteropServices.DllImport("Kernel32")]
static extern bool QueryPerformanceCounter(out long @ref);
[System.Runtime.InteropServices.DllImport("Kernel32")]
static extern bool QueryPerformanceFrequency(out long @ref);
private long _Start = 0;
private long _Elapsed = 0;
private bool _IsRunning = false;
/// <summary>
/// the current performance-counter frequency, in counts per second
/// </summary>
readonly private long _CounterFrequency;
public Stopwatch()
{
/// prelinks all win32 api calls so there's less performance hit when called
System.Runtime.InteropServices.Marshal.PrelinkAll(typeof(Stopwatch));
if (QueryPerformanceFrequency(out _CounterFrequency) == false)
{
throw new Exception("High resolution timers are not available on this CPU");
}
}
/// <summary>
/// Starts, or resumes, measuring elapsed time for an interval.
/// </summary>
public void Start()
{
if (_IsRunning) this.Stop();
_Start = this.CurrentTime;
_IsRunning = true;
}
/// <summary>
/// Stops measuring elapsed time for an interval.
/// </summary>
public void Stop()
{
if (!_IsRunning) return;
_Elapsed += this.CurrentTime - _Start;
_Start = 0;
_IsRunning = false;
}
/// <summary>
/// Stops time interval measurement and resets elapsed time span to zero.
/// </summary>
public void Reset()
{
if (_IsRunning) this.Stop();
_Elapsed = 0;
}
/// <summary>
/// retrieves the current value of the high-resolution performance counter.
/// </summary>
private long CurrentTime
{
get
{
long l = 0;
QueryPerformanceCounter(out l);
return l;
}
}
/// <summary>
/// Indicates whether the Stopwatch timer is running.
/// </summary>
public bool IsRunning
{
get { return (_IsRunning); }
}
/// <summary>
/// Gets the total elapsed time measured by the current instance.
/// </summary>
public TimeSpan Elapsed
{
get { return new TimeSpan(this.ElapsedTicks); }
}
/// <summary>
/// Gets the total elapsed time measured by the current instance, in milliseconds
/// </summary>
public long ElapsedMilliseconds
{
get
{
if (_Elapsed == 0) return 0;
return (long)(((double)_Elapsed / _CounterFrequency) * 1000);
}
}
/// <summary>
/// Gets the total elapsed time measured by the current instance, in timer ticks
/// </summary>
public long ElapsedTicks
{
get
{
return (long)(this.ElapsedMilliseconds * TimeSpan.TicksPerMillisecond);
}
}
}
Did you ever wonder how the QueryPerformance* Win32 API functions work their magic and provide accurate near-nanosecond timing results? There's some interesting trivia about these functions on Matt Walsh's wiki.
Shouldn't line 37 be
if (IsRunning) this.Stop();
instead of
if (!IsRunning) this.Stop();
? Although, it really doesn't have an effect on its operation.
Jeff, I've filed a bug report about this but I'm thinking that they won't be able to do anything about it. The computer is a Dell 5150 laptop with HyperThreading enabled. The problem appears to be that QueryPerformanceCounter and QueryPerformanceFrequency are not returning the correct results (the counter is slow). This is probably a HAL issue from what I'm reading on Google. It's too bad because I really don't need precise timing. I'm just tired of always having to roll my own stopwatch functions using DateTime.Now and such.
Thanks anyway!
matt on December 8, 2005 2:28 AMOops, yes. Thanks. Let me fix that.
Jeff Atwood on December 8, 2005 2:41 AMThat's interesting. Unfortunately, your implementation AND the System.Diagnostics.StopWatch implementation both run at only about half speed on my computer. What good is a "high resolution timer" if it doesn't keep time any better than that? I've googled for this issue and haven't found anything yet. I'm wondering if Microsoft is aware of this problem? Is anyone else having this issue? Here is some dirty code to show what I'm doing. If I watch the clock on my wall and let the thing run for about 30 seconds I get the results below. Note that the StopWatch2 class is your implementation.
public partial class Form1 : Form
{
private Stopwatch _stopWatch;
private Stopwatch2 _stopWatch2;
private DateTime _start;
private DateTime _end;
private TimeSpan _total;
public Form1()
{
InitializeComponent();
_stopWatch = new Stopwatch();
_stopWatch2 = new Stopwatch2();
}
private void button1_Click(object sender, EventArgs e)
{
if (!_stopWatch.IsRunning)
{
button1.Text = "Stop";
_stopWatch.Reset();
_stopWatch.Start();
_stopWatch2.Reset();
_stopWatch2.Start();
_start = DateTime.UtcNow;
}
else
{
button1.Text = "Start";
_stopWatch.Stop();
_stopWatch2.Stop();
_end = DateTime.UtcNow;
_total = _end - _start;
MessageBox.Show("Total: " + _stopWatch.Elapsed.TotalSeconds.ToString() + ", " + _stopWatch2.Elapsed.TotalSeconds.ToString() + ", " + _total.TotalSeconds.ToString());
}
}
RESULTS:
StopWatch - 16.6576836 seconds
StopWatch2 - 16.657 seconds
Manual Way - 30.015625 seconds (correct)
That's really strange. I just tested it to be sure, and I get accurate results in both .NET 1.1 (using my class) and 2.0 (using the built-in Stopwatch class).
static void Main(string[] args)
{
Stopwatch s = new Stopwatch();
Thread newThread = new Thread(new ThreadStart(ThreadMethod));
s.Start();
newThread.Start();
Thread.Sleep(300);
s.Stop();
Console.WriteLine(s.ElapsedMilliseconds.ToString());
Console.ReadLine();
}
static void ThreadMethod()
{
Thread.Sleep(1000);
}
I suspect it's something about your computer, since the QueryPerformance* APIs require interaction between the CPU and BIOS.. what kind of PC do you have?
Jeff Atwood on December 8, 2005 12:56 PMHello guys.
yes, unforunately QueryPerformanceCounter is unusable in real life, especially on laptops. Due to Intel's SpeedStep technology or similar power-saving methods using a variable CPU speed, the timer will return strange results because in most cases it is just a wrapper function for the RDTSC instruction which depends on a fixed clock speed.
This is a major problem in game development where it is critical to have a fast and reliable timer for consistant game logic updates and all that.
Your class is still useful for e.g. if you want to profile your code on a system that's known to have a reliable perf. counter,but for everything else you should use a reliable timer like GetTickCount.
Thanks ^jacob. That seems to be the same advice that I am seeing out on Google (especially with respect to game timing). I'm surprised that Microsoft chose this method for the stopwatch class if it can be so problematic. They should at least put in a boolean flag that lets me chose whether or not I want high performance results (along with a disclaimer telling me that they may not always be accurate). I never have a need for such high performance results and I would assume that very few other users do either. It seems to be a shame that they have over-engineer this class.
As a side note, I have at least two kids video games that appear to run slow on my laptop for no reason. I would assume that they are using the QueryPerformanceCounter method of timing which is causing issues. At least it is starting to make sense.
matt on December 9, 2005 9:26 AMSince ^jacob mentioned SpeedStep I decided to turn it off in my BIOS settings. The StopWatch class seems to work just fine now. However, my computer appears to generally run slower. :(
Won't everyone who has SpeedStep turned on (default on most Intel laptops) see issues with the StopWatch class? Hmmmm... it sounds like Microsoft has opened up a big can of worms by relying on the QueryPerformanceCounter calls for this new class.
Anyway, thanks for all the info!
matt on December 9, 2005 9:55 AMOk. Sorry for using this blog to debug this issue but I thought I'd post one more little detail for those that end up having to deal with this issue on their own computers.
It appears that SpeedStep is controlled in Windows XP through the power management settings. I have re-enabled SpeedStep in the BIOS and then used the Power Management applet in Control Panel to force the processor to run at maximum speed. Set the Power Scheme to "Always On" and your processor will always run at top speed. Set the Power Scheme to "Portable/Laptop" and SpeedStep will be enabled such that the processor's speed is variable. If the current power scheme is "Always On" the StopWatch class works just fine. If it is set to "Portable/Laptop" the StopWatch class runs slow. I can even change the scheme on the fly without a reboot and see the difference between the two settings.
Good luck!
matt on December 9, 2005 10:17 AMSorry for using this blog to debug this issue
That's what it's for, since this is totally on topic..
Jeff Atwood on December 9, 2005 12:35 PMHi Jeff
Would it be a good ER to pass the original tickcount as a parameter to a new Elapsed method? That way multiple clients could share a single instance. Each client would only have to remember the original tickcount and then ask the stopwatch for the elapsed time since it started the process?
myearwood on December 29, 2005 1:17 AMMaybe, but this class is designed to mirror exactly what is in the 2.0 stopwatch class-- that makes it easier to port code back and forth.
Jeff Atwood on December 30, 2005 6:04 AMthe link to the api is very, very broken. it gives like 50000000 authentication requests
shawn on May 3, 2008 8:07 AMNo vacation after school.
William on July 12, 2009 9:15 AMThanks for the class!
It was exactly what I needed when I discovered .net 1.1 didn't have it.
apaulson on February 6, 2010 9:46 PMThe comments to this entry are closed.
|
|
Traffic Stats |