Proper Timing with GetTickCount()
Often programmers are faced with implementing timers for short-period operations, anywhere from the sub-second to a few hours. These sort of timers are typically used in timeout operations, like idle connection timeouts, stale cache timeouts, and fade timers, but are also used in basic discrete physics calculations, input debounce timers, and timeslice allocations. In these applications I like using GetTickCount().
More information on why I like it and why to choose it over other timing options follow after the break.
Why Not TTimer?
TTimer is Delphi’s wrapper to the windows message WM_TIMER. Immediately that means that our code has to be accessible from a message processing loop, which may be inconvenient in the standard “you can’t get there from here” manner. The nature of the event-driven model means you’ll have to maintain state while you wait for the timer to fire, when often it is just more convenient to keep the state in the current scope of your task. The separation of the timer’s start from the event of its completion also can have an impact on the readability of the code and make the operation unintuitive.
Windows timer messages also aren’t suited for timing beyond a few events. A high volume of timer messages can clog up your WinProc message queue, making your UI less responsive if you’re handling them in the main thread. WM_TIMER is also defined as a low-priority message, which means delivery is postponed until no higher priority messages are in your message queue. The delay between the expected firing of the event and when the WM_TIMER actually arrives also may make the timer unsuitable for your application due to its imprecision.
Finally, the duration of a timer is limited depending on your platform. Older platforms used to limit the interval to an unsigned 16-bit word, or about 65 seconds. For longer intervals on these platforms, you’ll still need to maintain your own minute count. This shouldn’t be an issue, as everything from XP up supports 31-bit (yes 31-bit) timers, but something to keep in mind if you’re still supporting legacy code on unsupported platforms.
Why Not TDateTime?
Yes, why wouldn’t you store times in something that has time right in the type name? TDateTimes are sometimes suited for timing operations. Their benefits both stem from their absolute format; the timeout is a calendar value that can be visualized, and your timeout can be set any time from now until after you’re dead and buried. Hopefully you won’t need to be reanimated to maintain your code.
The drawbacks to using TDateTimes also come from their format. For starters, TDateTimes are actually 64-bit double floats, and measuring short-period timeouts with fractional days becomes non-obvious. For example, a cached item that will be flushed out in 5 seconds expires in 0.00005787 days which isn’t evident by looking at that number. I end up seeing a lot of timeouts specified in what I call “full day notation”:
if (dtElapsed > 1.0f/24/60/60*5) then
Your timer code is also invariably going to be calling Now() to measure start and elapsed times, which means a call to the kernel’s GetLocalTime, which in turn needs to get to the hardware Real-Time Clock. Slow. Working the TDateTimes means you’re also doing floating point math, which I try to avoid whenever possible for its performance-robbing characteristic.
Sell Me on GetTickCount()
GetTickCount() returns the number of milliseconds since the system was started in an unsigned 32-bit integer. Timing things in milliseconds is extremely intuitive and readable. 1 second is 1000 milliseconds. Everybody loves round numbers, with the exception of the people who invented the system of measurement used in the United States. The ability to read and quickly comprehend what the value represents can not be understated.
Another advantage is that GetTickCount, being an integer operation which references only an internal kernel counter, makes it fast. On a Windows 7 Core i5, a single start and elapsed measurement is 25x faster than with Now(). On older hardware yet still-current operating systems the difference can be more dramatic– 100x faster is not uncommon.
While GetTickCount is fast and easy to read, it isn’t without its faults. The first immediately obvious condition is that a 32-bit integer can only hold 4 billion milliseconds, which means the counter rolls over every 47.91 days. This means that GetTickCount is not suited for time periods to exceed 47 days. I wouldn’t consider the use of GetTickCount for anything more than even a day though, finding TDateTimes more intuitive for that operation. The second issue is granularity. GetTickCount isn’t a millisecond-resolution timer as you’d expect. In supported Win32 operating systems, the resolution is 15-16 milliseconds. If anyone can explain why it is varies between 15 or 16ms, I’d love to hear it.
The rollover is by far the most important thing to take away from this. Your code must account for rollover and also remember that your timer values need to be unsigned.
var iStartTime: integer; dwStartTime: DWORD; // or Cardinal ... if (GetTickCount() - iStartTime > 1000) // WRONG: Widened both operands MessageBox('Fails after $7FFFFFFF seconds'); if (GetTickCount() - dwStartTime > 1000) // RIGHT: Signedness match MessageBox('You did it right');
Delphi will try to help you out on the first case, emitting the warning “W1024: Combining signed and unsigned types – widened both operands”. However, because iStartTime can only go to 2 billion, after iStartTime is greater than 24.855 days this comparison will always evaluate to false and your timer will not fire again. Ever.
Another important thing to remember is that the proper way to deal with the rollover is using the pattern I used above. Always use Current – Start (comparison) Interval not Start + Interval (comparison) Current. This is part of the magic of unsigned rollover. Let’s look at some cases with a single byte integer
// Case 1 - Current: $00 Start: $FE Interval: $0F Current - Start > Interval $00 - $FE = $FFFFFF02 // ^^ only the bottom 8 bits are retained leaving the correct answer, 2 // Still below Interval // Case 1 - Current: $00 Start: $FE Interval: $0F Start + Interval > Current $FE + $0F = $0D // ^^ $0D is less than Current, so is true after 2 milliseconds instead of the 10 // Erroneously returns true for from MAX_INT-Interval to MAX_INT // Case 2 - Current: $0D Start: $02 Interval: $0F Current - Start > Interval $0D - $02 = $0B // ^^ Simple math, no rollover // Case 2 - Current: $0D Start: $02 Interval: $0F Current - Interval > Start $0A - $0F = $FE // ^^ Looks like a ton of time has passed // Erroneously returns true from $00 to Interval
- Rollover is 49.71 days Do not attempt to time intervals of this duration.
- Storage type must be unsigned Use the DWORD or Cardinal data type to avoid automatic operand widening.
- Compare as Current – Start (compare) Interval Other patterns fail on the corner cases.
So there you have it. GetTickCount() is a high performance timing method that is easily understandable as long as you understand the rollover and use the right types.
|Print article||This entry was posted by Bryan Mayland on August 12, 2010 at 2:18 pm, and is filed under delphi, win32. Follow any responses to this post through RSS 2.0. Both comments and pings are currently closed.|
Comments are closed.