Windows上的微秒级时间戳

19

如何在Windows上获得微秒级分辨率的时间戳?

我正在寻找比QueryPerformanceCounterQueryPerformanceFrequency更好的解决方案(这些只能给出自启动以来经过的时间,并且如果它们在不同的线程上调用,则不一定准确 - 也就是说,QueryPerformanceCounter可能会在不同的CPU上返回不同的结果。还有一些处理器为节能而调整其频率,这显然没有反映在它们的QueryPerformanceFrequency结果中。)

有一个名为Implement a Continuously Updating, High-Resolution Time Provider for Windows 的资源,但它似乎并不可靠。When microseconds matter看起来很棒,但现在已无法下载了。

另一个资源是Obtaining Accurate Timestamps under Windows XP,但它需要执行一些步骤,运行助手程序加上一些初始化操作,而且我不确定它是否适用于多个CPU。

我还看了维基百科的文章Time Stamp Counter,但并没有太大帮助。

如果答案是在BSD或Linux上做这件事情会更容易一些,那么也没关系,但我想确认一下,并获得一些解释,为什么在Windows上这样做很难,在Linux和BSD上却很容易。这是同样好的硬件...


在Linux或BSD中,有没有一个易于实现的例子? - JonM
2
你知道吗,如果你阅读QueryPerformanceCounter的文档,它明确表示它可以在不同的线程中调用,并且不受节能影响。唯一的例外是有缺陷的BIOS、驱动程序和/或硬件。在忽略API之前,请先阅读文档 ;) - jalf
9个回答

21

我认为这仍然有用:系统内核:提供多媒体定时器支持的指南

它很好地解释了各种可用的定时器及其局限性。也许你的敌人不是分辨率,而是延迟。

QueryPerformanceCounter并不总是以CPU速度运行。实际上,在多处理器(/多核心)系统上,它可能会尝试避免RDTSC:如果Windows Vista及更高版本可用,则使用HPET,否则使用ACPI / PM计时器。 在我的系统上(Windows 7 x64,双核AMD),定时器运行在14.31818 MHz。

先前的系统也是如此:

  

默认情况下,Windows Server 2003 Service Pack 2(SP2)对所有多处理器APIC或ACPI HAL使用PM计时器,除非检查过程无法确定BIOS是否支持APIC或ACPI HAL。

问题是,当检查失败时。这仅意味着你的计算机/ BIOS存在某种损坏。然后,你可以修复BIOS(推荐),或者至少暂时切换到使用ACPI计时器(/usepmtimer)

使用Stopwatch.IsHighResolution检查高分辨率计时器支持非常容易,而不需要使用P/Invoke。然后可以查看Stopwatch.Frequency来获取必要的QueryPerformanceCounter调用内部信息。

同时需要考虑的是,如果计时器失效,整个系统将变得混乱,并且通常会表现出负的经过时间、减速等行为,而不仅仅是您的应用程序受影响。

这意味着您实际上可以依赖于QueryPerformanceCounter。

与普遍认为的相反,QueryPerformanceFrequency()“在系统运行时无法更改”

编辑:根据QueryPerformanceCounter()文档的说明,“调用哪个处理器并不重要” - 实际上,如果APIC/ACPI检测失败并且系统只能使用TSC,那么整个线程亲和力的操作都是不必要的。这应该是一个不应该发生的情况。如果在旧系统上发生了这种情况,制造商可能会提供BIOS更新/驱动程序修复。如果没有,/usepmtimer启动开关仍然可用。如果它也失败了,因为系统除了Pentium TSC之外没有合适的计时器,你可能需要考虑干扰线程亲和性 - 但即使如此,在页面“社区内容”区域中提供的示例由于在每次启动/停止调用时设置线程亲和性而引入了相当大的延迟,可能会减少使用高分辨率计时器的好处。

游戏定时和多核处理器是关于如何正确使用它们的建议。请注意,它现在已经五年了,在那个时候较少的系统完全支持ACPI - 这就是为什么在抨击它时,文章会详细介绍TSC以及如何通过保持亲和线程来解决其限制性问题。

我相信现在很难找到一个没有任何ACPI支持和可用PM计时器的普通PC。最常见的情况可能是BIOS设置错误(有时是出厂默认设置)导致ACPI支持不正确。

逸闻轶事告诉我们,八年前这种情况只是极少数。 (读起来很有趣,开发人员绕过设计“缺陷”并抨击芯片设计师。公平地说,反过来也可能一样。:-))


