QueryPerformanceCounter and overflows

ronag picture ronag · Mar 14, 2011 · Viewed 7.9k times · Source

I'm using QueryPerformanceCounter to do some timing in my application. However, after running it for a few days the application seems to stop functioning properly. If I simply restart the application it starts working again. This makes me a believe I have an overflow problem in my timing code.

// Author: Ryan M. Geiss
// http://www.geisswerks.com/ryan/FAQS/timing.html
class timer
{
public:
    timer()
    {
        QueryPerformanceFrequency(&freq_);
        QueryPerformanceCounter(&time_);
    }

    void tick(double interval)
    {       
        LARGE_INTEGER t;
        QueryPerformanceCounter(&t);

        if (time_.QuadPart != 0)
        {
            int ticks_to_wait = static_cast<int>(static_cast<double>(freq_.QuadPart) * interval);
            int done = 0;
            do
            {
                QueryPerformanceCounter(&t);

                int ticks_passed = static_cast<int>(static_cast<__int64>(t.QuadPart) - static_cast<__int64>(time_.QuadPart));
                int ticks_left = ticks_to_wait - ticks_passed;

                if (t.QuadPart < time_.QuadPart)    // time wrap
                    done = 1;
                if (ticks_passed >= ticks_to_wait)
                    done = 1;

                if (!done)
                {
                    // if > 0.002s left, do Sleep(1), which will actually sleep some 
                    //   steady amount, probably 1-2 ms,
                    //   and do so in a nice way (cpu meter drops; laptop battery spared).
                    // otherwise, do a few Sleep(0)'s, which just give up the timeslice,
                    //   but don't really save cpu or battery, but do pass a tiny
                    //   amount of time.
                    if (ticks_left > static_cast<int>((freq_.QuadPart*2)/1000))
                        Sleep(1);
                    else                        
                        for (int i = 0; i < 10; ++i) 
                            Sleep(0);  // causes thread to give up its timeslice
                }
            }
            while (!done);            
        }

        time_ = t;
    }       
private:    
    LARGE_INTEGER freq_;
    LARGE_INTEGER time_;
};

My question is whether the code above should work deterministically for weeks of running continuously?

And if not where the problem is? I thought the overflow was handled by

if (t.QuadPart < time_.QuadPart)    // time wrap
    done = 1;

But maybe thats not enough?

EDIT: Please observe that I did not write the original code, Ryan M. Geiss did, the link to the original source of the code is in the code.

Answer

Erik picture Erik · Mar 14, 2011

QueryPerformanceCounter is notorious for its unreliability. It's fine to use for individual short-interval timing, if you're prepared to handle abnormal results. It is not exact - It's typically based on the PCI bus frequency, and a heavily loaded bus can lead to lost ticks.

GetTickCount is actually more stable, and can give you 1ms resolution if you've called timeBeginPeriod. It will eventually wrap, so you need to handle that.

__rdtsc should not be used, unless you're profiling and have control of which core you're running on and are prepared to handle variable CPU frequency.

GetSystemTime is decent for longer periods of measurements, but will jump when the system time is adjusted.

Also, Sleep(0) does not do what you think it does. It will yield the cpu if another context wants it - otherwise it'll return immediately.

In short, timing on windows is a mess. One would think that today it'd be possible to get accurate long-term timing from a computer without going through hoops - but this isn't the case. In our game framework we're using several time sources and corrections from the server to ensure all connected clients have the same game time, and there's a lot of bad clocks out there.

Your best bet would likely be to just use GetTickCount or GetSystemTime, wrap it into something that adjusts for time jumps/wrap arounds.

Also, you should convert your double interval to an int64 milliseconds and then use only integer math - this avoids problems due to floating point types' varying accuracy based on their contents.