探討性能測試中的計時問題
摘要:本文結合作者在代碼性能測試工作中的經驗,介紹一組自己封裝的的計時函數。使用該組函數可以簡化測試工作,從而把更多的精力放在主要工作上,不需要過多地維護計時代碼,僅僅使用兩個宏就可以方便、精確地實現多個模塊、多方式的時間性能測試,并且計時結果以一個文本文件獨立保存,清晰直觀。
在系統測試時,尤其在需要測試算法或者某些模塊的運行時間時,往往需要調用一些時間函數庫(如VC中的timeGetTime等可以獲取毫秒級的時間),在待測試的模塊前后分別測試時間,然后,計算前后兩個時間的差值,就得到模塊的運行時間,如圖 1。 圖 1 一個典型的模塊計時方法
但是,使用原始的計時函數直接進行時間測試在很多復雜情況下不方便,如圖 1,當在一個模塊中有多個子模塊需要分別計時,所編寫的計時代碼甚至比原有的代碼還多,這增加了程序維護和閱讀的難度,容易出錯。作者結合自己在相關工作中的經驗,封裝了一組計時函數,共享給大家。該組函數有如下幾個優點:
- 計時精確:封裝的是高精度的計時API函數QueryPerformanceCounter(),該函數根據硬件定時器的頻率,理論上可以得到微秒(us)級精度的計時結果;
- 使用簡單:只用在待測試的模塊前后加上兩個宏BM_START和BM_END,不需要對結果進行計算,也不需要考慮對各個模塊測試結果數據的維護,這些操作已經被封裝。
- 結果輸出獨立:在系統運行結果時,只需要調用一個函數就可以把計時結果保存在一個文本文件里,如圖 5和圖 8所示。
1. 高精度計時函數
在Windows系統下,程序員通常可以使用多種方式來進行時間控制:如使用前文提到的timeGetTime()函數,或者使用GetTickCount()函數,又或實現WM_TIMER消息的映射等等。但是這些方法得到的時間精度都有一定的局限性,為了增加下文將到介紹的計時函數庫的適用性,本文采用高精度的時控API函數QueryPerformanceCounter()。
計時之前,調用QueryPerformanceFrequency()函數獲得機器內部定時器的時鐘頻率,然后在需要計時的模塊前后分別調用QueryPerformanceCounter()函數,利用兩次獲得的計數之差獲得時鐘頻率,計算出模塊的運行時間。代碼如圖 2:
圖 2 精確計時代碼段
2. 封裝計時函數
2.1. 數據結構
為了維護計時結果,我們定義如下幾個數據:
double gStarts[BENCHMARK_MAX_COUNT];
double gEnds[BENCHMARK_MAX_COUNT];
double gCounters[BENCHMARK_MAX_COUNT];
double dfFreq = 1;
其中,BENCHMARK_MAX_COUNT定義了需要計時的模塊總數,20表示最多可以定時20個模塊,該值可以根據具體應用而定。gStarts和gEnds分別用于保存開始計時和終止計時的計數器的值,gCounters用來保存計時結果。全局變量dfFreq用來保存上文介紹的時鐘頻率,如圖 2所示。
2.2. 初始化InitBenchmark()
初始化函數InitBenchmark()包括兩部分內容:
- 對數組gStarts, gEnds, gCounter清零;
- 獲得機器內部定時器時鐘頻率。
InitBenchmark()代碼如下所示:
ResetBenchmarkCounters();
GetClockFrequent();}
該函數一般在程序運行最初調用。
2.3. 開始計時BMTimerStart()
開始計時函數BMTimerStart()放在計時模塊的開始,函數定義如下:
LARGE_INTEGER litmp;
QueryPerformanceCounter(litmp);
gStarts[iModel] = litmp.QuadPart;}
其中參數iModel表示當前計時的模塊序號,0=iModel=BENCHMARK_MAX_COUNT;為了簡化調用代碼,我們給出一個宏定義如下:
BMTimerStart(t);
2.4. 終止計時BMTimerEnd()
終止計時函數BMTimerEnd()放在計時模塊的結束,函數定義如下:
LARGE_INTEGER litmp;
QueryPerformanceCounter(litmp);
gEnds[iModel] = litmp.QuadPart;
gCounters[iModel] += (((gEnds[iModel] - gStarts[iModel]) / dfFreq) * 1000000);}
參數iModel同BMTimerStart()。本函數首先獲取當前的時鐘數,然后除以dfFreq得到運行時間。對于最后一條語句:
要注意兩點:
- 用“+=”而不是“=”,這個看似簡單的代替,可以實現對同一個模塊的重復計時,后文3.3節列舉的情況;
- 乘以1000000,表示計時單位為微秒(us)。
類似BMTimerStart(),同樣為BMTimerEnd()定義一個宏:
BMTimerEnd(t);
2.5. 結果輸出WriteData()
以一個文本文件(見圖 5和圖 8)把全局變量gCounters中的所有值輸出,該函數一般在程序結束處調用,如圖 4中最后一行代碼所示。由于篇幅限制,具體實現代碼請參考源程序。
3. 計時測試實例
3.1. 多個模塊計時
圖 3展示了嵌套計時以及對一個函數中多個模塊進行計時的代碼,圖中可以看到,利用輸入參數我們對計時模塊進行統一編號,測試代碼相對圖 1更清晰、直觀。 圖 3 用我們的函數實現嵌套計時
3.2. 循環內部計時
圖 4中的代碼展示了我們對循環體內每次執行運算的計時,只需簡單地給出參數 就可以得到從1到20的階乘的每次計算計算時間,計時結果輸出為文件“D:\log.txt”,如圖 5。
圖 4 對循環中每次運算的計時
圖 5 計時結果1
3.3. 循環累加計時
從圖 5可以看到,由于現代計算機處理速度越來越快,一些簡單運算的模塊,微秒的計時單位幾乎都不夠精確,因此,一種常用的測試方法就是對同一模塊進行N (N 取1000,10000等)次重復執行。使用本文介紹的計時函數,我們可以采用兩種方式對這種情況進行測試,代碼分別如圖 6和圖 7,請注意二者的區別,并請讀者分析為何圖 7中的方法也是可行的。 N次運算計時結果如圖 8。 圖 6 累加計時1
圖 7 累加計時2
圖 8 計時結果2
4.結束語
本文實現了一組計時函數的封裝,并給出幾種特殊情況下的測試實例,實驗表明該組函數可以滿足各種復雜情況下的計時,能夠很方便地應用的實際的測試工作中。當然,還可以進一步封裝成一個計時類,留給讀者們自己去做。