"使用 QueryPerformanceCounter?哇,如果我早想到就好了......""即使在我的新 core i7 上,这仍然可能返回负结果,所以要小心。此外,我不认为有人建议您更改线程亲和性来调用 QueryPerformanceCounter,然后再改回来。只需永久将线程设置为一个核心即可。" - matt
@matt:我并不是说 - 或者此贴中的任何人- 建议在每个调用上更改线程亲和性。然而,就在这个页面上提供的社区内容确实如此:http://msdn.microsoft.com/en-us/library/ms644904%28VS.85%29.aspx - Andras Vass
@matt:实际上,是的,请使用QPC - 但请确保它不会使用RDTSC,否则您将最终得到一个很好伪装的对RDTSC的API调用。 - Andras Vass
如果您需要HPET,请确保BIOS中的设置正确。 我遇到的一个是“ACPI HPET表=>启用”。 另一个是“HPET支持=>启用”,然后根据您运行的是Vista(Win7)x86还是x64,选择“HPET模式=>32位/64位”。HPET:http://blog.fpmurphy.com/2009/07/linux-hpet-support.htmlHPET仅适用于Vista及以上版本。 旧系统也可以使用PM(ACPI)计时器。 - Andras Vass
@andras:很酷,我一定会检查BIOS设置!我肯定在至少一台电脑上进行了QPC,其中频率与CPU时钟无关,可能更接近于14MHz标记。+1 - matt
@matt:谢谢。我急切地等待你的回复。我见过几年前的8路Xeon出现了同样的问题。它是在当时ACPI规范最终确定之前构建的。Win2003无法决定使用什么,只能退回到最坏的情况(TSC)。在那上面测试我们的应用程序的性能真是太有趣了... :-). - Andras Vass

12

QueryPerformanceCounter / QueryPerformanceFrequency,处理器速度分辨率。

多线程要小心。处理器上的每个核心都可以有自己的计数器。

更多信息可以参考 在Windows XP下获取准确的时间戳

如果必须使用这种方法:

当我尝试手动向串口(用于红外发射器)写入数据时,我发现将进程和线程优先级设置为最高(实时)可以大大提高其可靠性(即无错误)。如果我没记错话,这需要约40 kHz的分辨率,因此它应该足够精确以达到毫秒级分辨率。


1
这些只能给你自开机以来经过的时间,并且如果它们在不同的线程上被调用,它们不一定准确 - 例如,QueryPerformanceCounter 可能会在不同的 CPU 上返回不同的结果。也有一些处理器为了省电而调整频率,这显然并不总是反映在它们的 QueryPerformanceFrequency 结果中。 - kibibu
1
@kenny,是的,处理器亲和力。这是我用来做所有高精度工作(如分析)的方法。无论如何,是的,我应该先阅读msdn链接,它建议这是一个解决方案,然后解释了它不完美的地方。@Nikhil:我很好奇你为什么需要这个?理论上,所有PC都有一个系统时钟,在处理器关闭电源时维护时间。我不知道它的分辨率是多少,也不知道如何获取它。 - matt
1
QPC是标准Windows发行版中可用的最高分辨率计时器,除非您想安装自定义硬件/驱动程序,否则您需要解决QPC的限制。 - Chris Becke
+1 - 这是在Windows上实现的方法。计数器将在同一线程内准确,如果您需要跨线程准确性,您只需使用处理器亲和力即可。我知道这就是大多数游戏计算游戏帧的方式。 - JonM
我已经取消了对这个答案的踩 - 我确信如果你不需要纳秒级别的精度,会有更好的选择,但毫秒是各种返回实际时间的方法中最好的。不幸的是,现在我无法切换到点赞...线程亲和力确实很重要,只需确保在调用此函数时没有跨多个线程而未将它们全部设置为同一处理器即可。仍然存在(显然)某些版本的SpeedStep和类似问题。通常情况下,正确的答案取决于应用程序。 - kibibu
显示剩余4条评论

6
  1. Windows不是一个实时操作系统。

  2. 在多任务操作系统上,进程需要让出时间给另一个线程/进程。这会导致一些时间开销。

  3. 每个函数调用都会有开销,因此在返回请求时会有一定的延迟。

  4. 此外,调用系统调用将需要您的进程从用户空间模式切换到内核空间模式,这具有相对较高的延迟。您可以通过在内核模式下运行整个进程(例如设备驱动程序代码)来克服这个问题。

  5. 一些操作系统,如Linux或BSD,更好,但它们仍然无法保持精确的子微秒定时分辨率(例如,在Linux上nanosleep()的准确度约为1毫秒,而不是小于1毫秒),除非您将内核补丁到某些特定的调度程序,以使您的应用程序受益。

我认为,最好是使你的应用程序适应这些问题,例如经常重新校准你的计时例程,这就是你的链接提供的内容。据我所知AFAIK,无论其准确性如何,Windows 的最高计时器分辨率仍然是 GetPerformanceCounter/Frequency()。你可以通过在单独的线程中运行计时器池例程,并将该线程亲和力设置为一个核处理器,并将线程优先级设置为最高来获得更好的准确性。


6

