计时器、线程和编译器不当行为

Posted

技术标签:

【中文标题】计时器、线程和编译器不当行为【英文标题】:timers, threads and compiler misbehaviour 【发布时间】:2012-04-29 15:11:08 【问题描述】:

我遇到了问题,找不到任何答案,因为我什至不知道要搜索什么。我已经使用 QueryPerformanceCounter 完成了一个计时器类,从我的应用程序中,我启动了第二个线程对象,它有自己的实例化计时器,我只有一个无限循环,从计时器获取增量时间并使用它来输出每个循环迭代的次数第二个。

我注意到它给了我奇怪的值,所以我开始打印 delta time 并发现它有时会变为 0,所以我进入了返回 delta time 的方法并进行了一些测试。这是我的 deltaTime() 方法:

    double MyTimer2::deltaTime()
    
        LARGE_INTEGER timenow;
        QueryPerformanceCounter(&timenow);
        //std::cout << "timenow=" << (double)timenow.QuadPart << "   currentticks=" << (double)m_currentTicks.QuadPart << std::endl;

        double m_deltaTime = (double)(timenow.QuadPart - m_currentTicks.QuadPart) /* 1000.0*/ / (double)m_frequency.QuadPart;

        m_currentTicks = timenow;

        if(m_deltaTime < 0.000001)
            return 0.0;

        return m_deltaTime;
    

所以,我在“return 0.0;”上放了一个断点;发生的情况是它大部分时间都到达那里,这是不正确的。但是,如果我取消注释打印代码并运行,我将永远不会在断点处停止。所以理论上,我的打印代码使它正常工作,而如果我删除它,事情就会停止正常工作!这怎么可能,为什么会发生,我该如何解决?我试过 _ReadWriteBarrier() 没有成功。

提前致谢!

编辑:我需要一个用于物理模拟的高分辨率计时器!

【问题讨论】:

嗯,打印代码需要时间。而且从证据来看,大概需要0.000001秒以上。 好吧,另一个例子:我已经在一个 OpenGL 应用程序中使用了这个代码,它打开了 V-Sync 并打印了增量时间。它每 4 次迭代给我 0.016 秒,其他 3 次打印为 0。正确的答案是一直打印 0.016 秒,但大多数(但不是全部)时间都打印 0 ......为什么我的 delta时间方法返回 0??我用我当前的滴答数减去以前的滴答数......你是说我的代码需要 0 个 CPU 滴答来运行? 浮点数有点令人担忧。通常,QPC 回报以 64 位整数的形式减去,并且使用从较长的壁时间周期得出的平均滴答数/us 值,可以在没有 FP 的情况下计算以 uS 为单位的合理增量时间。 对不起,马丁,我听不懂你在说什么。那么你将如何计算增量时间?我们是什么?纳秒? 另外,0.016 秒,有时 16 毫秒,0,其他,非常可疑。如果准备就绪的线程多于内核数,则 16 毫秒似乎是线程准备就绪的时间。 【参考方案1】:

几代处理器前,QueryPerformanceCounter() 会读取 CPU 的周期计数器(例如 rdtsc)。使用这种方法,来自连续读取的滴答数将永远为零。分辨率等于 CPU 时钟频率,例如3 GHz。

现代处理器有两个特性使得循环计数器无法用于计时。首先,您有多个内核,每个内核都有自己的周期计数器。线程可以在内核之间迁移,如果您从两个不同的内核读取周期计数器,则差异与经过的时间无关。它甚至可能是负面的。其次,您有基于负载的动态时钟(降低频率以节省电力和超频以提高性能)。英特尔将这些分别称为“SpeedStep”和“Turbo Boost”。当循环速率不固定时,就无法从刻度转换为时间。

因此,QueryPerformanceCounter 现在使用称为高性能事件计数器 (HPET) 的专用硬件,其分辨率为几 MHz。重要的是,无论您拥有多少核心,都只有一个,而且它不会动态改变速度。但是,由于分辨率较低,现在可以在滴答之间读取两次,在这种情况下,您会得到一个报告为零的经过时间。

