5

我正在使用 QueryPerformanceCounter 在我的应用程序中进行一些计时。但是,在运行几天后,该应用程序似乎停止正常运行。如果我只是重新启动应用程序,它就会再次开始工作。这让我相信我的计时代码中有溢出问题。

// 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_;
};

我的问题是上面的代码是否应该在连续运行数周内确定性地工作?

如果不是,问题出在哪里?我以为溢出是由

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

但也许这还不够?

编辑:请注意,我没有编写原始代码,Ryan M. Geiss 做了,代码原始源的链接在代码中。

4

5 回答 5

15

QueryPerformanceCounter因其不可靠而臭名昭著。如果您准备好处理异常结果,则可以将其用于单个短间隔计时。这并不准确 - 它通常基于 PCI 总线频率,负载过重的总线可能会导致丢失滴答声。

GetTickCount实际上更稳定,如果您调用timeBeginPeriod. 它最终会包装,所以你需要处理它。

__rdtsc不应使用,除非您正在分析并控制您正在运行的内核并准备处理可变 CPU 频率。

GetSystemTime对于较长时间的测量来说是不错的,但在调整系统时间时会跳跃。

此外,Sleep(0)不做你认为它做的事。如果另一个上下文需要它,它将产生 cpu - 否则它将立即返回。

简而言之,Windows 上的时间是一团糟。有人会认为,今天可以从计算机上获得准确的长期计时而无需费力——但事实并非如此。在我们的游戏框架中,我们使用了来自服务器的多个时间源和更正,以确保所有连接的客户端具有相同的游戏时间,并且那里有很多错误的时钟。

您最好的选择可能是只使用 GetTickCount 或 GetSystemTime,将其包装成可以调整时间跳跃/环绕的东西。

此外,您应该将您的转换double interval为一个int64 milliseconds,然后只使用整数数学 - 这可以避免由于浮点类型根据其内容而变化的精度而导致的问题。

于 2011-03-14T10:06:54.003 回答
5

性能计数器是 64 位的,因此它们足够大,可以连续运行多年。例如,如果您假设性能计数器每秒增加 20 亿次(一些假想的 2 GHz 处理器),它将在大约 290 年内溢出。

于 2011-03-14T09:51:53.840 回答
5

根据您的评论,您可能应该改用可等待计时器

请参阅以下示例:

于 2011-03-14T12:27:39.787 回答
4

无论如何,使用纳秒级计时器来控制诸如 Sleep() 之类的东西,它最多可以精确到几毫秒(通常是几十毫秒),这有点争议。

您可能考虑的另一种方法是使用 WaitForSingleObject 或类似的函数。这会消耗更少的 CPU 周期,导致一天中的上下文切换次数减少一万亿次,并且也比 Sleep(0) 更可靠。

例如,您可以创建一个 semapore,并且在正常操作中永远不要触摸它。信号量的存在只是为了让您可以等待某些东西,如果您没有更好的等待。然后,您可以使用单个系统调用指定最长 49 天的超时时间(以毫秒为单位)。而且,它不仅会减少工作量,而且会更加准确。

这样做的好处是,如果“有事”,那么你想早点分手,你只需要发出信号量。等待调用将立即返回,您将从 WAIT_OBJECT_0 返回值知道这是由于发出信号,而不是由于时间耗尽。所有这一切都没有复杂的逻辑和计数周期。

于 2011-03-14T12:55:14.767 回答
2

您最直接询问的问题: if (t.QuadPart < time_.QuadPart) 应该是这样的: if (t.QuadPart - time_.QuadPart < 0)

这样做的原因是你想寻找相对时间的包装,而不是绝对时间。在对 QPC 的引用调用之后,相对时间将换行 (1ull<<63) 时间单位。绝对时间可能会在重新启动后换行 (1ull<<63) 时间单位,但它可以在任何其他时间换行,这是未定义的。

QPC 在某些系统上存在一些问题(例如,早期多核 CPU 上基于 RDTSC 的旧 QPC),因此可能需要像这样允许小的负时间增量: if (t.QuadPart - time_.QuadPart < -1000000)//time wrap

实际的换行会产生非常大的负时间增量,所以这是安全的。在现代系统上应该没有必要,但信任微软很少是一个好主意。

... 但是,时间换行的更大问题在于ticks_to_wait,ticks_passedticks_left都是 int ,而不是 LARGE_INT 或 long long ,就像它们应该的那样。如果涉及任何重要的时间段,这会使大部分代码包装 - 在这种情况下,“重要”是平台相关的,在少数情况下(这些天很少见),它可能在 1 秒左右,或者在某些情况下甚至更少假设的未来系统。

其他问题:

if (time_.QuadPart != 0)

零不是那里的特殊值,不应被视为特殊值。我的猜测是代码将返回时间为零的 QPC 与返回值为零的 QPC 混为一谈。返回值不是指针传递的 64 位时间,而是 QPC 实际返回的 BOOL。

此外,Sleep(0) 循环是愚蠢的——它似乎被调整为仅在特定级别的争用和特定的每线程 CPU 性能上才能正确运行。如果您需要解决方案,这是一个可怕的想法,如果您不需要解决方案,那么整个函数应该只是对 Sleep 的一次调用。

于 2014-04-15T12:04:28.630 回答