我认为你不会找到比QueryPerformanceCounter更好的解决方案。标准技术是设置代码以捕获并丢弃由线程切换CPU可能导致的时间向后跳跃和异常值。如果你正在测量非常小的间隔(如果不是,则不需要那种精度),那么这种情况并不常见。只需将其视为可容忍的错误而不是关键错误。

在极少数情况下,如果您绝对需要确保它永远不会发生,则通过设置处理器亲和力掩码锁定线程是唯一的选择。


6

QueryPerformanceCounter是解决这个问题的正确方法。与您和一些回答者所写的相反,此调用即使在多处理器系统中(除非问题系统已损坏),也会提供正确的答案,并处理甚至更改的CPU频率。在大多数现代系统上,它是从RDTSC派生出来的,但为您处理所有这些多CPU和频率更改的细节。(它比RDTSC慢得多)。

请参见QueryPerformanceCounter

在多处理器计算机上,调用哪个处理器不重要。但是,由于基本输入/输出系统(BIOS)或硬件抽象层(HAL)中的错误,不同处理器可能会产生不同的结果。


3
到目前为止,答案中有很多有用的信息。如果您正在寻找一种简单的跨平台方法来获取自1970年1月1日以来在毫秒或更高分辨率下经过的时间(仅限于Windows XP或更高版本),则在苹果的MacOS 10.7.5版JavaScriptCore的CurrentTime.cpp中有一个非常简单的跨平台示例(我似乎在他们的10.8+版本中找不到它)。我所指的代码位于CurrentTime()函数中。它使用标准技术使用QueryPerformanceCounter()计算高于毫秒分辨率的经过时间差,并定期将其与系统时钟同步以计算时间戳并解决时钟漂移问题。为了获得更高分辨率的时间戳,它要求您运行Windows XP或更高版本,以便调用QueryPeformanceFrequency()保证成功。
它没有考虑上下文切换会稍微影响时间(如"为Windows实现连续更新的高分辨率时间提供程序""Windows时间戳项目"所做的),但它会不断重新同步。我不会用它来发射火箭,但在大约50行代码的情况下,它很容易实现,并且对许多目的已足够好。

此外,如果您知道您保证运行Windows 8 / Windows Server 2012,则应该使用GetSystemTimePreciseAsFileTime(),因为它以最高精度(1微秒或更好)返回系统日期和时间。


此外,从Visual Studio 2015开始,Windows上的std::chrono将具有亚毫秒级分辨率。 - Chris Kline

1

我使用了DateTimePrecise类来自The Code Project

唯一的问题是,如果我不每10秒钟调用它,它会给出疯狂的结果--我认为内部存在某种整数溢出--所以我有一个定时器,每隔几秒钟执行DateTimePrecise.Now

如果您想要时间准确,请在计算机上运行NTP

祝你好运...


1

我发现在使用PerformanceCounterPerformanceCounterFrequency时存在困难,因为给定的PerformanceCounterFrequency与实际频率偏差较大。

它存在一个偏移量,并且还显示热漂移。新硬件似乎有较少的漂移,但漂移和偏移量相当可观。几个ppm的漂移就会大大损坏微秒级精度,因为1 ppm是1 µs/s!因此,在使用PerformanceCounterPerformanceCounterFrequency时强烈建议进行仔细的硬件特定校准。这也可能是未频繁调用某些函数时观察到“疯狂结果”的原因。

我对此进行了更详细的调查。描述可以在Windows微秒分辨率时间服务中找到。


0

自从C++11出现了新的<chrono>头文件,所以这个任务会变得更加简单。只需要使用std::chrono::high_resolution_clockstd::chrono::system_clock(墙上时钟)或std::chrono::steady_clock(单调时钟),你就可以拥有一个跨平台的解决方案。

auto start1 = std::chrono::high_resolution_clock::now();
auto start2 = std::chrono::system_clock::now();
auto start3 = std::chrono::steady_clock::now();
// do some work
auto end1 = std::chrono::high_resolution_clock::now();
auto end2 = std::chrono::system_clock::now();
auto end3 = std::chrono::steady_clock::now();

std::chrono::duration<long long, std::micro> diff1 = end1 - start1;
std::chrono::duration<double, std::nano>     diff2 = end2 - start2;
auto diff3 = std::chrono::duration_cast<std::chrono::microseconds>(end3 - start3);

std::cout << diff.count() << ' ' << diff2.count() << ' ' << diff3.count() << '\n';

在C++17及以上版本(或C11及以上版本)中,还有另一种解决方案: std::timespec_get()
#include <iostream>
#include <ctime>
 
int main()
{
    std::timespec ts;
    std::timespec_get(&ts, TIME_UTC);
    char buf[100];
    std::strftime(buf, sizeof buf, "%D %T", std::gmtime(&ts.tv_sec));
    std::cout << "Current time: " << buf << '.' << ts.tv_nsec << " UTC\n";
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接