"上一篇" 中我們初步體驗了google benchmark的使用,在本文中我們將更進一步深入瞭解google benchmark的常用方法。 本文索引 向測試用例傳遞參數 簡化多個類似測試用例的生成 使用參數生成器 向測試用例傳遞參數 之前我們的測試用例都只接受一個 類型的參數,如果我們需要給測試 ...
上一篇中我們初步體驗了google benchmark的使用,在本文中我們將更進一步深入瞭解google benchmark的常用方法。
本文索引
向測試用例傳遞參數
之前我們的測試用例都只接受一個benchmark::State&
類型的參數,如果我們需要給測試用例傳遞額外的參數呢?
舉個例子,假如我們需要實現一個隊列,現在有ring buffer和linked list兩種實現可選,現在我們要測試兩種方案在不同情況下的性能表現:
// 必要的數據結構
#include "ring.h"
#include "linked_ring.h"
// ring buffer的測試
static void bench_array_ring_insert_int_10(benchmark::State& state)
{
auto ring = ArrayRing<int>(10);
for (auto _: state) {
for (int i = 1; i <= 10; ++i) {
ring.insert(i);
}
state.PauseTiming(); // 暫停計時
ring.clear();
state.ResumeTiming(); // 恢復計時
}
}
BENCHMARK(bench_array_ring_insert_int_10);
// linked list的測試
static void bench_linked_queue_insert_int_10(benchmark::State &state)
{
auto ring = LinkedRing<int>{};
for (auto _:state) {
for (int i = 0; i < 10; ++i) {
ring.insert(i);
}
state.PauseTiming();
ring.clear();
state.ResumeTiming();
}
}
BENCHMARK(bench_linked_queue_insert_int_10);
// 還有針對刪除的測試,以及針對string的測試,都是高度重覆的代碼,這裡不再羅列
很顯然,上面的測試除了被測試類型和插入的數據量之外沒有任何區別,如果可以通過傳入參數進行控制的話就可以少寫大量重覆的代碼。
編寫重覆的代碼是浪費時間,而且往往意味著你在做一件蠢事,google的工程師們當然早就註意到了這一點。雖然測試用例只能接受一個benchmark::State&
類型的參數,但我們可以將參數傳遞給state對象,然後在測試用例中獲取:
static void bench_array_ring_insert_int(benchmark::State& state)
{
auto length = state.range(0);
auto ring = ArrayRing<int>(length);
for (auto _: state) {
for (int i = 1; i <= length; ++i) {
ring.insert(i);
}
state.PauseTiming();
ring.clear();
state.ResumeTiming();
}
}
BENCHMARK(bench_array_ring_insert_int)->Arg(10);
上面的例子展示瞭如何傳遞和獲取參數:
- 傳遞參數使用
BENCHMARK
巨集生成的對象的Arg
方法 - 傳遞進來的參數會被放入state對象內部存儲,通過
range
方法獲取,調用時的參數0是傳入參數的需要,對應第一個參數
Arg
方法一次只能傳遞一個參數,那如果一次想要傳遞多個參數呢?也很簡單:
static void bench_array_ring_insert_int(benchmark::State& state)
{
auto ring = ArrayRing<int>(state.range(0));
for (auto _: state) {
for (int i = 1; i <= state.range(1); ++i) {
ring.insert(i);
}
state.PauseTiming();
ring.clear();
state.ResumeTiming();
}
}
BENCHMARK(bench_array_ring_insert_int)->Args({10, 10});
上面的例子沒什麼實際意義,只是為了展示如何傳遞多個參數,Args
方法接受一個vector對象,所以我們可以使用c++11提供的大括弧初始化器簡化代碼,獲取參數依然通過state.range
方法,1對應傳遞進來的第二個參數。
有一點值得註意,參數傳遞只能接受整數,如果你希望使用其他類型的附加參數,就需要另外想些辦法了。
簡化多個類似測試用例的生成
向測試用例傳遞參數的最終目的是為了在不編寫重覆代碼的情況下生成多個測試用例,在知道瞭如何傳遞參數後你可能會這麼寫:
static void bench_array_ring_insert_int(benchmark::State& state)
{
auto length = state.range(0);
auto ring = ArrayRing<int>(length);
for (auto _: state) {
for (int i = 1; i <= length; ++i) {
ring.insert(i);
}
state.PauseTiming();
ring.clear();
state.ResumeTiming();
}
}
// 下麵我們生成測試插入10,100,1000次的測試用例
BENCHMARK(bench_array_ring_insert_int)->Arg(10);
BENCHMARK(bench_array_ring_insert_int)->Arg(100);
BENCHMARK(bench_array_ring_insert_int)->Arg(1000);
這裡我們生成了三個實例,會產生下麵的結果:
看起來工作良好,是嗎?
沒錯,結果是正確的,但是記得我們前面說過的嗎——不要編寫重覆的代碼!是的,上面我們手動編寫了用例的生成,出現了可以避免的重覆。
幸好Arg
和Args
會將我們的測試用例使用的參數進行註冊以便產生用例名/參數
的新測試用例,並且返回一個指向BENCHMARK
巨集生成對象的指針,換句話說,如果我們想要生成僅僅是參數不同的多個測試的話,只需要鏈式調用Arg
和Args
即可:
BENCHMARK(bench_array_ring_insert_int)->Arg(10)->Arg(100)->Arg(1000);
結果和上面一樣。
但這還不是最優解,我們仍然重覆調用了Arg方法,如果我們需要更多用例時就不得不又要做重覆勞動了。
對此google benchmark也有解決辦法:我們可以使用Range
方法來自動生成一定範圍內的參數。
先看看Range的原型:
BENCHMAEK(func)->Range(int64_t start, int64_t limit);
start表示參數範圍起始的值,limit表示範圍結束的值,Range所作用於的是一個_閉區間_。
但是如果我們這樣改寫代碼,是會得到一個錯誤的測試結果:
BENCHMARK(bench_array_ring_insert_int)->Range(10, 1000);
為什麼會這樣呢?那是因為Range預設除了start和limit,中間的其餘參數都會是某一個基底(base)的冪,基地預設為8,所以我們會看到64和512,它們分別是8的平方和立方。
想要改變這一行為也很簡單,只要重新設置基底即可,通過使用RangeMultiplier
方法:
BENCHMARK(bench_array_ring_insert_int)->RangeMultiplier(10)->Range(10, 1000);
現在結果恢復如初了。
使用Ranges可以處理多個參數的情況:
BENCHMARK(func)->RangeMultiplier(10)->Ranges({{10, 1000}, {128, 256}});
第一個範圍指定了測試用例的第一個傳入參數的範圍,而第二個範圍指定了第二個傳入參數可能的值(註意這裡不是範圍了)。
與下麵的代碼等價:
BENCHMARK(func)->Args({10, 128})
->Args({100, 128})
->Args({1000, 128})
->Args({10, 256})
->Args({100, 256})
->Args({1000, 256})
實際上就是用生成的第一個參數的範圍於後面指定內容的參數做了一個笛卡爾積。
使用參數生成器
如果我想定製沒有規律的更複雜的參數呢?這時就需要實現自定義的參數生成器了。
一個參數生成器的簽名如下:
void CustomArguments(benchmark::internal::Benchmark* b);
我們在生成器中計算處參數,然後調用benchmark::internal::Benchmark
對象的Arg或Args方法像上兩節那樣傳入參數即可。
隨後我們使用Apply
方法把生成器應用到測試用例上:
BENCHMARK(func)->Apply(CustomArguments);
其實這一過程的原理並不複雜,我做個簡單的解釋:
BENCHMARK
巨集產生的就是一個benchmark::internal::Benchmark
對象然後返回了它的指針- 向
benchmark::internal::Benchmark
對象傳遞參數需要使用Arg和Args等方法 Apply
方法會將參數中的函數應用在自身- 我們在生成器里使用
benchmark::internal::Benchmark
對象的指針b的Args等方法傳遞參數,這時的b其實指向我們的測試用例
到此為止生成器是如何工作的已經一目瞭然了,當然從上面得出的結論,我們還可以讓Apply做更多的事情。
下麵看下Apply的具體使用:
// 這次我們生成100,200,...,1000的測試用例,用range是無法生成這些參數的
static void custom_args(benchmark::internal::Benchmark* b)
{
for (int i = 100; i <= 1000; i += 100) {
b->Arg(i);
}
}
BENCHMARK(bench_array_ring_insert_int)->RangeMultiplier(10)->Apply(custom_args);
自定義參數的測試結果:
至此向測試用例傳遞參數的方法就全部介紹完了。
下一篇中我會介紹如何將測試用例寫成模板,傳遞參數只能解決一部分重覆代碼,對於擁有類似方法的不同待測試類型的測試用例,使用模板將會大大減少我們不必要的工作。