在某些Benchmark或者计算Kernel的性能测试场景下,需要对绑定执行的CPU核进行锁频,以获得可以复现(reproducible)的结果,更好地实现横向对比,排除CPU频率动态调度对于性能表现的影响。当然这也只是我个人的一个猜想,对于锁频应该还有一些其他的需求,比如节能。
不过在最近,我在测试的时候遇到了一个频率没能锁住的“问题”。
测试服务器CPU是Intel(R) Xeon(R) Gold 6146 CPU @ 3.20GHz,有两个NUMA节点每个12个物理核,一共24个物理核。当我按照Archwiki上的指示配好acpi_cpufreq驱动和对应的频率配置信息后:
sudo cpupower frequency-set -g userspace
sudo cpupower frequenct-set -f 2.5GHz
跑了个vtune测试发现CPU的频率依然是在标称最高(3.2GHz),甚至有点时候会Boost到更高。这就奇了怪了,怎么这个驱动没有把频率锁住?
第一时间就想到了Boost机制可能还在工作,后来查阅了一下 Linux Kernel 的文档,果然是在X86架构下,Boost机制的触发是hardware-based,软件可能控不住。
The frequency boost mechanism may be either hardware-based or software-based. If it is hardware-based (e.g. on x86), the decision to trigger the boosting is made by the hardware (although in general it requires the hardware to be put into a special state in which it can control the CPU frequency within certain limits). If it is software-based (e.g. on ARM), the scaling driver decides whether or not to trigger boosting and when to do that.
在驱动里把boost关了就好了,另外还可以从BIOS选项中关闭Boost。AMD上是用 Core Performance Boost (CPB) 的机制。
sudo -i
echo 0 > /sys/devices/system/cpu/cpufreq/boost
在关了Boost之后,跑了一个MemoryMountain的测试,里面自带的频率估计函数的输出还是在3.2GHz附近,表现出频率没有锁住的“问题”。这个时候跑vtune已经是能够锁在2.5GHz了,可以确定这里的频率估计函数可能计算错了。
进一步查看这个频率估计函数,是使用RDTSC
读取头尾的 TimeStamp Counter (TSC) 值,然后夹一个sleep(sleeptime)
作差,实现频率估计的。
#if IS_x86
void access_counter(unsigned *hi, unsigned *lo)
{
/* Get cycle counter */
asm("rdtsc; movl %%edx,%0; movl %%eax,%1"
: "=r" (*hi), "=r" (*lo)
: /* No input */
: "%edx", "%eax");
}
void start_counter()
{
access_counter(&cyc_hi, &cyc_lo);
}
double get_counter()
{
unsigned ncyc_hi, ncyc_lo;
unsigned hi, lo, borrow;
double result;
/* Get cycle counter */
access_counter(&ncyc_hi, &ncyc_lo);
/* Do double precision subtraction */
lo = ncyc_lo - cyc_lo;
borrow = lo > ncyc_lo;
hi = ncyc_hi - cyc_hi - borrow;
result = (double) hi * (1 << 30) * 4 + lo;
if (result < 0) {
fprintf(stderr, "Error: Cycle counter returning negative value: %.0f\n", result);
}
return result;
}
#endif /* x86 */
这里就引出RDTSC
这个指令的使用了。
Intel的CPU有一个专门的 TimeStamp Counter 去统计核心经历的周期数,RDTSC
就是专门设计来读取这个 Counter 内容的。然而,由于乱序执行的存在,需要保证在RDTSC
执行的时机就是测试代码中的正确位置,简单直观的办法就是在读取TSC的前后设置fence。
Intel提供了CPUID
指令去实现当前还没退休的指令序列化(也就是等待他们退休)。CPUID
可以在任何的特权级进行使用,在RDTSC
之前配合使用,可以排除乱序执行的影响。
RDTSCP
是RDTSC
的一个改进版本,会等待之前issue的指令退休之后再对TSC进行读取,但是不能控制之后的指令是否提前执行。于是Intel白皮书给出的一个方案是:
asm volatile ("CPUID\n\t"
"RDTSC\n\t"
"mov %%edx, %0\n\t"
"mov %%eax, %1\n\t": "=r" (cycles_high), "=r" (cycles_low)::
"%rax", "%rbx", "%rcx", "%rdx");
/***********************************/
/*call the function to measure here*/
/***********************************/
asm volatile("RDTSCP\n\t"
"mov %%edx, %0\n\t"
"mov %%eax, %1\n\t"
"CPUID\n\t": "=r" (cycles_high1), "=r" (cycles_low1)::
"%rax", "%rbx", "%rcx", "%rdx");
这个白皮书是2010年的,比较老了,最近的架构可以使用mfence
代替CPUID
,测试的额外开销更小。
另外关于TSC的在单核和多核之间的频率变化和同步问题,Intel 引入了一个constant_tsc
的CPUFLAG,可以在/proc/cpuinfo里查询到。有这个flag的CPU可以保证TSC的计数频率和CPU核心频率是解耦的,这样测出来的时间也就是稳定的。
参考细说RDTSC的坑里给出的结论:
- 如果你的cpuinfo里有constant_tsc的flag,那么无论在同一CPU不同核心之间,还是在不同CPU的不同核心之间,TSC都是同步的,可以随便用
- 如果你用的是Intel的CPU,但是cpuinfo里没有constant_tsc的flag,那么在同一处理器的不同核心之间,TSC仍然是同步的,但是不同CPU的不同核心之间不同步,尽量不要用
在没有workload的时候,CPU的频率会回落到BIOS上设置的最低频率1.2GHz。
watch cat /sys/devices/system/cpu/cpu*/cpufreq/scaling_cur_freq
这在某种程度上也算是没锁上频,不过这个倒是不打紧,需要注意的就是在实际测试之前要进行预热。
wow,好帅哦