ARMv8的精确测量时钟周期
一、TSC (Time Stamp Counter)
TSC是Time Stamp Counter(时间戳计数器)的缩写,它是 Inter X86 架构上的一个计数器,它记录自启动以来处理器消耗的时钟周期数。
在每个时钟到来时,该计数器自动加一。
因为 TSC 随着处理器周期速率的变化而变化,所以它提供了非常高的精确度。它经常被用来分析和检测代码。
TSC 的值可以通过在X86指令rdtsc指令来读取。
在X86上的内联汇编实现如下:
uint64_t Rdtsc(void)
{
uint32_t lo, hi;
__asm__ __volatile__ ("rdtsc" : "=a" (lo), "=d" (hi));
return (uint64_t)hi << 32 | lo;
}
二、ARM Generic Timer
通用计时器(ARM Generic Timer)为 ARM 内核提供了一个标准化的计时器框架。通用计时器包括一个系统计数器(a System Counter)和一组单核计时器(a set of per-core timers),如下图所示:
系统计数器是一种常开设备,提供固定频率的递增计数(其计数频率和CPU主频无关)。系统计数器的计数值通过广播到系统中的每一个内核中,从而使内核对时间的流逝有一个共同的看法。其频率通常等于CPU外部时钟频率。
每个核心都有一组计时器(Each core has a set of timers)。这些计时器是作为比较器存在的,用来与系统计数器所广播值进行比较。软件可以配置计时器用来在将来的设定点产生中断或事件。软件还可以使用系统计数器值来添加时间戳,因为系统计数为所有内核提供了一个公共参考点。
注意
通用计时器仅测量时间的流逝。它不报告时间或日期。通常,SoC也会包含一个实时时钟(RTC)来表示时间和日期。
三、精确计时理论公式
设系统计数器的值为 N N N,其固定频率为 f f f,则精确流逝的时间 t t t(单位:秒)是:
t = N f t=\frac{N}{f} t=fN
设CPU主频为 F F F,则CPU精确流逝的时钟周期 C C C是:
C = t F C=tF C=tF
综合一下可得:
C = N F f C=N\frac{F}{f} C=NfF
又由于 f f f的值通常等于外频,那么 F f \frac{F}{f} fF就是倍频了,如果设倍频为 B B B的话,那么该式可转化为:
C = N B C=NB C=NB
那么这下牛逼了!
注意
这里测量出来的时钟周期其实和真实的CPU时钟周期之间还是存在着误差的,由于系统计数器每跳1下,CPU实际上跳了倍频的数目,所以误差在0到倍频之间。即:
C = N B 0 + B 或 C ∈ [ N B , ( N + 1 ) B ) C=NB_{0}^{+B} \qquad\text{或}\qquad C\in[NB,(N+1)B) C=NB0+B或C∈[NB,(N+1)B)
四、实现
1. 读取系统计数器固定频率
方法一:通过读取cntfrq_el0寄存器的值得到
long long cntfrq(void)
{
long long val;
__asm__ __volatile__("mrs %0, cntfrq_el0" : "=r" (val));
return val;
}
方法二:通过外部时钟频率(external clock)得到
系统计数器的固定频率其实采用的是外部时钟频率,而 x86 上的 TSC 采用的是主频,所以ARM上系统计数器的值和x86 上的 TSC还是有所区别。
By the way,外部时钟频率指得就是外频。所谓外部频率,指的就是系统总线频率,即 CPU 与外部(主板芯片组)交换数据、指令的工作时钟频率。
目前主流 CPU 的外频大多为 66MHz 与 100MHz 。鲲鹏920得外频为 100MHz。我们可以通过在 Linux 系统得 Bash 中输入 dmidecode |grep MHz 命令得到,该命令须在 root 权限下才能使用。
2. 读取系统计数器值
long long cntvct(void)
{
long long val;
__asm__ __volatile__("mrs %0, cntvct_el0" : "=r" (val));
return val;
}
3. 计算CPU时钟周期
方法一:原则上计算
#define CPUFRQ (2600000000) //2.6 GHz
long long cycle(void)
{
return cntvct()*CPUFRQ/cntfrq();
}
方法二:利用倍频计算
#define CPUMUL (26) //26倍频
long long cycle(void)
{
return cntvct()*CPUMUL;
}
4. 完整的测时钟周期实现
#define SYS_CPUFRQ (2600000000) //主频
#define SYS_BUSFRQ (100000000) //外频
#define SYS_FRQMULPER (26) //倍频
#define Cycle_Start(_cs) long long _cs; __asm__ __volatile__("mrs %0, cntvct_el0" : "=r" (_cs))
#define Cycle_End(_ce) long long _ce; __asm__ __volatile__("mrs %0, cntvct_el0" : "=r" (_ce))
#define Cycle_Elapse(_cu, _cs, _ce) long long _cu = ((_ce)-(_cs))*SYS_FRQMULPER