天天看點

Celero:一個 C++ 的基準測試管理庫

Celero:一個 C++ 的基準測試管理庫

介紹

對代碼進行持續性開發和有意義的基準測試是一個複雜的任務。雖然測試工具本身(Intel® VTune™ Amplifier, SmartBear AQTime, Valgrind)與應用程式沒有相關性,但是它們在某些時候對一些小團隊,或者說是一些繁瑣的工作來說還是很重要的。這個Celero項目,主要是要建倉一個小型的程式庫,使它可以在加入 C++ 工程和對代碼進行基準測試時能夠非常容易地去重建,分享,并允許在獨立的運作程序、開發者或者是工程間進行比較。Celero 使用一個與 GoogleTest 相似的構架,使得他的 API 很容易地使用,并融入一個工程中。當你在開發過程中進行自動測試時,自動化基準将會扮演舉足輕重的作用。

背景

通常,編寫基準的目的是為了測量一段代碼的性能。基準有助于比較解決同一問題的不同方案并選擇其中最合适的一個。其他時候,基準能以一種很有意義的方式突出設計或算法改變後的性能影響。

通過測量代碼的性能,可以為性能消除那些你以為是正确方案的錯誤。隻有通過測量,你才能确定比如用一張查找表比計算一個值更快。這種傳聞(通常是重複的)可能導緻糟糕的設計決策,最終生成效率更慢的代碼。

編寫好的基準的目的是為了消除所有噪音和開銷,并且隻測量被測代碼。在測量中,噪音源包括時鐘分辨率噪音、作業系統背景操作、測試設定/清除、架構開銷以及其他不相幹的系統行為。

理論上,我們想測量被測代碼的執行時間“t”。實際上,我們測到的是“t”與所有噪音的和。

Celero:一個 C++ 的基準測試管理庫

這些影響我們測量的“t”的不相幹因素會随時間變化而波動。是以,我們想試圖将“t”隔離出來。實作這樣的方法是通過多次測量,但隻保留最小的總時間。這個最小的總時間必然是受噪音幹擾最小且最接近時間“t”的值。

一旦得到這個測量值,在進行隔離就沒什麼意義了。建立一個基線測試作比較很重要。基線通常應該是一個基于你正要測量出一個解決方案的問題的“經典”或“純粹”的方案。一旦有了一個基線,你花時間去比較你的算法就有意義。簡單地說,你中意的排序算法(fSort)不能在10毫秒内自動排序100萬元素。然而,相對于一個經典的排序算法基線如快速排序(qSort),你可以說,fSort處理100萬元素比去Sort快50%。這是個很有意義且強大的測量。

實作

Celero大量使用Visual C++和GCC4.7都支援的C++11特性。這對使代碼整潔、便攜大有幫助。為了使代碼更容易采用,使用者所需的所有定義都放在一個命名空間為celero的單一頭檔案:Celero.h裡。

Celero.h裡包含将每個使用者基準用例轉換為獨特的、具有與之相關測試固件的類(如果有的話),然後登記測試用例到一個工廠。這些宏自動将基線測試用例與相關的測試基準關聯起來,這樣,在運作時基準相關的資料就可以被算出。這一關聯由測試向量來維護。

測試向量利用PImpl慣語來隐藏實作并且保持包含Celero.h的開銷降到最小。

Celero将結果輸出到指令行。因為顔色是有用的(可能有助于主觀因素/結果的可讀性),是以std::cout調用了自身之外的一些東西。Console.h定義了一個簡單的顔色函數,SetConsoleColor,它可以被celero::print命名空間中的函數用來格式化程式的輸出。

測量基準的執行時間位于TestFixturebase類中,而且所有基準的寫法都是以此為基礎派生出來的。首先,測試夾具(譯注:test fixture 是檢測被測試系統時所需要的所有東西)的建立代碼被執行。接着,測試的開始時間被擷取到并以毫秒的形式用一個unsigned long儲存起來。這麼做是為了減少浮點指針錯誤。再下一步,指定次數的操作(疊代)被執行。結束時,結束時間被抓取到,測試夾具銷毀,本次執行的測量時間被傳回,并且這個結果會被儲存下來。

無論指定多少個樣本,這個循環就像這樣重複。如果樣本沒有指定(為零),那麼測試将會重複運作直到一秒鐘,或者至少采集了30個樣本。當寫到代碼的這個特定部分時,顯然這裡存在有一種“if-else”的關系。不管怎麼說,大量的代碼都是如此重複着“if”與"else"的片段。這裡可以使用一個老式的函數,但是利用std::function來定義一個 匿名函數(lambda)再自然不過,這樣就可以調用它并使所有的代碼整潔。(c++ 11真是一個奇妙的東西)最後,結果被列印到螢幕。

使用代碼

Celero 使用 CMake 去提供跨平台構件。由于它使用得是 C++ 11,因而需要請求一個現在流行的編譯器(Visual C++ 2012 or GCC 4.7+)。

一旦你的項目中加入了 Celero,你能夠建立專門的基準項目和源檔案。為了友善,一個頭檔案和aCELERO_MAINmacro 能提供給 main() ,來幫助你的基準項目自動去執行所有的基準測試。

下面有一個簡單的 Celero 基準的例子:

#include <celero/Celero.h>

CELERO_MAIN;

// 運作一個自動的基線。  

