目标
Benchmarking in C++中描述了一个简易的benchmark,将逐步分析如何实现该benchmark,及模板化思路。
应用场景
衡量算法其中一种方法是BigO,以n为因子,判断随着数量级增大耗时增加情况,而且考虑到屏蔽一些随机扰动,需要进行多次采样比较。
譬如文中举例比较std::vector
和std::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::milliseconds>(std::chrono::high_resolution_clock::now() - start);
以上的代码得出的duration
就是毫秒量级的耗时记录结果。
一个最小的耗时测量实现如下:
//invoke function and return time used
template<typename TTime = std::chrono::milliseconds>
struct measure
{
template<typename F, typename ...Args>
static typename TTime::rep execution(F func, Args&&... args)
{
auto start = std::chrono::high_resolution_clock::now();
func(std::forward<Args>(args)...);
auto duration = std::chrono::duration_cast<TTime>(std::chrono::high_resolution_clock::now() - start);
return duration.count();
}
};
其中用到了完美转发std::forward
来处理参数传递。
使用方法为:
auto oVal = measure<>::execution(CommonUse<std::vector<int>>,1000);
这样就得到了n=1000
时的耗时毫秒数。
支持多种测量
当希望支持多种测量时,就需要将测量结果存储到map
之中;实现时将其拆分为三块:
- 执行函数获取耗时
- 耗时信息保存
- 整合执行和保存
执行函数获取耗时
struct measure
{
//measure function implement
template<typename F, typename TFactor>
inline static auto duration(F&& callable, TFactor&& factor)
{
auto start = std::chrono::high_resolution_clock::now();
std::forward<decltype(callable)>(callable)(std::forward<TFactor>(factor));
return (std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::high_resolution_clock::now() - start));
}
}
保存测量结果
//save results
template<typename TFactor>
struct experiment_impl
{
std::string _fctName;
std::map<TFactor,std::vector<std::chrono::milliseconds>> _timings;
experiment_impl(const std::string& factorName):_fctName(factorName){};
protected:
~experiment_impl() = default;
};
整合执行和保存
定义了experment_model
来在构造时完成函数执行和结果保存:
template<typename TFactor>
struct experment_model final :detail::experiment_impl<TFactor>
{
//invoke function and save result
template<typename F>
experment_model(std::size_t nSample, F&& callable, const std::string& factorName, std::initializer_list<TFactor>&& factors)
:experiment_impl<TFactor>(factorName)
{
for (auto&& factor:factors)
{
experiment_impl<TFactor>::_timings[factor].reserve(nSample);
for (std::size_t i = 0 ; i < nSample ; i++)
{
experiment_impl<TFactor>::_timings[factor].push_back(measure::duration(std::forward<decltype(callable)>(callable),factor));
}
}
}
};
封装成benchmark
来支持多次、多因子、多种测量:
template<typename TFactor>
class benchmark
{
std::vector<std::pair<std::string,std::unique_ptr<detail::experment_model<TFactor>>>> _data;
public:
benchmark() = default;
benchmark(benchmark const&) = delete;
public:
template<class F>
void run(const std::string& name, std::size_t nSample, F&& callable,
const std::string& factorName, std::initializer_list<TFactor>&& factors)
{
_data.emplace_back(name,std::make_unique<detail::experment_model<TFactor>>(nSample,
std::forward<decltype(callable)>(callable),factorName,
std::forward<std::initializer_list<TFactor>&&>(factors)));
}
}
使用方法
bmk::benchmark<std::size_t> bm;
bm.run("vector",10, CommonUse<std::vector<int>>,"number of elements",{10,100,1000,10000});
bm.run("list",10, CommonUse<std::list<int>>,"number of elements",{10,100,1000,10000});
支持无因子测量
那么当要测试的是普通函数时,并没有因子输入,只是执行了多次,那么就需要做出调整:
- 执行无因子函数获取耗时
- 耗时信息保存
- 提供无因子版experiment_model
- 提供无因子版benchmark.run
执行无因子函数
//measure function void version
template<typename F>
inline static auto duration(F&& callable)
{
auto start = std::chrono::high_resolution_clock::now();
std::forward<decltype(callable)>(callable)();
return std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::high_resolution_clock::now() - start);
}
调整的方式为移除测量时的因子参数输入。
保存测量结果
测量结果只是一个vector<milliseconds>
,提供experment_impl
的偏特化版本:
//save result void version
template<>
struct experiment_impl<void>
{
std::vector<std::chrono::milliseconds> _timings;
experiment_impl(std::size_t nSample):_timings(nSample){};
protected:
~experiment_impl() = default;
};
提供无因子版experiment_model
//invoke function and save result [void version]
template<typename F>
experment_model(std::size_t nSample, F&& callable)
:experiment_impl<void>(nSample)
{
for (std::size_t i = 0; i < nSample; i++)
{
experiment_impl<TFactor>::_timings.push_back(measure::duration(std::forward<decltype(callable)>(callable)));
}
}
提供无因子版benchmark.run
由于benchmark需要支持无因子,而其存储的内容为experiment_model
,那么需要提供基类,保证experiment_model
在两种情况下都适用:
struct experiment
{
virtual ~experiment() {};
};
template<typename TFactor = void>
struct experment_model final :detail::experiment,detail::experiment_impl<TFactor>
{
......
};
同样,benchmark
需要移除模板参数TFactor
class benchmark
{
std::vector<std::pair<std::string,std::unique_ptr<detail::experiment>>> _data;
......
}
然后是无因子版的run
template<class F>
void run(const std::string& name, std::size_t nSample, F&& callable)
{
_data.emplace_back(name, std::make_unique<detail::experment_model<>>(nSample,
std::forward<decltype(callable)>(callable)));
}
支持多分辨率测量
之前一直采用的是毫秒,在一些情况下函数执行很快,需要采用微秒、纳秒级别,那么就需要把之前写死的std::chrono::milliseconds
替换成模板参数,同步修改所有位置:
template<typename TTime ,typename TFactor>
struct experiment_impl
{
......
}
......
template<typename TTime>
struct measure
{
......
}
......
template<typename TTime,typename TFactor = void>
struct experment_model final :detail::experiment,detail::experiment_impl<TTime,TFactor>
{
......
}
......
template<typename TTime>
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