Google benchmark(C++性能測試)hmh_2

在上一篇博客(https://blog.csdn.net/weixin_42067873/article/details/106150877)中,簡單介紹了Google benchmark的相關知識,在本次博客中,我們一起來探究Google benchmark其他的一些用法:

一、如何往測試用例中傳遞參數

在這之前,我們的測試用例都只接受一個benchmark::State&類型的參數,如果我們需要給測試用例傳遞額外的參數呢?

google-benchmark可以通過傳遞參數來進行一系列相關的標準測試。

案例1:例如,通過傳遞參數,我們可以測試memcpy函數在不同內存大小情況下的執行效率:

#include <benchmark/benchmark.h>
#include <cstring>

static void BM_memcpy(benchmark::State& state) {
    char* src = new char[state.range(0)];
    char* dst = new char[state.range(0)];
    memset(src, 'x', state.range(0));
    for (auto _ : state)
        memcpy(dst, src, state.range(0));
    state.SetBytesProcessed(int64_t(state.iterations()) * int64_t(state.range(0)));
    delete[] src;
    delete[] dst;
}
BENCHMARK(BM_memcpy)->Arg(8)->Arg(64)->Arg(512)->Arg(1<<10)->Arg(8<<10);
BENCHMARK_MAIN(); 

案例2:假如我們需要實現一個隊列,現在有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對應傳遞進來的第二個參數。

有一點值得注意,參數傳遞只能接受整數,如果你希望使用其他類型的附加參數,就需要另外想些辦法了。

二、簡化多個相似測試用例的生成

1、向測試用例傳遞參數的最終目的是爲了在不編寫重複代碼的情況下生成多個測試用例,在知道了如何傳遞參數後你可能會這麼寫:

#include <benchmark/benchmark.h>
#include <vector>

static void bench_array_ring_insert_int(benchmark::State& state)
{
    auto length = state.range(0);
    std::vector<int> ring(length);
    for (auto _: state) {
        for (int i = 1; i <= length; ++i) {
            ring.push_back(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);

BENCHMARK_MAIN(); //main

在這裏,我們生成了三個實例,會產生如下的測試結果:

2、看起來工作良好,是嗎?

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

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

BENCHMARK(bench_array_ring_insert_int)->Arg(10)->Arg(100)->Arg(1000);

這還不是最優解,我們仍然重複調用了Arg方法,如果我們需要更多用例時就不得不又要做重複勞動了。

3、對此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);

現在結果恢復如初了:再次執行後,結果如下:

4、使用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})

特別注意:實際上就是用生成的第一個參數的範圍與後面指定內容的參數做了一個笛卡爾積。

三、參數生成器的使用

如果我想定製沒有規律的更復雜的參數呢?這時上述做法就顯得很棘手,所以就需要實現自定義的參數生成器了。

1、參數生成器的簽名如下:

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

參數自定義後測試結果如下:

到這裏,向測試用例傳遞參數的方法就全部介紹完了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章