4、RDTSC 校准:如何通过 CPU 频率(TSC 频率)将 TSC 计数值转换为纳秒/微秒时间
好,咱们接着聊 RDTSC。上一节我们讲了怎么拿到那个 64 位的 TSC 计数值,但光拿到数字没用啊。你想想看,一个计数器从 0 跑到 10 亿,这到底过了多久?是 1 秒还是 0.5 秒?
说白了,TSC 计数值本身没有物理意义。它只是一个不断累加的整数。要把它变成我们熟悉的纳秒、微秒,就必须知道 CPU 的 TSC 频率——也就是每秒钟 TSC 跳了多少次。
我个人习惯把这一步叫做「校准」。嗯,这里要注意,校准不是一次性的。不同 CPU、不同电源策略下,TSC 频率可能会变。我曾在项目中遇到过一台服务器,BIOS 里开了节能模式,结果 TSC 频率在 2.6GHz 和 2.2GHz 之间来回跳,搞得我抓狂了好几天。
4.1 为什么需要校准?
先看一个简单的公式:
时间(秒) = TSC差值 / TSC频率(Hz)
比如你两次 RDTSC 读到的值相差 2,600,000,000,而 CPU 主频是 2.6GHz,那时间就是 1 秒。但如果频率不准呢?
举个例子:
- 你以为是 2.6GHz,算出来 1 秒
- 实际是 2.4GHz,真实时间其实是 1.083 秒
- 误差 8.3%!
在微秒级交易系统里,8% 的误差意味着什么?意味着你的订单可能比别人晚 80 微秒到达交易所。80 微秒,足够让一笔套利从盈利变成亏损。
4.2 校准方法一:利用操作系统时钟
这是最直接的方法。思路很简单:
- 记录开始时的 TSC 值
- 调用 sleep(1) 或类似函数等待 1 秒
- 记录结束时的 TSC 值
- 差值就是 TSC 频率
代码示例:
#include <stdio.h>
#include <unistd.h>
#include <stdint.h>
static inline uint64_t rdtsc() {
uint32_t lo, hi;
__asm__ volatile("rdtsc" : "=a"(lo), "=d"(hi));
return ((uint64_t)hi << 32) | lo;
}
double calibrate_tsc_via_sleep() {
uint64_t start = rdtsc();
sleep(1); // 等待 1 秒
uint64_t end = rdtsc();
double freq_hz = (double)(end - start);
printf("TSC Frequency: %.2f Hz (%.2f GHz)\n", freq_hz, freq_hz / 1e9);
return freq_hz;
}
这个方法简单,但有个大问题:sleep(1) 的精度不够。操作系统调度、中断、上下文切换都会引入误差。我实测过,误差通常在 0.1% ~ 0.5% 之间。对于微秒级计时来说,这误差太大了。
4.3 校准方法二:利用 HPET 或 ACPI 定时器
HPET(高精度事件定时器)是主板上的硬件定时器,精度通常在 10 微秒以内。用它来校准 TSC 频率,比 sleep 靠谱多了。
思路是这样的:
- HPET 有一个固定的频率(通常是 14.31818 MHz 或 24 MHz)
- 我们同时读取 HPET 计数器和 TSC 计数器
- 等待 HPET 走过一个已知的时间间隔(比如 1 毫秒)
- 计算 TSC 在这段时间内的增量
代码示例(伪代码,实际需要访问 HPET 内存映射寄存器):
// 假设 HPET 基地址为 0xFED00000
// HPET 主计数器偏移 0xF0
// HPET 频率可通过寄存器 0x00 读取
uint64_t read_hpet_counter() {
return *(volatile uint64_t*)(0xFED00000 + 0xF0);
}
double calibrate_tsc_via_hpet() {
uint64_t hpet_start = read_hpet_counter();
uint64_t tsc_start = rdtsc();
// 等待 HPET 走过 1 毫秒
// HPET 频率假设为 14.31818 MHz
uint64_t hpet_target = hpet_start + 14318; // 1ms
while (read_hpet_counter() < hpet_target);
uint64_t tsc_end = rdtsc();
uint64_t tsc_delta = tsc_end - tsc_start;
// 1ms = 0.001s
double freq_hz = tsc_delta / 0.001;
return freq_hz;
}
这个方法精度高很多,误差通常在 0.01% 以内。但缺点是需要访问硬件寄存器,需要 root 权限,而且不同主板的 HPET 基地址可能不同。
4.4 校准方法三:利用 CPU 自身的恒定 TSC 特性
现代 Intel 和 AMD CPU 都有一个叫 Invariant TSC 的特性。简单说,就是 TSC 频率不再随 CPU 主频变化,而是固定在一个恒定值上。
你可以通过 CPUID 指令来检测是否支持:
// 检查 Invariant TSC 支持
// CPUID.0x80000007.EDX[8] = 1 表示支持
int check_invariant_tsc() {
uint32_t eax, ebx, ecx, edx;
__asm__ volatile("cpuid"
: "=a"(eax), "=b"(ebx), "=c"(ecx), "=d"(edx)
: "a"(0x80000007));
return (edx >> 8) & 1;
}
如果支持 Invariant TSC,那 TSC 频率就等于 CPU 的「标称频率」。这个值可以从 MSR(模型特定寄存器)中读取:
// MSR 0xCE 包含 TSC 频率信息(部分 CPU)
// 需要内核模块或 wrmsr 指令
uint64_t read_msr(uint32_t msr) {
uint32_t lo, hi;
__asm__ volatile("rdmsr" : "=a"(lo), "=d"(hi) : "c"(msr));
return ((uint64_t)hi << 32) | lo;
}
double get_tsc_freq_from_msr() {
uint64_t msr_val = read_msr(0xCE);
// 解析 MSR 得到频率,不同 CPU 格式不同
// 这里以 Intel Skylake 为例
uint32_t ratio = (msr_val >> 8) & 0xFF;
double bus_freq = 100.0e6; // 100 MHz
return ratio * bus_freq;
}
4.5 校准后的转换公式
不管用哪种方法,最终我们得到了 TSC 频率 freq_hz。然后转换就很简单了:
// TSC 差值转纳秒
inline uint64_t tsc_diff_to_ns(uint64_t diff, double freq_hz) {
return (uint64_t)(diff * 1e9 / freq_hz);
}
// TSC 差值转微秒
inline uint64_t tsc_diff_to_us(uint64_t diff, double freq_hz) {
return (uint64_t)(diff * 1e6 / freq_hz);
}
// 使用示例
uint64_t tsc1 = rdtsc();
// ... 做一些操作 ...
uint64_t tsc2 = rdtsc();
uint64_t elapsed_ns = tsc_diff_to_ns(tsc2 - tsc1, tsc_freq);
printf("Elapsed: %lu ns\n", elapsed_ns);
这里有个性能陷阱:除法运算很慢。在热路径上,我建议用乘法加移位来替代除法。比如:
// 预计算一个乘数因子
// 假设 freq_hz = 2.6e9
// 1ns = 2.6 个 TSC 周期
// 所以 diff / 2.6 ≈ diff * 0.3846
// 用定点数表示
// 2^32 / 2.6e9 ≈ 1.652
// 但更精确的做法是:
uint64_t tsc_to_ns_factor; // 定点数,Q32 格式
void init_tsc_to_ns(double freq_hz) {
// 将 1e9 / freq_hz 转换为定点数
double factor = 1e9 / freq_hz;
tsc_to_ns_factor = (uint64_t)(factor * (1ULL << 32));
}
inline uint64_t tsc_diff_to_ns_fast(uint64_t diff) {
// 使用乘法替代除法
return (diff * tsc_to_ns_factor) >> 32;
}
这样就把除法变成了一个乘法和一个移位。在 Intel CPU 上,乘法延迟只有 3 个周期,而除法要 20-40 个周期。对于微秒级交易系统来说,每一纳秒都很宝贵。
4.6 校准流程总结
我把整个校准流程画了一张图,方便你理解:
这张图展示了三种校准路径。我个人推荐优先使用 HPET 校准(方法B),因为它兼顾了精度和可靠性。如果硬件不支持 HPET,再退而求其次用 MSR 读取。sleep 校准只适合做快速验证,别用在生产环境。
好了,关于 TSC 校准就讲到这里。记住一句话:校准不准,后面全白费。花 10 分钟把校准做好,能省下后面 10 小时的调试时间。