细说时间测量RDTSC和RDTSCP
细说RDTSC的坑
1.前言
前几天写了一篇文章(链接在此),探索各种取时间的方式,结论是TSC是精度最高、开销最小的方式,但是同时也声明了,使用的时候可能会碰见很多坑。拉到最后拿代码。
今天我们将进行深入探讨。
2.TSC原理
TSC是一个64位的寄存器,从Intel Pentium开始,在所有的x86平台上均会提供。它存放的是CPU从启动以来执行的指令周期数。通过rdtsc指令,可以将TSC的数值存放在EDX:EAX中,示例代码如下:
uint64_t get_tsc()
{
uint64_t a, d;
__asm__ volatile("rdtsc" : "=a"(a), "=d"(d));
return (d << 32) | a;
}
3.TSC的坑
TSC曾经是一个极高精度,极低开销的取时间的方法,但是随着CPU往多核、多处理器、低功耗的方向上走,在使用TSC时就会遇到很多坑。
【坑1】比如有的CPU会根据机器负载情况动态调节工作频率, 那么单位时间CPU的指令周期数就会发生变化,也就很难将其转换成时间。另外,CPU进入休眠再次重启后,TSC会清零。
【坑2】再比如,在同一处理器的多个核心之间,以及不同处理器的不同核心之间,rdtsc的结果是否是同步的呢?如果不同步,那么取时的结果就不能用来相互比较。
【坑3】再比如,Intel的处理器自Pentium Pro开始,引入了乱序执行的功能,导致程序读取的TSC结果可能不准。如果编写测试程序的时候没有主动回避,也可能会掉到坑里。
4.官方填坑
在较新版本的CPU中,引入了常量速率TSC的特性(constant rate TSC)。可以通过如下命令查看你的CPU是否支持(我的机器有四个核,因此输出了四条):
支持该特性的CPU,其TSC是按照其标称频率流逝的,与CPU的实际工作频率与状态无关。如果你的CPU也是支持constant_tsc特性的,那么【坑1】算是填上了。
关于【坑2】,即不同核心读取的tsc是否同步,目前没有找到统一的说法,Intel的官方手册也没有明说,比如:vol 3b,17.15.1 Invariant TSC章节:
The time stamp counter in newer processors may support an enhancement, referred to as invariant TSC.
Processor’s support for invariant TSC is indicated by CPUID.80000007H:EDX[8].
The invariant TSC will run at a constant rate in all ACPI P-, C-. and T-states. This is the architectural behavior
moving forward. On processors with invariant TSC support, the OS may use the TSC for wall clock timer services
(instead of ACPI or HPET timers). TSC reads are much more efficient and do not incur the overhead associated with
a ring transition or access to a platform resource.
但这里面只是说TSC能够在CPU处于任何(电源)状态下都能保证以标称速率递增,并没有明确说明TSC能够在多核甚至多处理器的情况下保持同步。
另一个蛛丝马迹是在Linux内核代码中(链接在此):
这里有一个unsynchronized_tsc()函数,用于判断系统的TSC是不是同步的,代码实现如下:
/*
* Make an educated guess if the TSC is trustworthy and synchronized
* over all CPUs.
*/
int unsynchronized_tsc(void)
{
if (!boot_cpu_has(X86_FEATURE_TSC) || tsc_unstable)
return 1;
#ifdef CONFIG_SMP
if (apic_is_clustered_box())
return 1;
#endif
if (boot_cpu_has(X86_FEATURE_CONSTANT_TSC))
return 0;
if (tsc_clocksource_reliable)
return 0;
/*
* Intel systems are normally all synchronized.
* Exceptions must mark TSC as unstable:
*/
if (boot_cpu_data.x86_vendor != X86_VENDOR_INTEL) {
/* assume multi socket systems are not synchronized: */
if (num_possible_cpus() > 1)
return 1;
}
return 0;
}
这里有几个有意思的点:
- 开头的注释说,“make an educated guess”,即有根据的猜测,即这里是不是TSC同步的判断依然是一个猜测
- 中间的代码判断了是否开启了CONSTANT TSC特性,如果开启就直接返回0,即TSC是同步的,也就是说,只要我们在cpuinfo里看到constant_tsc的flag,就证明我们的机器的TSC是同步的
- 后面还有一句注释“Intel systems are normally all synchronized.Exceptions must mark TSC as unstable:”,即Intel的系统,只要用户没有手动禁用TSC同步,一般都是同步的。
- 在Intel CPU下还有一个注释“assume multi socket systems are not synchronized”,即在多处理器系统上,不同CPU(处理器、socket、NUMA节点)之间的TSC是不同步的。
看到这里,我们基本上可以确定了,即:
- 如果你的cpuinfo里有constant_tsc的flag,那么无论在同一CPU不同核心之间,还是在不同CPU的不同核心之间,TSC都是同步的,可以随便用
- 如果你用的是Intel的CPU,但是cpuinfo里没有constant_tsc的flag,那么在同一处理器的不同核心之间,TSC仍然是同步的,但是不同CPU的不同核心之间不同步,尽量不要用
至此,【坑2】也基本上解决了。
关于【坑3】,即乱序执行问题,可以使用RDTSCP命令来代替RDTSC,前者开销虽然略高,但胜在稳定好用。另外,如果不想用这个指令,还可以用memory barrier技术(后面的文章中我们将详细解释该技术)或者CPUID指令来实现,不过这两者我都没试,据说开销也不小,详细资料可以就参见参考资料中的wiki页面和intel的官方手册。
下面这个程序可以用来测试RDTSC和RDTSCP指令的性能:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdint.h>
#include <time.h>
#include <sys/time.h>
// gcc -o time_6 time_6.c
uint64_t get_tsc()
{
uint64_t a, d;
__asm__ volatile("rdtsc" : "=a"(a), "=d"(d));
return (d << 32) | a;
}
uint64_t get_tscp()
{
uint64_t a, d;
__asm__ volatile("rdtscp" : "=a"(a), "=d"(d));
return (d << 32) | a;
}
#define LOOP_TIMES 1000000000
int main(int argc, char **argv)
{
uint64_t beg_tsc, end_tsc;
long loop;
long sum;
printf("-------------rdtsc-------------\n");
loop = LOOP_TIMES;
sum = 0;
while(loop--)
{
beg_tsc = get_tsc();
end_tsc = get_tsc();
sum += (end_tsc - beg_tsc);
}
printf("AVG_CYCLE : %ld\n", sum / LOOP_TIMES);
sleep(1);
printf("-------------rdtscp-------------\n");
loop = LOOP_TIMES;
sum = 0;
while(loop--)
{
beg_tsc = get_tscp();
end_tsc = get_tscp();
sum += (end_tsc - beg_tsc);
}
printf("AVG_CYCLE : %ld\n", sum / LOOP_TIMES);
return 0;
}
测试结果如下:
我一共跑了三次,每次差别都不大,RDTSCP指令比RDTSC多耗费10个指令周期左右,慢不到1倍。如果你能接受这点差别,建议还是用RDTSCP命令吧。
另外,RDTSCP指令也是需要平台支持的,是否支持可以使用cat /proc/cpuinfo | grep rdtscp命令查看。
5.使用建议
- 如果你的cpuinfo里面没有constant_tsc的flag,建议老老实实用clock_gettime吧,或者换台支持constant_tsc的机器
- 如果你的cpuinfo里面有constant_tsc的flag,那么在同一处理器的不同核心之间可以放心使用TSC,跨处理器的不同核之间,尽量避免使用,可能会有未知的问题
- 如果不是对性能极其敏感,尽量使用RDTSCP代替RDTSC,前者略慢,但能避免CPU乱序执行问题
6.待明确问题
- TSC是真实的寄存器,还是上层封装的一个虚拟的指令
- TSC究竟是一个核心一个,还是一个CPU一个,还是整个系统一个
- RDTSCP究竟是如何实现禁止乱序执行的,使用memory barrier和cpuid指令如何实现同样的功能,开销如何
7.参考资料
再论 Time stamp counter - 一念天堂 - 博客园
Pitfalls of TSC usage | Oliver Yang
linux - rdtsc accuracy across CPU cores - Stack Overflow
8.测量代码
#include <chrono>
#include <thread>
#include <cstdint>
#include <iostream>
/*
cat /proc/cpuinfo | grep constant_tsc
cat /proc/cpuinfo | grep rdtscp
*/
__inline__ uint64_t get_tsc()
{
uint64_t a, d;
__asm__ volatile("rdtsc" : "=a"(a), "=d"(d));
return (d << 32) | a;
}
__inline__ uint64_t get_tscp(void)
{
uint32_t lo, hi;
// take time stamp counter, rdtscp does serialize by itself, and is much cheaper than using CPUID
__asm__ __volatile__ (
"rdtscp" : "=a"(lo), "=d"(hi)
);
return ((uint64_t)lo) | (((uint64_t)hi) << 32);
}
/*
* Accelerators for sched_clock()
* convert from cycles(64bits) => nanoseconds (64bits)
* basic equation:
* ns = cycles / (freq / ns_per_sec)
* ns = cycles * (ns_per_sec / freq)
* ns = cycles * (10^9 / (cpu_khz * 10^3))
* ns = cycles * (10^6 / cpu_khz)
*
* Then we use scaling math (suggested by george@mvista.com) to get:
* ns = cycles * (10^6 * SC / cpu_khz) / SC
* ns = cycles * cyc2ns_scale / SC
*
* And since SC is a constant power of two, we can convert the div
* into a shift.
*
* We can use khz divisor instead of mhz to keep a better precision, since
* cyc2ns_scale is limited to 10^6 * 2^10, which fits in 32 bits.
* (mathieu.desnoyers@polymtl.ca)
*
* -johnstul@us.ibm.com "math is hard, lets go shopping!"
*/
__inline__ uint64_t cycles_2_ns(uint64_t cycles, uint64_t hz)
{
return cycles * (1000000000.0 / hz);
}
uint64_t get_cpu_freq()
{
FILE *fp=popen("lscpu | grep CPU | grep MHz | awk {'print $3'}","r");
if(fp == nullptr)
return 0;
char cpu_mhz_str[200] = { 0 };
fgets(cpu_mhz_str,80,fp);
fclose(fp);
return atof(cpu_mhz_str) * 1000 * 1000;
}
int main()
{
for (int i = 0; i < 100; i++) {
uint64_t t1 = get_tsc();
uint64_t t2 = get_tsc();
std::cout << t1 << '\n' << t2 << "\n t2-t1 " << t2-t1 << "\n ns:" << cycles_2_ns(t2-t1, get_cpu_freq()) << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
return 0;
}
另外C++封装代码:GitHub - MengRao/tscns: A low overhead nanosecond clock based on x86 TSC
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)