I want to make a benchmark
function that, given an invocable and its parameters, will time the execution of the given invocable and return the measured std::chrono::duration<...>
as well as the value returned by the invocation of the invocable.
I am having problems with perfect forwarding the value returned from the invocation. Currently I return the value returned by the invocation and use a reference parameter to return the duration:
using bench_clock = std::conditional_t<std::chrono::high_resolution_clock::is_steady,
std::chrono::high_resolution_clock, std::chrono::steady_clock>;
decltype(auto) benchmark(
bench_clock::duration& duration, auto&& func, auto&&... args)
{
auto begin{ bench_clock::now() };
decltype(auto) result{
std::invoke(
std::forward<decltype(func)>(func),
std::forward<decltype(args)>(args)...)
};
duration = bench_clock::now() - begin;
return result;
}
As far as I know, this perfectly forwards the value returned from the invocation.
I would prefer to also return the duration conventionally, as an example, by using std::tuple
though I am not sure how to do it t perfectly forward the returned value.
My guess would be to use std::invoke_result_t
like this:
using bench_clock = std::conditional_t<
std::chrono::high_resolution_clock::is_steady,
std::chrono::high_resolution_clock, std::chrono::steady_clock>;
auto benchmark(auto&& func, auto&&... args)
{
auto begin{ bench_clock::now() };
decltype(auto) result{
std::invoke(
std::forward<decltype(func)>(func),
std::forward<decltype(args)>(args)...)
};
auto duration{ bench_clock::now() - begin };
return std::tuple<std::invoke_result_t<decltype(func), decltype(args)...>, bench_clock::duration>{std::forward<decltype(result)>(result), duration};
//------------------------------------------------------------------------------------------------^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ is this needed?
}
I am not sure if this approach correctly perfectly forwards. I also don't know if is required to use std::forward
in the std::tuple
constructor.
There is also a problem, that if the invocable returns void
, the tuple cannot be used, as std::tuple<void>
cannot be instantiated.
I am not sure how to go around this.
Yes, you are right. You need to handle the void
return case when you invoke the function func
inside the benchmark
function.
Since you have used std::invoke_result_t
and std::conditional_t
, I assume that you have access to c++17. Then this can be easily solved by an if constexpr
checking.
#include <iostream>
#include <chrono>
#include <tuple>
#include <type_traits> // std::is_same_v, std::invoke_result_t
#include <functional> // std::invoke
#include <utility> // std::forward, std::move
using namespace std::chrono;
using bench_clock = std::conditional_t<
high_resolution_clock::is_steady, high_resolution_clock, steady_clock>;
template<typename Func, typename... Args>
decltype(auto) benchmark(Func&& func, Args&&... args)
{
auto begin{ bench_clock::now() };
if constexpr (std::is_same_v<void, std::invoke_result_t<Func, Args...>>)
{
// can not have void result: therefore just invoke the func!
std::invoke(std::forward<Func>(func), std::forward<Args>(args)...);
return bench_clock::now() - begin; // only the time duration will be returned!
}
else
{
// all other cases return the tuple of result-duration!
decltype(auto) result{ std::invoke(std::forward<Func>(func), std::forward<Args>(args)...) };
const auto duration{ bench_clock::now() - begin };
return std::make_tuple(std::move(result), duration);
}
}