前几天写了一篇文章(链接在此),探索各种取时间的方式,结论是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;
}
TSC曾经是一个极高精度,极低开销的取时间的方法,但是随着CPU往多核、多处理器、低功耗的方向上走,在使用TSC时就会遇到很多坑。
【坑1】比如有的CPU会根据机器负载情况动态调节工作频率, 那么单位时间CPU的指令周期数就会发生变化,也就很难将其转换成时间。另外,CPU进入休眠再次重启后,TSC会清零。
【坑2】再比如,在同一处理器的多个核心之间,以及不同处理器的不同核心之间,rdtsc的结果是否是同步的呢?如果不同步,那么取时的结果就不能用来相互比较。
【坑3】再比如,Intel的处理器自Pentium Pro开始,引入了乱序执行的功能,导致程序读取的TSC结果可能不准。如果编写测试程序的时候没有主动回避,也可能会掉到坑里。
在较新版本的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;
}
这里有几个有意思的点:
看到这里,我们基本上可以确定了,即:
至此,【坑2】也基本上解决了。
关于【坑3】,即乱序执行问题,可以使用RDTSCP命令来代替RDTSC,前者开销虽然略高,但胜在稳定好用。另外,如果不想用这个指令,还可以用memory barrier技术(后面的文章中我们将详细解释该技术)或者CPUID指令来实现,不过这两者我都没试,据说开销也不小,详细资料可以就参见参考资料中的wiki页面和intel的官方手册。
下面这个程序可以用来测试RDTSC和RDTSCP指令的性能:
#include
#include
#include
#include
#include
#include
// 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命令查看。
再论 Time stamp counter - 一念天堂 - 博客园
Pitfalls of TSC usage | Oliver Yang
linux - rdtsc accuracy across CPU cores - Stack Overflow
#include
#include
#include
#include
/*
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 [email protected]) 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.
* ([email protected])
*
* [email protected] "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