VC獲取精確時間的作法

聲明:本文章是我整合網上的資料而成的,其中的大部分文字不是我所爲的,我所起的做用只是概括整理並添加個人一些見解。很是感謝引用到的文字的做者的辛勤勞動,所參考的文獻在文章最後我已一一列出。html

 

    對關注性能的程序開發人員而言,一個好的計時部件既是益友,也是良師。計時器既能夠做爲程序組件幫助程序員精確的控制程序進程,又是一件有力的調試武器,在有經驗的程序員手裏能夠儘快的肯定程序的性能瓶頸,或者對不一樣的算法做出有說服力的性能比較。 

    在Windows平臺下,經常使用的計時器有兩種,一種是timeGetTime多媒體計時器,它能夠提供毫秒級的計時。但這個精度對不少應用場合而言仍是太粗糙了。另外一種是QueryPerformanceCount計數器,隨系統的不一樣能夠提供微秒級的計數。對於實時圖形處理、多媒體數據流處理、或者實時系統構造的程序員,善用QueryPerformanceCount/QueryPerformanceFrequency是一項基本功。 

本文要介紹的,是另外一種直接利用Pentium CPU內部時間戳進行計時的高精度計時手段。如下討論主要得益於《Windows圖形編程》一書,第 15頁-17頁,有興趣的讀者能夠直接參考該書。關於RDTSC指令的詳細討論,能夠參考Intel產品手冊。本文僅僅做拋磚之用。 
在 Intel Pentium以上級別的CPU中,有一個稱爲「時間戳(Time Stamp)」的部件,它以64位無符號整型數的格式,記錄了自CPU上電以來所通過的時鐘週期數。因爲目前的CPU主頻都很是高,所以這個部件能夠達到納秒級的計時精度。這個精確性是上述兩種方法所沒法比擬的。 

在Pentium以上的CPU中,提供了一條機器指令RDTSC(Read Time Stamp Counter)來讀取這個時間戳的數字,並將其保存在EDX:EAX寄存器對中。因爲EDX:EAX寄存器對剛好是Win32平臺下C++語言保存函數返回值的寄存器,因此咱們能夠把這條指令當作是一個普通的函數調用。像這樣: 

inline unsigned __int64 GetCycleCount() 

__asm RDTSC 


可是不行,由於RDTSC不被C++的內嵌彙編器直接支持,因此咱們要用_emit僞指令直接嵌入該指令的機器碼形式0X0F、0X31,以下: 

inline unsigned __int64 GetCycleCount() 

__asm _emit 0x0F 
__asm _emit 0x31 


之後在須要計數器的場合,能夠像使用普通的Win32 API同樣,調用兩次GetCycleCount函數,比較兩個返回值的差,像這樣: 

unsigned long t; 
t = (unsigned long)GetCycleCount(); 
//Do Something time-intensive ... 
t -= (unsigned long)GetCycleCount(); 

     《Windows圖形編程》第15頁編寫了一個類,把這個計數器封裝起來。有興趣的讀者能夠去參考那個類的代碼。做者爲了更精確的定時,作了一點小小的改進,把執行RDTSC指令的時間,經過連續兩次調用GetCycleCount函數計算出來並保存了起來,之後每次計時結束後,都從實際獲得的計數中減掉這一小段時間,以獲得更準確的計時數字。但我我的以爲這一點點改進意義不大。在個人機器上實測,這條指令大概花掉了幾十到100多個週期,在 Celeron 800MHz的機器上,這不過是十分之一微秒的時間。對大多數應用來講,這點時間徹底能夠忽略不計;而對那些確實要精確到納秒數量級的應用來講,這個補償也過於粗糙了。 

程序員

      我從《Windows圖形編程》上把這個類的源碼拷貝了下來供你們看看,下面是使用RDTSC指令的CPU時鐘循環秒錶類:web

 

  1. // Timer.h
  2. #pragma once
  3. inline unsigned __int64 GetCycleCount(void)
  4. {
  5.     _asm  _emit 0x0F
  6.     _asm  _emit 0x31
  7. }
  8. class KTimer
  9. {
  10.     unsigned __int64 m_startcycle;
  11. public:
  12.   
  13.       unsigned __int64 m_overhead;
  14.       KTimer(void)
  15.       {
  16.           m_overhead = 0;
  17.           Start();
  18.           m_overhead  = Stop();
  19.       }
  20.       void Start(void)
  21.       {
  22.           m_startcycle = GetCycleCount();
  23.       }
  24.       unsigned __int64 Stop(void)
  25.       {
  26.           return GetCycleCount()-m_startcycle-m_overhead;
  27.       }
  28. };

 

這個方法的優勢是: 

1.高精度。能夠直接達到納秒級的計時精度(在1GHz的CPU上每一個時鐘週期就是一納秒),這是其餘計時方法所難以企及的。 

2. 成本低。timeGetTime 函數須要連接多媒體庫winmm.lib,QueryPerformance* 函數根據MSDN的說明,須要硬件的支持(雖然我尚未見過不支持的機器)和KERNEL庫的支持,因此兩者都只能在Windows平臺下使用(關於DOS平臺下的高精度計時問題,能夠參考《圖形程序開發人員指南》,裏面有關於控制定時器8253的詳細說明)。但RDTSC指令是一條CPU指令,凡是i386平臺下Pentium以上的機器均支持,甚至沒有平臺的限制(我相信i386版本UNIX和Linux下這個方法一樣適用,但沒有條件試驗),並且函數調用的開銷是最小的。
算法

(這裏我想說的是:照這樣看,跨平臺也只能說是操做系統平臺,不能跨硬件平臺,就是說只能用在Intel Pentium以上的機器)編程



3. 
具備和CPU主頻直接對應的速率關係。一個計數至關於1/(CPU主頻Hz數)秒,這樣只要知道了CPU的主頻,能夠直接計算出時間。這和 QueryPerformanceCount不一樣,後者須要經過QueryPerformanceFrequency獲取當前計數器每秒的計數次數才能換算成時間。 

這個方法的缺點是: 

1.現有的C/C++編譯器多數不直接支持使用RDTSC指令,須要用直接嵌入機器碼的方式編程,比較麻煩。 

2.數據抖動比較厲害。其實對任何計量手段而言,精度和穩定性永遠是一對矛盾。若是用低精度的timeGetTime來計時,基本上每次計時的結果都是相同的;而RDTSC指令每次結果都不同,常常有幾百甚至上千的差距。這是這種方法高精度自己固有的矛盾。
windows

 

(這裏數據抖動確實是一個大問題,我遇到過這樣一種狀況,好比測試a和b兩種算法,因爲數據抖動,有時a比b耗時少,有時b比a耗時少。我想過兩種測試辦法:ide

(1)增多測試次數,好比對a和b兩種算法各測試10次,看a比b耗時少的次數和b比a耗時少的次數哪一個多,以此斷定哪一個算法效率高。函數

(2)增大測試數據量,我想一增大測試數據量,算法效率的差別就會顯現出來)

關於這個方法計時的最大長度,咱們能夠簡單的用下列公式計算: 

自CPU上電以來的秒數 = RDTSC讀出的週期數 / CPU主頻速率(Hz) 

64位無符號整數所能表達的最大數字是1.8×10^19,在個人Celeron 800上能夠計時大約700年(書中說能夠在200MHz的Pentium上計時117年,這個數字不知道是怎麼得出來的,與個人計算有出入)。不管如何,咱們大可沒必要關心溢出的問題。 

性能

