再論 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 只是程序員的工具箱中最基礎的一個。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM