c++ windows下計時
多核時代不宜再用 x86 的 RDTSC 指令測試指令周期和時間
陳碩
Blog.csdn.net/Solstice
自從 Intel Pentium 加入 RDTSC 指令以來,這條指令是 micro-benchmarking
的利器,可以以極小的代價獲得高精度的 CPU 時鐘周期數(Time Stamp
Counter),不少介紹優化的文章[1]和書籍用它來比較兩段代碼的快慢。甚至有的代碼用 RDTSC 指令來計時,以替換
gettimeofday() 之類的系統調用。在多核時代,RDTSC 指令的準确度大大削弱了,原因有三:
- 不能保證同一塊主機闆上每個核的 TSC 是同步的;
- CPU 的時鐘頻率可能變化,例如筆記本電腦的節能功能;
- 亂序執行導緻 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>就可以了。