实际上,这不是问题。如果您需要比 HPET 提供的更精确的计时,那么通用计算机不适合您。纳秒范围内的时序会受到中断的严重影响。

【讨论】:

知道这很有趣,谢谢,但是我该如何修复我的计时器类呢?我想要一些准确的东西,这是线程安全的,并且不会通过返回 0 来搞砸我的代码 @FranciscoInácio:测量更大的代码块。正如我所提到的,一小段代码的计时结果毫无价值,因为中断可能随时增加大约微秒的暂停。与其尝试获取一个循环迭代的时间(由于中断会导致可怕的跳跃),不如测量 100 或 1000 次迭代的时间并除以。 我还没有做任何实际的物理计算,所以,也许我做了之后,事情就不会乱了。【参考方案2】:

这个区块的目的可能是什么?

if(m_deltaTime < 0.000001)
    return 0.0;

它没有任何价值,它只是把结果搞砸了,告诉你时间为零,而实际上不是。

【讨论】:

那是用于测试的,并且 m_deltaTime 每次进入 if 语句时确实为零。那是我的问题 如果单独计算long long elapsedticks = timenow.QuadPart - m_currentTicks.QuadPart;呢?也许您在 double 部门期间失去了精确度。 然后我会得到增量滴答声,我仍然需要除以频率以获得增量时间:S 我怎样才能获得正确的增量时间函数? @FranciscoInácio:但你会知道时间是否真的为零,或者这只是双重算术的结果。 我已将代码更改为首先计算 deltaticks,然后“if(deltaticks == 0) deltaticks = 1;”最后除以频率。 7 秒后,它停在我在 if 语句上放置的断点处,因此 deltaticks 最终为 0。【参考方案3】:

首先,您的计时器是错误的:它会大量消耗您的 CPU。在单核机器上,它会减慢所有系统的速度。如果你想创建一个定时器并以 Windows 为目标,你可以使用timer functions。

然后,deltaTime() 函数返回的每个非负值都是有效的。虽然您不是在实时操作系统中托管,但每个操作都可能花费任意时间。一次迭代可能需要数十个处理器周期或数十年。没有人保证。

第三,关于实验结果。似乎如果上下文将在两个连续的时间测量之间切换一次,您将获得大约 0.016s 的值,否则,您将获得低于 0.000001s 的值,该值被限制为 0s

如前所述,打印到控制台是一项相对繁重的操作,实际上,当您启用它时,您总是会切换上下文。

编辑

虽然QueryPerformanceCounter 似乎提供了很好的分辨率,但它会让你陷入困境。除非您在实时操作系统中工作,否则您将永远无法获得真正高分辨率的计时器。

【讨论】:

那么我怎样才能做一个不返回 0 而是返回真实 delta 时间的 delta 时间函数呢?我如何将所有这些包装在一个计时器类中? @FranciscoInácio, 0 是有效的返回值。这意味着真正的增量时间是几乎 0。如果你想要timer,使用timer functions 你是说如果我的循环代码更昂贵,我永远不会有 deltatime=0?我已经在使用计时器功能,如果您查看您给我的链接,那里有 QueryPerformance 计时器功能。我需要一个高分辨率计时器来获得准确的增量时间来进行物理模拟。 @FranciscoInácio:挂钟时间在模拟中毫无用处。通常你也模拟一个时钟。 @FranciscoInácio,我在链接上只看到 setTimerkillTimer :) 另请参阅我的编辑。

以上是关于计时器、线程和编译器不当行为的主要内容,如果未能解决你的问题,请参考以下文章

使用 HAL 的 NUCLEO-F303K8 上的 DMA 不当行为

带定时器的 C++ 多线程编程

译Gradle 的依赖关系处理不当,可能导致你编译异常

Object C 属性、特性、类型

Gradle 的依赖关系处理不当,可能导致你编译异常!解决方案在此!

完成/线程订单协助