天天看點

通過 RDTSC 指令從 CPU 寄存器中直接擷取系統時鐘

很多時候我們使用函數 ​

​gettimeofday​

​​ 以及 ​

​clock_gettime​

​​ 作為我們擷取 wall lock的時鐘函數。

因為這兩種函數是 glibc 提供的使用者封裝,簡單易用,而且能夠精确到 ns,對于大多數的時鐘需求場景都已經夠用了。

但是如果 我們的應用 調用時鐘頻繁 且 對 多線程場景下時鐘的單調性要求不是特别高的時候 ,以上提到的兩個 glibc 函數的開銷其實是有點大的。

  • ​gettimeofday​

    ​​ 底層是會調用到系統調用 ​

    ​sys_gettimeofday​

    ​,執行邏輯會陷入核心,将核心儲存的 walllock 和 jiffies 做一個綜合的精度計算,将計算結果從核心态拷貝到使用者态,交給timeval,這整個過程可以說開銷是比較大的。(需要系統調用)
  • ​clock_gettime​

    ​,這個函數同樣的需要執行一個系統調用 ,并且做 精度處理之後才傳回給使用者态。

舉個例子,我們分布式系統中的打點系統可以說非常重要,事關整個系統健康情況的展示,能夠幫助我們提前或者及時得準确發現系統中潛在的問題, 但是因為這一些打點需要在關鍵路徑上,往往對整個系統性能會有一定的損耗。而這一些打點 并不影響整個分布式系統中 為分布式事務選擇的時鐘(可以用兩套時鐘),隻需要能夠計算出一個操作前後的時間差即可,保持單線程内的單調性即可。

是以,如果我們的系統中所有的時鐘都采用 glibc 實作的兩種方式,對于頻繁的打點系統來說 代價實在是有點大,而且對關鍵路徑的性能都會有一定的影響。

今天介紹一個可以從 CPU 寄存器中直接擷取 walllock 的彙編指令 ​

​RDTSC​

​​。

我們目前大多數的伺服器系統是 AMD64 , x86_32/x86_64 位,可以通過如下 代碼擷取:

#include <stdio.h>
#include <inttypes.h>

int main() {
  uint64_t a,d,t;
  __asm__ volatile("rdtsc" : "=a"(a), "=d"(d));
  t = ((d << 32) | a);

  printf("%lu\n",t);
  return 0;
}      

拿到的 walllock 是 自1970.1.1 到現在 的CPU周期數,對于 GHZ 的CPU 來說機關就是ns。

我們的伺服器主機闆有一個即使機器斷電重新開機也不會重設的存儲單元 RTC,它會将目前機器到 1970.1.1 經過的時間轉化為目前機器的CPU時鐘周期并存儲下來。

我們的CPU 在完成一個時鐘周期之後會在 MSR(model-specific register) 寄存器中存儲計數(64位),我們的 RDTSC (read Time-Stamp Counter) 指令就是直接從 MSR 寄存器中取出計數資訊,其中的 高32位 放在 edx 寄存器中,低 32位放在eax 寄存器中。

這也就是為什麼 我們在AMD64 下執行這個指令需要在取到結果之後對存儲高 32位資料 的 ​

​d​

​ 變量做一個移位。

這樣我們就可以完整得實作一個 在AMD64 架構下的 rdtsc 指令的時間統計:

#include <stdio.h>
#include <inttypes.h>

static inline 
uint64_t rdtsc_time() {
  uint64_t a,d;
  __asm__ volatile("rdtsc" : "=a"(a), "=d"(d));
  return ((d << 32) | a);
}

static inline 
uint64_t clock_to_nsec(uint64_t begin, uint64_t end) {
  double clock_diff;

  //  這裡做一些簡單的單調性處理, 防止 end < begin.
  if (end < begin)
    return (0);
  clock_diff = (double)(end - begin);
  return ((uint64_t)(clock_diff);
}
            
int main() {
  uint64_t start_ns = rdtsc_time();
  // do some code
  ......
  uint64_t end_ns = rdtsc_time();
  fprintf(stdout, "code execute time(ns) : %lu\n", 
          clock_to_nsec(start_ns, end_ns));
  return 0;
}      
static inline uint64_t
rdtsc_time(void)
{
#if defined(__i386)  // intel 80386 架構
    {
        uint64_t x;

        __asm__ volatile("rdtsc" : "=A"(x));
        return (x);
    }
#elif defined(__amd64) // amd x86_64架構
    {
        uint64_t a, d;

        __asm__ volatile("rdtsc" : "=a"(a), "=d"(d));
        return ((d << 32) | a);
    }
#elif defined(__aarch64__) // arm 64為架構
    {
        uint64_t t;

        __asm__ volatile("mrs %0,  cntvct_el0" : "=r"(t));
        return (t);
    }
#else
    return (0);
#endif
}      

繼續閱讀