¶Win32 timer queues are not suitable for high-performance timing
I read a suggestion on a blog that Win32 timer queues should be used instead of timeSetEvent(), so I decided to investigate.
First, a review of what timer queues do. A timer queue is used when you have a bunch of events that need to run at various times. What the timer queue does is maintain a sorted list of timers and repeatedly handles the timer with the nearest deadline. Not only is this more efficient when you have a bunch of timers because you don't have a bunch of pieces of code all maintaining their own timing, but it's also a powerful technique because it can allow you to multiplex a limited timing resource. It's especially good when you have a bunch of low-priority, long-duration timers like UI timers, where you don't want to spend a lot of system resources and precise timing is not necessary.
The classic timer queue API in Windows is SetTimer(). This is mainly intended for UI purposes, and as a result it's both cheap and imprecise. If you're trying to do multimedia timing, SetTimer() is not what you want. It's also a bit annoying to use because you need a message loop and there's no way to pass a (void *) cookie to the callback routine (a common infraction which makes C++ programmers see red). The newer timer API, however, is CreateTimerQueue(). This allows you to create your own timer queue without having to tie it to a message loop, and looks like it would be a good replacement for timeSetEvent().
Unfortunately, if you're in a high-performance timing scenario like multimedia, Win32 timer queues suck.
The first problem is the interface. CreateTimerQueueTimer() takes a parameter called DueTime, which specifies the delay until the timer fires for the first time. Delay relative to what? Well, when you call the CreateTimerQueueTimer() function. Or rather, some undetermined time between when you call the function and it returns. The problem with an interface like this is that you have no idea if something sneaks in between and stalls your thread for a while, like another thread or a page fault. Therefore, you get a random amount of jitter in your start time. Another problem is that if you are creating a repeating timer, you can only set the period in milliseconds. That's not precise enough for a lot of applications. If you're trying to lock to a beam position on a 60Hz display, for instance, this forces you to take a third of a second error per frame, or a 2% error.
That's not the worst part, though. Let's say we just want a regular 100ms beat. That shouldn't be hard for a modern CPU to do. Well, here are the results:
0 0 109 109 219 110 328 109 437 109 547 110 656 109 766 110 875 109
The first number is the time offset in milliseconds, measured by timeGetTime(), which has approx. 1ms accuracy and precision. The second number is the delta from the last timer event. Notice a problem? The period is consistently longer than requested. In this case, we're 10% slower than intended. If you request a lower period, it gets much worse. Here's the results for a 47ms periodic timer:
0 0 63 63 125 62 188 63 250 62 313 63 375 62 438 63 500 62 563 63
The average period is about 63ms, which is about 30% off from our requested period. That's terrible!
There's another factor, by the way: the timer queue API shares the same characteristic as many timing APIs in Windows of being dependent upon the resolution of the system timer interrupt and is thus also affected by timeBeginPeriod(). The reason I know is that the first time I tried this, I got results that still weren't great, but were a lot better than what you see above. The 47ms timer, for instance, turned in deltas of 48-49ms. Then I realized that I was playing a song in WinAmp in the background, and had to redo the test again.
After being somewhat depressed at the mediocre performance of the timer queue API, I remembered the CreateWaitableTimer() API. This is a different API where you create a timer object directly instead of part of a timer queue, and it also runs the timer in your thread instead of a thread pool thread, which is much easier to deal with if you're trying to time work that requires APIs that must be called on a specific thread, particularly most DirectX APIs. As it turns out, the waitable timer API doesn't fare any better than the timer queue API for periodic timers, as it still takes the period in milliseconds and still has the same problems of significant, consistent error in period and sensitivity to the timer interrupt rate. However, the good side is that you specify the initial delay in 100ns units instead of milliseconds, and more importantly, you can specify an absolute deadline. This is very advantageous, because it means that you can compute timer deadlines internally in high precision off of a consistent time base, and although each individual timer may be imprecise, you can precisely control the average period.
Caveat: I've only tried this in Windows XP, so I don't know if the situation has improved in Windows Vista or Windows 7.
In current versions of VirtualDub, I don't use any of these methods for frame timing in preview mode. Instead, I have my own timer queue thread that uses timeGetTime() coupled with a Sleep() loop, with precision boosted by timeBeginPeriod() and with thread priority raised. You might think this is a bit barbaric, and it is, but it was the best way I could think of to get a reliable, precise timer on all versions of Windows. The trick to making this work is putting in feedback and adjusting the delay passed to Sleep() so that you don't accumulate error. As a result, I can do what I couldn't do with timeSetEvent(), which is to have a timer that has an average period of 33.367 ms. I suppose I could rewrite it on top of the waitable timer API, but it didn't seem worth the effort.
Here is the test program output from Windows 7 RC for a 50ms timer:
0 0 47 47 94 47 140 46 203 63 250 47 296 46 343 47 390 47 452 62 499 47 546 47 593 47 640 47 702 62 749 47 796 47 842 46 889 47 952 63
It seems that Windows 7 does use a method that prevents accumulated error, thus giving an accurate period on average at the cost of consistency.
More interesting is when you give it a very short period, one that is shorter than that of the timing source:
0 0 16 16 16 0 16 0 31 15 31 0 31 0 47 16 47 0 47 0 63 16 63 0 63 0 78 15 78 0 78 0 94 16 94 0 94 0
In this case, the timer fires multiple times back to back.