3、x86_64 时间戳指令:RDTSC 与 RDTSCP 的原理、乱序执行风险与序列化屏障
做量化交易系统,时间戳就是我们的心跳。你想想看,一笔订单从产生到成交,中间差了几个微秒,可能就是盈利和亏损的分界线。今天咱们聊聊 x86_64 平台上两个最核心的时间戳指令——RDTSC 和 RDTSCP。
我个人习惯把这两个指令称为「CPU 内部的秒表」。它们直接读取处理器内部的时间戳计数器(TSC),精度极高,延迟极低。但用起来有不少坑,我当年第一次在实盘系统里用 RDTSC 时,就踩过一个大雷。
3.1 RDTSC 指令:基础但危险
RDTSC(Read Time-Stamp Counter)是 x86 架构上最古老的时间戳读取指令。它把 TSC 寄存器的 64 位值写入 EDX:EAX 寄存器对中。
它的核心特点就一个字:快。通常只需要几十个 CPU 周期。但问题也出在这里——它不保证指令执行的顺序。
现代 CPU 为了提升性能,会动态调整指令的执行顺序。RDTSC 本身是一条非序列化指令,这意味着 CPU 可能在你读取时间戳之前,就已经执行了后面的指令。
我曾经在调试一个高频交易策略时发现,明明代码逻辑是先记录时间戳再执行交易指令,但实际日志里时间戳却比成交时间还晚。查了两天才发现是 RDTSC 被 CPU 乱序执行了。
具体来说,RDTSC 可能被 CPU 重排到以下几种情况之后:
- 前面的内存加载指令(LOAD)
- 前面的分支预测指令
- 前面的算术运算指令
这就导致你读到的时间戳,可能比实际执行点早了或者晚了几个纳秒。在微秒级交易系统里,这几个纳秒足以让你的策略失效。
3.2 RDTSCP 指令:带序列化的改进版
RDTSCP 是 RDTSC 的升级版。它做了两件事:
- 读取 TSC 值到 EDX:EAX
- 同时读取 CPU 核心 ID 到 ECX
最关键的是,RDTSCP 是一条部分序列化指令。它保证在它之前的所有指令都执行完毕后,才会读取时间戳。这就解决了乱序执行的问题。
我建议在交易系统中优先使用 RDTSCP。虽然它比 RDTSC 慢那么一点点(大概多 10-20 个周期),但换来的是时间戳的准确性,这笔买卖很划算。
RDTSC:快,但不保证顺序,有乱序执行风险
RDTSCP:稍慢,但保证之前指令全部完成,时间戳更可靠
3.3 序列化屏障:给 CPU 戴上「紧箍咒」
如果你必须用 RDTSC(比如某些老平台不支持 RDTSCP),那就得手动加序列化屏障。说白了,就是强制 CPU 在读取时间戳之前,把前面的指令全部执行完。
常用的序列化指令有:
- CPUID:最常用的序列化指令,但开销较大(几百个周期)
- LFENCE:轻量级序列化,只保证加载指令的顺序
- MFENCE:保证加载和存储指令的顺序
- SFENCE:只保证存储指令的顺序
我个人的经验是,在交易系统中用 LFENCE 配合 RDTSC 就够了。CPUID 太慢,会引入不可接受的延迟抖动。
来看一个实际代码示例:
// 使用 LFENCE + RDTSC 的安全读取方式
static inline uint64_t read_tsc_safe() {
uint32_t lo, hi;
// 先执行 LFENCE,确保前面的指令都完成了
__asm__ volatile("lfence" ::: "memory");
// 再读取 TSC
__asm__ volatile("rdtsc" : "=a"(lo), "=d"(hi));
// 再执行 LFENCE,确保后面的指令不会提前执行
__asm__ volatile("lfence" ::: "memory");
return ((uint64_t)hi << 32) | lo;
}
// 使用 RDTSCP 的推荐方式
static inline uint64_t read_tscp() {
uint32_t lo, hi, core_id;
__asm__ volatile("rdtscp" : "=a"(lo), "=d"(hi), "=c"(core_id));
// RDTSCP 自带序列化,不需要额外屏障
return ((uint64_t)hi << 32) | lo;
}
我曾经在代码里只用了 RDTSC 没加屏障,结果在 Intel Skylake 架构上跑得好好的,换到 AMD Zen 架构上就出问题了。不同 CPU 对乱序执行的激进程度不一样,千万别赌这个。
3.4 知识体系结构图
下面这张图展示了 RDTSC 和 RDTSCP 在交易系统时间戳体系中的位置,以及它们与序列化屏障的关系:
3.5 实战建议
说了这么多,总结几条我在实战中沉淀下来的经验:
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 高频交易核心路径 | RDTSCP | 自带序列化,延迟可接受 |
| 监控/日志系统 | RDTSC + LFENCE | 对延迟不敏感,节省资源 |
| 跨核心时间同步 | RDTSCP + 核心ID | 需要知道在哪个核心上执行 |
| 基准测试 | RDTSC + CPUID | 需要最严格的序列化保证 |
记住一句话:在交易系统里,时间戳的准确性比速度更重要。宁可多花 20 个周期用 RDTSCP,也别为了省那点时间用裸 RDTSC 然后被乱序执行坑了。
嗯,这里要注意一点:RDTSCP 虽然自带序列化,但它只保证之前的指令完成,不保证之后的指令不会提前执行。所以如果你需要在读取时间戳之后做某些操作,还是得在 RDTSCP 后面加 LFENCE。
我见过不少新手在 RDTSCP 后面直接开始计算,结果计算指令被 CPU 提前执行了,时间戳反而比计算结果还晚。这种 bug 特别难查,因为不是每次都会复现。
好了,关于 RDTSC 和 RDTSCP 的核心内容就这些。记住:选对指令,加对屏障,你的时间戳才能真实反映系统的运行状态。