gettimeofday()函数保证具有微秒级的分辨率吗?

106
我正在将一个最初为Win32 API编写的游戏移植到Linux(实际上是将Win32移植到OS X,然后再移植到Linux)。
我通过提供自进程启动以来的微秒数来实现了QueryPerformanceCounter。
BOOL QueryPerformanceCounter(LARGE_INTEGER* performanceCount)
{
    gettimeofday(&currentTimeVal, NULL);
    performanceCount->QuadPart = (currentTimeVal.tv_sec - startTimeVal.tv_sec);
    performanceCount->QuadPart *= (1000 * 1000);
    performanceCount->QuadPart += (currentTimeVal.tv_usec - startTimeVal.tv_usec);

    return true;
}

这个,再加上QueryPerformanceFrequency()返回一个恒定的1000000作为频率,在我的机器上运行良好,并给我一个包含自程序启动以来的uSeconds的64位变量。
那么,这个可移植吗?我不想发现它在内核以某种方式编译时工作方式不同之类的情况。不过,如果除了Linux之外的其他系统不可移植,我可以接受。
10个回答

63
也许吧。但你有更大的问题。如果系统上有改变计时器的进程(例如,ntpd),gettimeofday()可能会导致不正确的时间。然而,在一个“正常”的Linux系统上,我相信gettimeofday()的分辨率是10微秒。它可以根据系统上运行的进程向前或向后跳跃时间。因此,对于你的问题,实际上是没有答案的。
你应该研究一下clock_gettime(CLOCK_MONOTONIC)来进行定时间隔。它由于多核系统和外部时钟设置等原因,遇到的问题要少得多。
另外,还要了解一下clock_getres()函数。

1
clock_gettime只存在于最新的Linux系统中,其他系统只有gettimeofday()。 - vitaly.v.ch
3
这是POSIX标准,因此不仅限于Linux,并且也不是最新的吗?即使像Red Hat Enterprise Linux这样的“企业”发行版也基于2.6.18,其中包含clock_gettime函数,因此不算很新。(在RHEL中man页的日期为2004年3月12日,所以它已经存在一段时间了)除非你指的是真正陈旧的内核,否则你到底想表达什么意思啊? - Spudd86
clock_gettime函数在2001年被包含在POSIX标准中。据我所知,目前clock_gettime()已经在Linux 2.6和QNX中实现了。但是许多生产系统目前仍在使用Linux 2.4。 - vitaly.v.ch
它于2001年被引入,但直到POSIX 2008才成为强制性要求。 - R.. GitHub STOP HELPING ICE
2
从Linux FAQ中了解到lock_gettime(请参见David Schlosnagle的答案):“CLOCK_MONOTONIC……通过adjtimex()由NTP进行频率调整。在未来(我仍在努力推出补丁),将会有一个CLOCK_MONOTONIC_RAW,它不会被修改,并且与硬件计数器具有线性相关性。” 我认为_RAW时钟从未进入内核(除非它被重命名为_HR,但我的研究表明这些努力也被放弃了)。 - Tony Delroy

45

英特尔处理器高分辨率、低开销的计时

如果你使用英特尔硬件,以下是如何读取CPU实时指令计数器。它会告诉你自处理器启动以来执行的CPU周期数量。这可能是你可以用于性能测量的最细粒度计数器。

请注意,这是CPU周期数。在Linux上,您可以从/proc/cpuinfo获取CPU速度并将其除以得到秒数。将其转换为双精度类型非常方便。

当我在我的设备上运行此操作时,我获得了:

11867927879484732
11867927879692217
it took this long to call printf: 207485

这是英特尔开发者指南,其中提供了大量细节。

#include <stdio.h>
#include <stdint.h>

inline uint64_t rdtsc() {
    uint32_t lo, hi;
    __asm__ __volatile__ (
      "xorl %%eax, %%eax\n"
      "cpuid\n"
      "rdtsc\n"
      : "=a" (lo), "=d" (hi)
      :
      : "%ebx", "%ecx");
    return (uint64_t)hi << 32 | lo;
}

main()
{
    unsigned long long x;
    unsigned long long y;
    x = rdtsc();
    printf("%lld\n",x);
    y = rdtsc();
    printf("%lld\n",y);
    printf("it took this long to call printf: %lld\n",y-x);
}

11
请注意,不同核心之间的TSC可能不总是同步的,在处理器进入低功耗模式时可能会停止或更改其频率(您无法知道它是否这样做),而且通常不总是可靠的。内核能够检测它何时是可靠的,检测其他替代选项,如HPET和ACPI PM定时器,并自动选择最佳选项。除非您确信TSC稳定且单调,否则最好始终使用内核进行计时。 - CesarB
12
在英特尔平台上,核心及以上的处理器上的TSC(时间戳计数器)会在多个CPU之间同步,并且独立于电源管理状态以恒定的频率递增。详细信息请参阅Intel软件开发手册,第3卷18.10节。然而,计数器递增的速率与CPU频率不相同。TSC递增的速率为“平台的最大可解决频率,等于可扩展总线频率和最大可解决总线比例的乘积”,详细信息请参阅Intel软件开发手册,第3卷18.18.5节。您可以从CPU的型号特定寄存器(MSR)中获取这些值。 - sstock
7
您可以通过查询CPU的特定型号寄存器(MSR)来获取可扩展总线频率和最大解析总线比,方法如下:可扩展总线频率 == MSR_FSB_FREQ [2:0],ID为0xCD;最大已解析总线比 == MSR_PLATFORM_ID [12:8],ID为0x17。请参阅Intel SDM Vol.3附录B.1以解释寄存器值。您可以在Linux上使用msr-tools查询这些寄存器。http://www.kernel.org/pub/linux/utils/cpu/msr-tools/ - sstock
1
你的代码在执行第一条 RDTSC 指令之后,在执行被基准测试的代码之前,是否应该再次使用 CPUID?否则,如何防止被基准测试的代码在第一次 RDTSC 之前或同时执行,从而导致 RDTSC 差值不准确? - Tony Delroy

