c++ windows下計時


多核時代不宜再用 x86 的 RDTSC 指令測試指令周期和時間

 

陳碩
Blog.csdn.net/Solstice

 

自從 Intel Pentium 加入 RDTSC 指令以來,這條指令是 micro-benchmarking 的利器,可以以極小的代價獲得高精度的 CPU 時鍾周期數(Time Stamp Counter),不少介紹優化的文章[1]和書籍用它來比較兩段代碼的快慢。甚至有的代碼用 RDTSC 指令來計時,以替換 gettimeofday() 之類的系統調用。在多核時代,RDTSC 指令的准確度大大削弱了,原因有三:

 

  1. 不能保證同一塊主板上每個核的 TSC 是同步的;
  2. CPU 的時鍾頻率可能變化,例如筆記本電腦的節能功能;
  3. 亂序執行導致 RDTSC 測得的周期數不准,這個問題從 Pentium Pro 時代就存在。

 

這些都影響了 RDTSC 的兩大用途,micro-benchmarking 和計時。

 

RDTSC 一般的用法是,先后執行兩次,記下兩個 64-bit 整數 start 和 end,那么 end-start 代表了這期間 CPU 的時鍾周期數。

 

在多核下,這兩次執行可能會在兩個 CPU 上發生,而這兩個 CPU 的計數器的初值不一定相同(由於完成上電復位的准確時機不同),(有辦法同步,見[3]),那么就導致 micro-benchmarking 的結果包含了這個誤差,這個誤差可正可負,取決於先執行的那塊 CPU 的時鍾計數器是超前還是落后。

 

另外,對於計時這個用途,時間 = 周期數 / 頻率,由於頻率可能會變(比如我的筆記本的 CPU 通常半速運行在 800MHz,繁忙的時候全速運行在 1.6GHz),那么測得的時間也就不准確了。有的新 CPU 的 RDTSC 計數頻率是恆定的,那么時鍾是准了,那又會導致 micro-benchmarking 的結果不准,見 [2]。還有一個可能是掉電之后恢復(比如休眠),那么 TSC 會清零。 總之,用 RDTSC 來計時是不靈的。

 

亂序執行這個問題比較簡單 [1],但意義深遠:在現代 CPU 的復雜架構下,測量幾條或幾十條指令的耗時是無意義的,因為觀測本身會干擾 CPU 的執行(cache, 流水線, 多發射,亂序, 猜測),這聽上去有點像量子力學系統了。要么我們以更宏觀的指標來標示性能,把"花 xxx 個時鍾周期"替換"每秒處理 yyy 條消息"或"消息處理的延時為 zzz 毫秒";要么用專門的 profiler 來減小對觀測結果的影響(無論是 callgrind 這種虛擬 CPU,還是 OProfile 這種采樣器)。

 

雖然 RDTSC 廢掉了,性能測試用的高精度計時還是有辦法的 [2],在 Windows 用 QueryPerformanceCounter 和 QueryPerformanceFrequency,Linux 下用 POSIX 的 clock_gettime 函數,以 CLOCK_MONOTONIC 參數調用。或者按文獻 [3] 的辦法,先同步 TSC, 再使用它。(我不知道現在最新的 Linux 官方內核是不是內置了這個同步算法。也不清楚校准后的兩個 CPU 的“鍾”會不會再次失步。)

 

通過調用SetThreadAffinityMask,就能為各個線程設置親緣性屏蔽: 


  DWORD_PTR  SetThreadAffinityMask  ( 
      HANDLE  hThread,                                  //  handle  to  thread 
      DWORD_PTR  dwThreadAffinityMask    //  thread  affinity  mask 
  ); 
  該函數中的  hThread  參數用於指明要限制哪個線程,  dwThreadAffinityMask用於指明該線程 
  能夠在哪個CPU上運行。dwThreadAffinityMask必須是進程的親緣性屏蔽的相應子集。返回值 
  是線程的前一個親緣性屏蔽。例如,可能有一個包含4個線程的進程,它們在擁有4個CPU的計算機上運行。如果這些線程中的一個線程正在執行非常重要的操作,而你想增加某個CPU始終可供它使用的可能性,為此你對其他3個線程進行了限制,使它們不能在CPU  0上運行,而只能在CPU  1、2和3上運行。因此,若要將3個線程限制到CPU  1、2和3上去運行,可以這樣操作: 
   
  //線程0只能在cpu  0上運行 
  SetThreadAffinityMask(hThread0,0x00000001); 
  //線程1,2,3只能在cpu  1,2,3上運行 
  SetThreadAffinityMask(hThread1,0x0000000E); 
  SetThreadAffinityMask(hThread2,0x0000000E); 
  SetThreadAffinityMask(hThread3,0x0000000E); 
 
 
本文對Windows平台下常用的計時函數進行總結,包括精度為秒、毫秒、微秒三種精度的 5種方法。分為在標准C/C++下的二種time()及clock(),標准C/C++所以使用的time()及clock()不僅可以用在 Windows系統,也可以用於Linux系統。在Windows系統下三種,使用Windows提供的API接口timeGetTime()、 GetTickCount()及QueryPerformanceCounter()來完成。文章最后給出了5種計時方法示例代碼。

 

標准C/C++的二個計時函數time()及clock()

 

time_t time(time_t *timer);

返回以格林尼治時間(GMT)為標准,從1970年1月1日00:00:00到現在的此時此刻所經過的秒數。

time_t實際是個long長整型typedef long time_t;

頭文件:#include <time.h>

 

clock_t clock(void);

返回進程啟動到調用函數時所經過的CPU時鍾計時單元(clock tick)數,在MSDN中稱之為掛鍾時間(wal-clock),以毫秒為單位。

clock_t實際是個long長整型typedef long clock_t;

頭文件:#include <time.h>

 

 

Windows系統API函數

timeGetTime()、GetTickCount()及QueryPerformanceCounter()

 

DWORD timeGetTime(VOID);

返回系統時間,以毫秒為單位。系統時間是從系統啟動到調用函數時所經過的毫秒數。注意,這個值是32位的,會在0到2^32之間循環,約49.71天。

頭文件:#include <Mmsystem.h>            

引用庫:#pragma comment(lib, "Winmm.lib")  

 

DWORD WINAPI GetTickCount(void);

這個函數和timeGetTime()一樣也是返回系統時間,以毫秒為單位。

頭文件:直接使用#include <windows.h>就可以了。

 

高精度計時,以微秒為單位(1毫秒=1000微秒)。

先看二個函數的定義

BOOL QueryPerformanceCounter(LARGE_INTEGER *lpPerformanceCount);

得到高精度計時器的值(如果存在這樣的計時器)。

BOOL QueryPerformanceFrequency(LARGE_INTEGER *lpFrequency);

返回硬件支持的高精度計數器的頻率(次每秒),返回0表示失敗。

再看看LARGE_INTEGER

它其實是一個聯合體,可以得到__int64 QuadPart;也可以分別得到低32位DWORD LowPart和高32位的值LONG HighPart。

在使用時,先使用QueryPerformanceFrequency()得到計數器的頻率,再計算二次調用QueryPerformanceCounter()所得的計時器值之差,用差去除以頻率就得到精確的計時了。

頭文件:直接使用#include <windows.h>就可以了。


免責聲明!

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



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