c++效能測試工具:google benchmark入門(二)

apocelipes發表於2019-06-22

上一篇中我們初步體驗了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);

上面的例子展示瞭如何傳遞和獲取引數:

  1. 傳遞引數使用BENCHMARK巨集生成的物件的Arg方法
  2. 傳遞進來的引數會被放入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);

這裡我們生成了三個例項,會產生下面的結果:

pass args

看起來工作良好,是嗎?

沒錯,結果是正確的,但是記得我們前面說過的嗎——不要編寫重複的程式碼!是的,上面我們手動編寫了用例的生成,出現了可以避免的重複。

幸好ArgArgs會將我們的測試用例使用的引數進行註冊以便產生用例名/引數的新測試用例,並且返回一個指向BENCHMARK巨集生成物件的指標,換句話說,如果我們想要生成僅僅是引數不同的多個測試的話,只需要鏈式呼叫ArgArgs即可:

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);

error

為什麼會這樣呢?那是因為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);

其實這一過程的原理並不複雜,我做個簡單的解釋:

  1. BENCHMARK巨集產生的就是一個benchmark::internal::Benchmark物件然後返回了它的指標
  2. benchmark::internal::Benchmark物件傳遞引數需要使用Arg和Args等方法
  3. Apply方法會將引數中的函式應用在自身
  4. 我們在生成器裡使用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);

自定義引數的測試結果:

custom_args

至此向測試用例傳遞引數的方法就全部介紹完了。

下一篇中我會介紹如何將測試用例寫成模板,傳遞引數只能解決一部分重複程式碼,對於擁有類似方法的不同待測試型別的測試用例,使用模板將會大大減少我們不必要的工作。

相關文章