The fix for multiple CPUs is easy. Call SetThreadAffinity() to force your process thread to run on one CPU. Then you will not get the effect of the TSC or QPC jumping back and forth due to the thread being switched from one CPU to the other.
Here is the code for a better timer class derived from a C++ class that I found a few years back. It uses both the QPC and timeGetTime to solve the problem of inaccurate QPC clocks.
Code:
// Original C++ source by Oleg Pudeyev
// Converted from original C++ source by Steve 'Sly' Williams
// Converted to use ticks internally instead of seconds for greater accuracy
//
// Original comments:
// I took timer and frame rate code from DXUtil.h/cpp (common DX sample code).
// You can find it in DXSDK\Samples\Multimedia\Common\Include and Src.
// Instead of using a single function with static data, I organized the code into
// classes, which allows for additional flexibility.
// Initialization (mmtime/performance frequency selection) is now performed on startup,
// in a dedicated class. I also added timeBeginPeriod/timeEndPeriod calls.
// CFrameRateCounterEx is my own creation.
//
// This code fixes the "feature" described in Q274323,
// "Performance Counter Value May Unexpectedly Leap Forward":
// http://support.microsoft.com/default.aspx?scid=kb;en-us;Q274323
unit BetterTimer;
interface
type
TBetterTimer = class
private
// Time elapsed before last pause, or 0 if the timer was never paused
m_ElapsedTime: Int64;
// Time in 'timer time' (seconds) when this timer was last resumed
m_ResumedTimeStamp: Int64;
// Time in QueryPerformanceTimer-counts converted to seconds
// when this timer was last resumed
m_LastTimeStampQPC: Int64;
// The same time in milliseconds as obtained from timeGetTime
m_LastTimeStampMMT: Cardinal;
// A counter indicating whether the counter is active.
// If it is greater than zero, the counter is running, otherwise it is paused
m_Running: Integer;
public
constructor Create;
function GetTime: Single;
procedure Pause;
procedure Reset;
procedure Resume;
end;
implementation
uses
Windows, MMSystem, SysUtils;
// The maximum deviation from timeGetTime reading that we will tolerate, in milliseconds
const
PerfCounterTolerance = 1000;
var
// This variable is TRUE if performance counter is available
gs_HavePerformanceCounter: Boolean = False;
// If performance counter is available, this variable contains 1/its resolution -- number of seconds in each count.
// It's a floating point value mostly for convenience.
gs_CountsPerSecond: Int64;
{ TBetterTimer }
constructor TBetterTimer.Create;
begin
inherited;
// Start and reset the timer
m_Running := 1;
Reset();
end;
function TBetterTimer.GetTime: Single;
var
QPCTime, DeltaQPC, TimeDelta: Int64;
MMTTime, DeltaMMT, DeltaQPCinMS: DWORD;
begin
Result := m_ElapsedTime / gs_CountsPerSecond;
// If the timer is paused, no time has passed since the pause time
// and all passed time is stored in elapsed time variable, so return that
if m_Running <= 0 then
Exit;
// Otherwise, retrieve current time, subtract last resumed time from it, add
// elapsed time, and return the result adjusted for possible QPC leaps
if gs_HavePerformanceCounter then
begin
// The code is the same as in Pause function
// Get current time in ticks
QueryPerformanceCounter(QPCTime);
// Determine the time difference between this and previous QPC query
DeltaQPC := QPCTime - m_LastTimeStampQPC;
// Get current mmtimer time
MMTTime := timeGetTime();
// And the difference between currnent and previous mmtimer query
DeltaMMT := MMTTime - m_LastTimeStampMMT;
// Check if the performance counter leaped forward,
// which is when difference in values returned by QPC and mmtimer is more than
// the predefined PerfCounterTolerance value
// Since all times are returned as unsigned variables, care must be taken when subtracting
// because we don't want leap adjustment to be applied in case mmtimer is lagging behind QPC
DeltaQPCinMS := DeltaQPC * 1000 div gs_CountsPerSecond;
if (DeltaQPCinMS > DeltaMMT) and (DeltaQPCinMS - DeltaMMT > PerfCounterTolerance) then
begin
// Performance counter leaped forward
// Adjust the elapsed time by the difference between QPC and mmtimer delta times
m_ElapsedTime := m_ElapsedTime - DeltaQPC - DeltaMMT;
end;
// Calculate total delta time since timer was reset
TimeDelta := QPCTime - m_ResumedTimeStamp + m_ElapsedTime;
// Update current timer timestamps
m_LastTimeStampQPC := QPCTime;
m_LastTimeStampMMT := MMTTime;
// Return calculated delta
Result := TimeDelta / gs_CountsPerSecond;
end
else
begin
// If we're using mmtimer, just return the time passed since last resume
// plus the elapsed time that passed before last resume
// No adjustments are necessary
Result := (timeGetTime() - m_ElapsedTime + m_ResumedTimeStamp) / gs_CountsPerSecond;
end;
end;
procedure TBetterTimer.Pause;
var
QPCTime, DeltaQPC: Int64;
MMTTime, DeltaMMT, DeltaQPCinMS: DWORD;
begin
Dec(m_Running);
// Allow for nested pause/resume calls.
// Only pause if active count reaches zero
if m_Running <> 0 then
Exit;
// Update the elapsed time
if gs_HavePerformanceCounter then
begin
// Get current time in ticks
QueryPerformanceCounter(QPCTime);
// Determine the time difference between this and previous QPC query
DeltaQPC := QPCTime - m_LastTimeStampQPC;
// Get current mmtimer time
MMTTime := timeGetTime();
// And the difference between currnent and previous mmtimer query
DeltaMMT := MMTTime - m_LastTimeStampMMT;
// Check if the performance counter leaped forward,
// which is when difference in values returned by QPC and mmtimer is more than
// the predefined PerfCounterTolerance value
// Since all times are returned as unsigned variables, care must be taken when subtracting
// because we don't want leap adjustment to be applied in case mmtimer is lagging behind QPC
DeltaQPCinMS := DeltaQPC * 1000 div gs_CountsPerSecond;
if (DeltaQPCinMS > DeltaMMT) and (DeltaQPCinMS - DeltaMMT > PerfCounterTolerance) then
begin
// Performance counter leaped forward
// Adjust the elapsed time by the difference between QPC and mmtimer delta times
m_ElapsedTime := m_ElapsedTime - DeltaQPC - DeltaMMT;
end;
// Add the time passed since last resume to the elapsed time variable
m_ElapsedTime := m_ElapsedTime + QPCTime - m_ResumedTimeStamp;
// Don't update last polled time stamps for QPC and mmtimer
// since they will be updated in Resume method
end
else
begin
// If we are using mmtimer, just add the time passed since last resume time
// to the elapsed time variable
m_ElapsedTime := m_ElapsedTime + timeGetTime() + m_ResumedTimeStamp;
end;
// The timer is now paused
end;
procedure TBetterTimer.Reset;
begin
// This function initializes or resets the timer
if gs_HavePerformanceCounter then
begin
// Retrieve the last resumed time stamp
QueryPerformanceCounter(m_ResumedTimeStamp);
m_LastTimeStampQPC := m_ResumedTimeStamp;
// To correct for unexpected leaps, retrieve the same time from the multimedia timer
m_LastTimeStampMMT := timeGetTime();
end
else
begin
// There are no issues with multimedia timer, so just get the current value
// and write it to the last resumed stamp
m_ResumedTimeStamp := timeGetTime();
end;
// Timer hasn't been paused, so set elapsed time to zero
m_ElapsedTime := 0;
end;
procedure TBetterTimer.Resume;
begin
Inc(m_Running);
// Allow for nested pause/resume calls.
// Only resume if active count reaches one
if m_Running <> 1 then
Exit;
// Update the last resumed time stamp
if gs_HavePerformanceCounter then
begin
// Get current time in ticks
QueryPerformanceCounter(m_ResumedTimeStamp);
m_LastTimeStampQPC := m_ResumedTimeStamp;
// Get the current time from mmtimer as well for QPC adjustments
m_LastTimeStampMMT := timeGetTime();
end
else
begin
// For mmtimer, just retrieve the current time
m_ResumedTimeStamp := timeGetTime();
end;
end;
var
// Set the highest resolution for the multimedia timer
tc: TIMECAPS;
initialization
// QueryPerformanceFrequency returns a BOOL value indicating if a performance counter is available
gs_HavePerformanceCounter := QueryPerformanceFrequency(gs_CountsPerSecond);
if not gs_HavePerformanceCounter then
gs_CountsPerSecond := 1000;
// Retrieve timer caps, which contain resolution range
timeGetDevCaps(@tc, SizeOf(tc));
// Set resolution with this call
timeBeginPeriod(tc.wPeriodMin);
finalization
// Don't retrieve the caps again to be absolutely sure we restore the same value that we set
// Restore old resolution
timeEndPeriod(tc.wPeriodMin);
end.
Bookmarks