再论 Time stamp counter

在很多年以前,rdtsc 指令是在 x86 平台作 micro benchmark 的不二选择,它可以用很小的代价(基本上在几十个 CPU 周期)获得时间戳计数器 (time stamp counter) 的值,用来计算小代码段的性能是比较方便的。

然而来了多核时代,以及变频时代,由于 CPU 核心的主频不是恒定的了,time stamp counter 的值不代表时间了;同时,又由于 CPU 有多个核心,这些核心之间的 time stamp counter 不一定是同步的,所以当进程在核心之间迁移后,rdtsc 的结果就未必有意义。这一点在陈硕的文章里说得很清楚:http://blog.csdn.net/solstice/article/details/5196544

话说时光荏苒,又过了两年,两年前的结论,到了今天又未必合适。简单点说,在较新的处理器中实现了恒定时间戳计数器 (Invariant TSC),在 Intel 的处理器手册 (http://www.intel.com/content/dam/doc/manual/64-ia-32-architectures-software-developer-vol-3b-part-2-manual.pdf) 里的17.12.1节是这么说的:

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.

上文的 newer processors 实际上是说 Nehalem 以及之后的处理器,在 AMD 那边,是 Bacelona 之后的。在这些处理器上,可以大胆使用 rdtsc,不用担心变频,不用担心多核,time stamp counter 以恒定速率增加,嗯,就是这样。

但是,还是没有那么简单,要做 micro benchmark ,除了 TSC 要准,还要顾及 CPU 的乱序执行,尤其是如果被测的代码段较小,乱序执行可能让你的测试变得完全没意义。解决这个一般是“先同步,后计时”,在 wikipedia 上的一段代码就用 cpuid 指令来同步 (http://en.wikipedia.org/wiki/Time_Stamp_Counter)。但是 cpuid 指令本身的开销相当不小,至少在 rdtsc 的3倍以上,而且自身的开销并不稳定,结果是为了同步,引入了更多不确定性,不太值得。

另外一个办法是用 memory barrier,在 Intel CPU 上,指令 lfence 可以“保护” rdtsc,起到和上面 cpuid 指令同样的作用,但是开销小得多。在 rdtsc 指令前后都加上一个 lfence ,就可以比较精确的控制 rdtsc 的行为,不让乱序执行影响时间戳的获取。在 AMD CPU 上,需要使用指令 mfence ,开销稍高一些,但仍然比 cpuid 要低。只是这样,就要对 Intel 和 AMD 写不同的代码,比较麻烦。

其实,rdtsc 指令有个兄弟叫 rdtscp,它自身保证同步,虽然它的开销比 rdtsc 高一些,但非常稳定可靠,基本上只要可用,就应该用它,下面的代码展示了怎么用:

#include <cstdint>
#include <iostream>

__inline__ uint64_t perf_counter(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);
}

int main()
{
  uint64_t t1 = perf_counter();
  uint64_t t2 = perf_counter();
  std::cout << t1 << '\n' << t2 << std::endl;

  return 0;
}

在我的 Core i7 Q 720 笔记本上,t2 和 t1 的差值稳定在 95 左右,偏差基本不超过2,对于现代 CPU 的两条指令来讲,这是个非常稳定的结果。所以 micro benchmark 重新变得有一点意义:只要知道 rdtscp 在一台计算机的开销,再把结果减去这个开销,就可以得到被测代码段比较精确的开销值。

尽管 rdtscp 是个好东西,尽管 micro benchmark 可以做,但需要指出的是,在多核时代下,一段代码在一个线程上的开销低,不等于它就能充分利用 CPU 的能力,尤其不等于多线程上的性能就高。最简单的例子,如果代码造成核心之间的反复通信和同步,则很有可能核心越多,性能越低(可以看看关于 false sharing 的论述)。总的教训是:在现代 CPU 的乱序、多核、流水线、多级 cache 、各种预取等种种复杂性下,代码的性能特性已经远远不同于大多数程序员在教科书上学到的那些。所以如果真的需要优化一段代码,千万不要想当然,一定要有适当的 profile,micro benchmark 只是程序员的工具箱中最基础的一个。

你可能感兴趣的:(count)