4、高精度计时器:HPET与TSC原理、CPU周期计数、rdtsc指令的使用与陷阱

说到高精度计时,我脑子里第一个蹦出来的就是当年调一个网络延迟抖动问题时的场景。那时候用 gettimeofday 死活测不出微秒级的波动,后来才发现——嗯,计时器的精度选错了,一切都是白搭。

今天咱们就聊聊两个最核心的高精度计时器:HPETTSC。还有那个让人又爱又恨的 rdtsc 指令。

4.1 HPET:高精度事件定时器

HPET,全称 High Precision Event Timer。说白了就是主板上的一个硬件定时器,精度能到纳秒级。

我个人习惯把 HPET 当作「系统的心跳」。它由一组计数器组成,每个计数器可以独立配置中断频率。你想想看,操作系统调度线程、处理时间片,底层靠的就是它。

特性 说明
最小分辨率 通常 1 微秒以下,取决于硬件实现
中断能力 支持周期性中断和一次性中断
多计数器 至少 3 个比较器/计数器
主频 通常 10 MHz 到 25 MHz

不过 HPET 有个问题——访问开销大。每次读计数器都要走 PCI 总线,延迟几十到几百纳秒。这在某些场景下是致命的。

我的经验: 如果你需要频繁获取时间戳(比如每秒百万次以上),HPET 不是好选择。它的强项是「定时触发」,而不是「高频率读取」。

4.2 TSC:时间戳计数器

TSC 就完全不一样了。它是 CPU 内部的一个寄存器,每个时钟周期递增一次。读取它只需要一条指令——rdtsc

速度快到什么程度?几十纳秒就能拿到一个时间戳。我在项目中做过对比,同样读 100 万次时间,TSC 比 HPET 快了将近两个数量级。

但 TSC 有个大坑——它不一定稳定

早期的 CPU 上,TSC 会随频率变化。你想想看,CPU 降频了,TSC 增长就变慢。这会导致时间计算完全乱套。后来 Intel 引入了 Invariant TSC,保证在所有核心、所有电源状态下都以恒定频率递增。

关键判断: 你的 CPU 是否支持 Invariant TSC?

用这条命令查:grep constant_tsc /proc/cpuinfo

如果输出里有 constant_tsc 标志,恭喜你,TSC 是可靠的。

4.3 rdtsc 指令的使用

好,咱们直接上手。rdtsc 指令把 TSC 的低 32 位存入 EAX,高 32 位存入 EDX。在 64 位系统里,通常用内联汇编或者编译器内置函数来调用。

// GCC/Clang 内置函数
#include <x86intrin.h>

unsigned long long tsc = __rdtsc();

// 或者用内联汇编
static inline unsigned long long rdtsc() {
    unsigned int lo, hi;
    __asm__ volatile ("rdtsc" : "=a"(lo), "=d"(hi));
    return ((unsigned long long)hi << 32) | lo;
}

拿到 TSC 值之后,怎么换算成纳秒?

你需要知道 CPU 的标称频率。比如 2.5 GHz,每个 tick 就是 0.4 纳秒。但注意——这里有个陷阱。

我曾经踩过的坑: 直接用 cpu_khz 或者 /proc/cpuinfo 里的频率做除法。结果发现不同核心上报的频率不一样,甚至同一个核心在不同时间读到的频率都有波动。后来我改用 calibrate_tsc() 的方式,通过 HPET 或者 ACPI 来校准 TSC 频率,才真正稳定下来。

4.4 rdtsc 的三大陷阱

说实话,rdtsc 用起来爽,但坑也多。我总结了三个最常见的陷阱:

  1. 乱序执行问题

    CPU 会乱序执行指令。rdtsc 可能在你预期的代码之前或之后执行。解决办法是加 lfencecpuid 指令做序列化。

    // 带序列化的 rdtsc
    static inline unsigned long long rdtsc_serialized() {
        unsigned int lo, hi;
        __asm__ volatile ("cpuid" ::: "%rax", "%rbx", "%rcx", "%rdx");
        __asm__ volatile ("rdtsc" : "=a"(lo), "=d"(hi));
        __asm__ volatile ("cpuid" ::: "%rax", "%rbx", "%rcx", "%rdx");
        return ((unsigned long long)hi << 32) | lo;
    }
  2. 多核不同步

    不同 CPU 核心的 TSC 可能不同步。线程在核心 A 上读了个值,切到核心 B 上再读,差值可能很大。我建议绑定核心来读 TSC。

  3. 虚拟机中的陷阱

    在虚拟机里,rdtsc 可能被截获并模拟。开销暴增,精度暴跌。如果你在云环境里做性能分析,千万别直接依赖 TSC。

我的建议: 在物理机上,用 rdtsc 做短时间间隔测量(微秒级)非常准。但如果是跨秒级的时间测量,还是老老实实用 clock_gettime(CLOCK_MONOTONIC, ...) 吧。

4.5 实战:用 TSC 测量函数耗时

最后给个完整的例子。这是我常用的微基准测试模板:

#include <stdio.h>
#include <x86intrin.h>

double tsc_to_ns(unsigned long long ticks, double freq_ghz) {
    return (double)ticks / freq_ghz;
}

int main() {
    // 假设 CPU 频率 2.5 GHz
    double freq = 2.5;

    unsigned long long start = __rdtsc();
    // 这里放你要测量的代码
    volatile int sum = 0;
    for (int i = 0; i < 1000; i++) sum += i;
    unsigned long long end = __rdtsc();

    unsigned long long elapsed = end - start;
    printf("耗时: %.2f 纳秒\n", tsc_to_ns(elapsed, freq));
    return 0;
}

嗯,这里要注意——volatile 关键字是为了防止编译器优化掉循环。实际测量时,记得多跑几次取中位数,避免单次抖动。

总结一下:HPET 适合做定时触发,TSC 适合做高频时间戳。rdtsc 很快,但乱序、多核、虚拟化这三个坑一定要避开。我个人现在做性能分析,首选 TSC + 核心绑定 + 序列化指令,这套组合拳打下来,精度和稳定性都还不错。