// Celero 保證能提供足夠的采樣來得到一個合理的測量結果

BASELINE(CeleroBenchTest, Baseline, 0, 7100000)

{

    celero::DoNotOptimizeAway(static_cast<float>(sin(3.14159265)));

}

// 運作一個自動測試。  

BENCHMARK(CeleroBenchTest, Complex1, 0, 7100000)

    celero::DoNotOptimizeAway(static_cast<float>(sin(fmod(rand(), 3.14159265))));

// 運作一個手動測試。這是對一個樣本進行每秒 7100000 次操作。

// Celero 保證能提供足夠的采樣來得到一個合理的測量結果。

BENCHMARK(CeleroBenchTest, Complex2, 1, 7100000)

// 運作一個手動測試。這是對 60 個樣本進行每秒 7100000 次操作。

BENCHMARK(CeleroBenchTest, Complex3, 60, 7100000)

這段代碼中我們做的第一件事情,就是定義一個BASELINE測試用例。這個模版有四個參數:

BASELINE(GroupName, BaselineName, Samples, Operations)

* GroupName- 基準組的名字。它是用來将運作與結果和它們相應的基線測量收集到一起。

* BaselineName- 為了報表目的的基線的名字。

* Samples- 對測試代碼進行給定次數的操作,對這些操作的總的循環運作次數。

* Operations- 你希望對每個樣本運作測試代碼的次數。

這裡的樣本與操作是用來測量非常快的代碼的。例如,如果你知道基準測試中的代碼運作時間小于100毫秒,那麼在進行一次測量之前,這個操作次數将會指定代碼運作"operations"次。而Samples則定義了要做多少次測量。

Celero允許指定零樣本也有助于此。零樣本将告訴Celero,基于完成指定次數操作所需要的時間,采集一些具有統計學意義數量的樣本。這些數字将會在運作時給出。

celero::DoNotOptimizeAway模版是用來保證優化編譯器不會消除你的函數或代碼。由于這個功能被用于所有的基準樣本以及它們的基線,是以相比較起來其中的額外時間消耗可以忽略不計。

在基線被定義之後,接着被定義的是各種各樣的基準。BENCHMARK宏的文法與普通宏的文法完全相同。

結果

示例項目被設定為一旦成功編譯就自動執行基準測試代碼。在我的PC上運作這個基準測試得到的是以下的輸出:

[  CELERO  ]

[==========]

[ STAGE    ] Baselining

[ RUN      ] CeleroBenchTest.Baseline -- Auto Run, 7100000 calls per run.

[   AUTO   ] CeleroBenchTest.Baseline -- 30 samples, 7100000 calls per run.

[     DONE ] CeleroBenchTest.Baseline  (0.517049 sec) [7100000 calls in 517049 usec] [0.072824 us/call] [13731773.971132 calls/sec]

[ STAGE    ] Benchmarking

[ RUN      ] CeleroBenchTest.Complex1 -- Auto Run, 7100000 calls per run.

[   AUTO   ] CeleroBenchTest.Complex1 -- 30 samples, 7100000 calls per run.

[     DONE ] CeleroBenchTest.Complex1  (2.192290 sec) [7100000 calls in 2192290 usec] [0.308773 us/call] [3238622.627481 calls/sec]

[ BASELINE ] CeleroBenchTest.Complex1 4.240004

[ RUN      ] CeleroBenchTest.Complex2 -- 1 run, 7100000 calls per run.

[     DONE ] CeleroBenchTest.Complex2  (2.199197 sec) [7100000 calls in 2199197 usec] [0.309746 us/call] [3228451.111929 calls/sec]

[ BASELINE ] CeleroBenchTest.Complex2 4.253363

[ RUN      ] CeleroBenchTest.Complex3 -- 60 samples, 7100000 calls per run.

[     DONE ] CeleroBenchTest.Complex3  (2.192378 sec) [7100000 calls in 2192378 usec] [0.308786 us/call] [3238492.632201 calls/sec]

[ BASELINE ] CeleroBenchTest.Complex3 4.240175

[ STAGE    ] Completed.  4 tests complete.

首次運作的測試将作為整個組測試的基線。這個基線顯示出它是一個“自動測試”,暗示着由Celero來衡量并決策運作測試代碼的次數。這個案例中,在我們的測試裡它要運作7100000次代碼疊代總共30次。(每調用7100000次測量一次,這樣30次以後取最小的時間。)整個測量需要0.517049秒。基于這個結果,可以測量出每次對基準代碼的調用需要0.072824毫秒。

在基線測試結束之後,将運作每個單獨的測試。每個測試的運作與測量方式都是相同的,不過有一個額外的度量報告:基線。将它運作基準測試代碼所需要的時間與基線進行比較。這裡的資料顯示,CeleroBenchTest.Complex1比基線運作需要的時間多了4.240004倍。

課外讀物

  • GitHub 項目點選 這裡 .
  • 基準應該總是要在發行版本中運作。但這絕對不是要求建立一個調試工程,也不是要改變原本的結果。編譯器(進行優化)将對你的代碼的執行起到非常好的作用。
  • Celero 在 Doxygen 中存放了 API 文檔。
  • Celero 對每一個基線組固定測試。
  • 當監聽到第三個存儲槽時,執行代碼使基準運作得更快。這是多麼有趣的事啊!

繼續閱讀