19

@Bernard:

我必须承认,你的大部分示例都超出了我的理解范围。它确实可以编译并且似乎可以工作。这对SMP系统或SpeedStep是否安全呢?

这是个好问题...我认为代码没问题。从实际角度来看,我们公司每天都在使用它,而且我们在各种配置的服务器上运行,包括2-8个内核。当然,具体情况可能有所不同,但这似乎是一种可靠且低开销的计时方法(因为它不会切换到系统空间进行上下文切换)。

通常的工作原理是:

  • 声明代码块为汇编代码(并且易失性的,这样优化器就不会对其进行优化)。
  • 执行CPUID指令。除了获取某些CPU信息(我们不需要这些信息)之外,它还会同步CPU的执行缓冲区,以使计时不受乱序执行的影响。
  • 执行RDTSC(读取时间戳)命令。这将获取自处理器重置以来执行的机器周期数。这是一个64位值,因此随着当前CPU速度,它每194年左右会重新开始。有趣的是,在最初的Pentium参考文献中,他们指出它每5800年左右会重新开始。
  • 最后几行将寄存器的值存储到变量hi和lo中,并将其放入64位返回值中。

具体说明如下:

  • 乱序执行可能导致不正确的结果,因此我们执行“cpuid”指令,它除了提供有关CPU的一些信息外,还会同步任何乱序指令执行。

  • 大多数操作系统在启动时同步CPU计数器,因此答案精度高达几纳秒。

  • 休眠的评论可能是真的,但实际上您可能不关心跨越休眠界限的时间。

  • 关于SpeedStep:新的Intel CPU通过调整时间戳计数速率来自适应处理器速度变化。这意味着时间戳计数不会受影响。

更改并返回调整后的计数。 我快速扫描了我们网络上的一些盒子,只发现一个没有它的盒子:一个运行旧数据库服务器的Pentium 3。(这些都是Linux盒子,所以我用grep constant_tsc /proc/cpuinfo检查过)

  • 我不确定AMD CPU,我们主要使用英特尔产品,虽然我知道我们的一些低级系统专家进行了AMD评估。

  • 希望这满足了您的好奇心,这是一个有趣而且(在我看来)未被充分研究的编程领域。当Jeff和Joel谈论程序员是否应该知道C语言时,我大声喊着:“嘿,忘记那些高级C语言…汇编是你应该学习的,如果你想知道计算机在做什么!”


    1
    内核开发人员一直在努力让人们停止使用rdtsc...并且通常避免在内核中使用它,因为它非常不可靠。 - Spudd86
    1
    作为参考,我之前问过的问题是:“我必须承认,你的大部分示例都超出了我的理解范围。虽然它能编译并且似乎工作正常,但对于SMP系统或SpeedStep来说安全吗?” - Bernard

    14

    11

    9
    实际的 gettimeofday() 分辨率取决于硬件架构。英特尔处理器和 SPARC 机器提供可测量微秒的高分辨率计时器。其他硬件架构则退回到系统计时器,通常设置为 100 Hz。在这种情况下,时间分辨率将不够准确。
    我从 High Resolution Time Measurement and Timers, Part I 中获得了这个答案。

    9
    数据结构中明确规定了微秒作为测量单位,但这并不意味着时钟或操作系统实际上能够如此精细地测量时间。在这种情况下,“分辨率”指的是它将被递增的最小量是多少。
    就像其他人建议的那样,使用gettimeofday()是不好的,因为设置时间可能会导致时钟偏移并扰乱计算。你需要使用clock_gettime(CLOCK_MONOTONIC),而clock_getres()将告诉你时钟的精度。

    当gettimeofday()在夏令时调整时向前或向后跳跃时,您的代码会发生什么? - mpez0
    3
    clock_gettime() 只存在于最新的 Linux 系统中,其他系统只有gettimeofday()函数。 - vitaly.v.ch

    6

    这个答案提到了时钟调整的问题。使用C++11的<chrono>库可以解决您保证滴答单位和时间调整的问题。

    时钟std::chrono::steady_clock保证不会被调整,而且它相对于实际时间以恒定速率前进,因此诸如SpeedStep之类的技术不会影响它。

    通过转换为std::chrono::duration特化之一(例如std::chrono::microseconds),您可以获得类型安全的单位。使用该类型,滴答值所使用的单位没有歧义。但是,请记住,时钟不一定具有此分辨率。您可以将持续时间转换为attoseconds,而无需实际拥有如此精确的时钟。


    4

    根据我的经验和在互联网上所读到的,答案是“不确定”,这取决于CPU速度、操作系统、Linux版本等因素。


    3

    在SMP系统中,读取RDTSC是不可靠的,因为每个CPU都维护自己的计数器,并且每个计数器不能保证与另一个CPU同步。

    我建议尝试使用clock_gettime(CLOCK_REALTIME)。 POSIX手册指出,这应该在所有兼容系统上实现。它可以提供纳秒计数,但您可能需要检查clock_getres(CLOCK_REALTIME)在您的系统上看看实际分辨率是多少。


    clock_getres(CLOCK_REALTIME) 不会给出真正的分辨率。当高精度定时器可用时,它总是返回“1 ns”(一纳秒)。请检查 include/linux/hrtimer.h 文件中的 define HIGH_RES_NSEC 1(更多信息请参见 https://dev59.com/nFjUa4cB1Zd3GeqPVuUL#23044075)。 - osgx

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