c++性能測試工具: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);

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

  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

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

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM