如何实现一个简易的benchmark

目标

Benchmarking in C++中描述了一个简易的benchmark,将逐步分析如何实现该benchmark,及模板化思路。

应用场景

衡量算法其中一种方法是BigO,以n为因子,判断随着数量级增大耗时增加情况,而且考虑到屏蔽一些随机扰动,需要进行多次采样比较。
譬如文中举例比较std::vectorstd::list时,进行了10次采用,n的数量级从10的1次方直到10的5次方,输出了2种情况下的耗时信息,再配合其可视化脚本输出比较图。

也就是说,这个benchmark将会支持多次采样、多个因子、执行多种算法测量。

最小实现

测量时,需要在执行前记录当前时间戳,然后执行完成后根据结束的时间戳计算出耗时。

auto start = std::chrono::high_resolution_clock::now();
//执行函数;
auto duration = std::chrono::duration_cast(std::chrono::high_resolution_clock::now() - start);

以上的代码得出的duration就是毫秒量级的耗时记录结果。

一个最小的耗时测量实现如下:

//invoke function and return time used
template
struct measure
{
    template
    static typename TTime::rep execution(F func, Args&&... args)
    {
        auto start = std::chrono::high_resolution_clock::now();
        func(std::forward(args)...);
        auto duration = std::chrono::duration_cast(std::chrono::high_resolution_clock::now() - start);
        return duration.count();
    }
};

其中用到了完美转发std::forward来处理参数传递。

使用方法为:

auto oVal =  measure<>::execution(CommonUse>,1000);

这样就得到了n=1000时的耗时毫秒数。

支持多种测量

当希望支持多种测量时,就需要将测量结果存储到map之中;实现时将其拆分为三块:

  1. 执行函数获取耗时
  2. 耗时信息保存
  3. 整合执行和保存

执行函数获取耗时

struct measure
{
    //measure function implement
    template
    inline static auto duration(F&& callable, TFactor&& factor)
    {
        auto start = std::chrono::high_resolution_clock::now();
        std::forward(callable)(std::forward(factor));
        return (std::chrono::duration_cast(std::chrono::high_resolution_clock::now() - start));
    }
}

保存测量结果

//save results
template
struct experiment_impl
{
    std::string _fctName;
    std::map> _timings;
    experiment_impl(const std::string& factorName):_fctName(factorName){};
 
protected:
    ~experiment_impl() = default;
};

整合执行和保存

定义了experment_model来在构造时完成函数执行和结果保存:

template
struct experment_model final :detail::experiment_impl
{
    //invoke function and save result
    template
    experment_model(std::size_t nSample, F&& callable, const std::string& factorName, std::initializer_list&& factors)
        :experiment_impl(factorName)
    {
        for (auto&& factor:factors)
        {
            experiment_impl::_timings[factor].reserve(nSample);
            for (std::size_t i = 0 ; i < nSample ; i++)
            {
                experiment_impl::_timings[factor].push_back(measure::duration(std::forward(callable),factor));
            }
        }
    }
 
};

封装成benchmark来支持多次、多因子、多种测量:

template
class benchmark
{
    std::vector>>> _data;
public:
    benchmark() = default;
    benchmark(benchmark const&) = delete;
public:
    template
    void run(const std::string& name, std::size_t nSample, F&& callable,
        const std::string& factorName, std::initializer_list&& factors)
    {
        _data.emplace_back(name,std::make_unique>(nSample,
            std::forward(callable),factorName,
            std::forward&&>(factors)));
    }
}

使用方法

bmk::benchmark bm;
bm.run("vector",10, CommonUse>,"number of elements",{10,100,1000,10000});
bm.run("list",10, CommonUse>,"number of elements",{10,100,1000,10000});

支持无因子测量

那么当要测试的是普通函数时,并没有因子输入,只是执行了多次,那么就需要做出调整:

  1. 执行无因子函数获取耗时
  2. 耗时信息保存
  3. 提供无因子版experiment_model
  4. 提供无因子版benchmark.run

执行无因子函数

//measure function void version
template
inline static auto duration(F&& callable)
{
    auto start = std::chrono::high_resolution_clock::now();
    std::forward(callable)();
    return std::chrono::duration_cast(std::chrono::high_resolution_clock::now() - start);
}

调整的方式为移除测量时的因子参数输入。

保存测量结果

测量结果只是一个vector,提供experment_impl的偏特化版本:

//save result void version
template<>
struct experiment_impl
{
    std::vector _timings;
    experiment_impl(std::size_t nSample):_timings(nSample){};
protected:
    ~experiment_impl() = default;
};

提供无因子版experiment_model

//invoke function and save result [void version]
template
experment_model(std::size_t nSample, F&& callable)
    :experiment_impl(nSample)
{
    for (std::size_t i = 0; i < nSample; i++)
    {
        experiment_impl::_timings.push_back(measure::duration(std::forward(callable)));
    }
}

提供无因子版benchmark.run

由于benchmark需要支持无因子,而其存储的内容为experiment_model,那么需要提供基类,保证experiment_model在两种情况下都适用:

struct experiment
{
    virtual ~experiment() {};
};
template
struct experment_model final :detail::experiment,detail::experiment_impl
{
  ......
};

同样,benchmark需要移除模板参数TFactor

class benchmark
{
    std::vector>> _data;
   ......
}

然后是无因子版的run

template
void run(const std::string& name, std::size_t nSample, F&& callable)
{
    _data.emplace_back(name, std::make_unique>(nSample,
        std::forward(callable)));
}

支持多分辨率测量

之前一直采用的是毫秒,在一些情况下函数执行很快,需要采用微秒、纳秒级别,那么就需要把之前写死的std::chrono::milliseconds替换成模板参数,同步修改所有位置:

template
struct experiment_impl
{
   ......
}
   ......
template
struct measure
{
   ......
}
   ......
template
struct experment_model final :detail::experiment,detail::experiment_impl
{
   ......
}
   ......
template
class benchmark
{
   ......
}

支持多种clock

在之前都采用的是std::chrono::high_resolution_clock,但是对MSVC2013来讲存在问题:
Time measurements with High_resolution_clock not working as intended
这个版本可以采用std::chrono::steady_clock,那么实现多种clock即可,与多分辨率的方式一致,提供模板参数TClock替换std::chrono::high_resolution_clock,并将默认参数设置为std::chrono::high_resolution_clock

其它

  • 输出结果
  • tic/toc

你可能感兴趣的:(如何实现一个简易的benchmark)