天天看點

c++ windows下計時 - c_dragon

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>就可以了。