在某些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了,可以确定这里的频率估计函数可能计算错了。

mm_freq.png
mm_freq.png

进一步查看这个频率估计函数,是使用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之前配合使用,可以排除乱序执行的影响。

RDTSCPRDTSC的一个改进版本,会等待之前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

这在某种程度上也算是没锁上频,不过这个倒是不打紧,需要注意的就是在实际测试之前要进行预热。