下面是幾個小例子,簡要比較了三種計時方法的用法與精度測試

 

  1. #include <stdio.h> 
  2. #include "KTimer.h" 
  3. main() 
  4. unsigned t; 
  5. KTimer timer; 
  6. timer.Start(); 
  7. Sleep(1000); 
  8. t = timer.Stop(); 
  9. printf("Lasting Time: %d/n",t); 
  10. //Timer2.cpp 使用了timeGetTime函數 
  11. //需包含<mmsys.h>,但因爲Windows頭文件錯綜複雜的關係 
  12. //簡單包含<windows.h>比較偷懶:) 
  13. //編譯行:CL timer2.cpp /link winmm.lib 
  14. #include <windows.h> 
  15. #include <stdio.h> 
  16. main() 
  17. DWORD t1, t2; 
  18. t1 = timeGetTime(); 
  19. Sleep(1000); 
  20. t2 = timeGetTime(); 
  21. printf("Begin Time: %u/n", t1); 
  22. printf("End Time: %u/n", t2); 
  23. printf("Lasting Time: %u/n",(t2-t1)); 
  24. //Timer3.cpp 使用了QueryPerformanceCounter函數 
  25. //編譯行:CL timer3.cpp /link KERNEl32.lib 
  26. #include <windows.h> 
  27. #include <stdio.h> 
  28. main() 
  29. LARGE_INTEGER t1, t2, tc; 
  30. QueryPerformanceFrequency(&tc); 
  31. printf("Frequency: %u/n", tc.QuadPart); 
  32. QueryPerformanceCounter(&t1); 
  33. Sleep(1000); 
  34. QueryPerformanceCounter(&t2); 
  35. printf("Begin Time: %u/n", t1.QuadPart); 
  36. printf("End Time: %u/n", t2.QuadPart); 
  37. printf("Lasting Time: %u/n",( t2.QuadPart- t1.QuadPart)); 
  38. // 這裏要計算時間(單位爲秒),應加上這一句
  39. double dTotalTime = (double)(t2.QuadPart-t1.QuadPart) / (double)tc.QuadPart;    //秒
  40. printf("耗時: %f/n", dTotalTime);

 

 

//以上三個示例程序都是測試1秒鐘休眠所耗費的時間 
file://測/試環境:Celeron 800MHz / 256M SDRAM 
// Windows 2000 Professional SP2 
// Microsoft Visual C++ 6.0 SP5 
//////////////////////////////////////////////// 

如下是Timer1的運行結果,使用的是高精度的RDTSC指令 
Lasting Time: 804586872 

如下是Timer2的運行結果,使用的是最粗糙的timeGetTime API 
Begin Time: 20254254 
End Time: 20255255 
Lasting Time: 1001 

如下是Timer3的運行結果,使用的是QueryPerformanceCount API 
Frequency: 3579545 
Begin Time: 3804729124 
End Time: 3808298836 
Lasting Time: 3569712 

古人說,舉一反三。從一本介紹圖形編程的書上獲得一個如此有用的實時處理知識,我感到很是高興。有美不敢自專,但願你們和我同樣喜歡這個輕便有效的計時器。

 

    網上有一種說法說

double dTotalTime=(double)(t2.QuadPart-t1.QuadPart)/(double)tc.QuadPart

可能有問題,好比說如今不少主板都有CPU頻率自動調整功能,主要是節能,尤爲在筆記本上,這樣除下來不能保證精確性。我不肯定這種說法是否準確,供你們研究

 

   上文主要摘自《使用CPU時間戳進行高精度計時》,其實除了上面提到的三種方法,還有一種經常使用固然沒有上面準確的辦法,就是使用GetTickCount函數,這種方法可以獲取毫秒級的時間,具體用法以下:

 

 

  1. DWORD startTime = GetTickCount();
  2. // do something 
  3. DWORD totalTime = GetTickCount() - startTime;

 

參考文獻:

《使用CPU時間戳進行高精度計時》     做者:zhangyan_qd

《Windows圖形編程》,(美)Feng Yuan 著

《VC中取得毫秒級的時間》,http://www.cppblog.com/humanchao/archive/2008/04/22/43322.html

相關文章
相關標籤/搜索