在 C++ 中以优雅的方式计时
Timing in an elegant way in c++
我对计算自由函数或成员函数(模板与否)的执行时间感兴趣。调用 TheFunc 有问题的函数,它的调用是
TheFunc(/*parameters*/);
或
ReturnType ret = TheFunc(/*parameters*/);
当然,我可以按如下方式包装这些函数调用:
double duration = 0.0 ;
std::clock_t start = std::clock();
TheFunc(/*parameters*/);
duration = static_cast<double>(std::clock() - start) / static_cast<double>(CLOCKS_PER_SEC);
或
double duration = 0.0 ;
std::clock_t start = std::clock();
ReturnType ret = TheFunc(/*parameters*/);
duration = static_cast<double>(std::clock() - start) / static_cast<double>(CLOCKS_PER_SEC);
但我想做一些比这更优雅的事情,即(从现在开始我将坚持 void return 类型)如下:
Timer thetimer ;
double duration = 0.0;
thetimer(*TheFunc)(/*parameters*/, duration);
其中 Timer 是我想设计的某个时间 class,这将允许我编写之前的代码,这样在执行之前代码的最后一行之后,双倍持续时间将包含
的执行时间
TheFunc(/*parameters*/);
但我不知道该怎么做,也不知道我的目标 syntax/solution 是否是最佳的...
使用可变参数模板,您可以:
template <typename F, typename ... Ts>
double Time_function(F&& f, Ts&&...args)
{
std::clock_t start = std::clock();
std::forward<F>(f)(std::forward<Ts>(args)...);
return static_cast<double>(std::clock() - start) / static_cast<double>(CLOCKS_PER_SEC);
}
我真的很喜欢 boost::cpu_timer::auto_cpu_timer
,当我不能使用 boost 时,我就自己破解:
#include <cmath>
#include <string>
#include <chrono>
#include <iostream>
class AutoProfiler {
public:
AutoProfiler(std::string name)
: m_name(std::move(name)),
m_beg(std::chrono::high_resolution_clock::now()) { }
~AutoProfiler() {
auto end = std::chrono::high_resolution_clock::now();
auto dur = std::chrono::duration_cast<std::chrono::microseconds>(end - m_beg);
std::cout << m_name << " : " << dur.count() << " musec\n";
}
private:
std::string m_name;
std::chrono::time_point<std::chrono::high_resolution_clock> m_beg;
};
void foo(std::size_t N) {
long double x {1.234e5};
for(std::size_t k = 0; k < N; k++) {
x += std::sqrt(x);
}
}
int main() {
{
AutoProfiler p("N = 10");
foo(10);
}
{
AutoProfiler p("N = 1,000,000");
foo(1000000);
}
}
多亏了 RAII,这个计时器才起作用。当您在范围内构建对象时,您会在该时间点存储时间点。当您离开范围(即在相应的 }
处)时,计时器首先存储时间点,然后计算滴答数(您可以将其转换为人类可读的持续时间),最后将其打印到屏幕.
当然,boost::timer::auto_cpu_timer
比我的简单实现要复杂得多,但我经常发现我的实现对我的目的来说绰绰有余。
我电脑中的示例 运行:
$ g++ -o example example.com -std=c++14 -Wall -Wextra
$ ./example
N = 10 : 0 musec
N = 1,000,000 : 10103 musec
编辑
我非常喜欢@Jarod42 建议的实现。我对其进行了一些修改,以便为所需的 "units" 输出提供一些灵活性。
它默认返回经过的微秒数(一个整数,通常 std::size_t
),但您可以请求输出在您选择的任何持续时间内。
我认为这是一种比我之前建议的方法更灵活的方法,因为现在我可以做其他事情,比如进行测量并将它们存储在容器中(就像我在示例中所做的那样)。
感谢@Jarod42 的启发。
#include <cmath>
#include <string>
#include <chrono>
#include <algorithm>
#include <iostream>
template<typename Duration = std::chrono::microseconds,
typename F,
typename ... Args>
typename Duration::rep profile(F&& fun, Args&&... args) {
const auto beg = std::chrono::high_resolution_clock::now();
std::forward<F>(fun)(std::forward<Args>(args)...);
const auto end = std::chrono::high_resolution_clock::now();
return std::chrono::duration_cast<Duration>(end - beg).count();
}
void foo(std::size_t N) {
long double x {1.234e5};
for(std::size_t k = 0; k < N; k++) {
x += std::sqrt(x);
}
}
int main() {
std::size_t N { 1000000 };
// profile in default mode (microseconds)
std::cout << "foo(1E6) takes " << profile(foo, N) << " microseconds" << std::endl;
// profile in custom mode (e.g, milliseconds)
std::cout << "foo(1E6) takes " << profile<std::chrono::milliseconds>(foo, N) << " milliseconds" << std::endl;
// To create an average of `M` runs we can create a vector to hold
// `M` values of the type used by the clock representation, fill
// them with the samples, and take the average
std::size_t M {100};
std::vector<typename std::chrono::milliseconds::rep> samples(M);
for(auto & sample : samples) {
sample = profile(foo, N);
}
auto avg = std::accumulate(samples.begin(), samples.end(), 0) / static_cast<long double>(M);
std::cout << "average of " << M << " runs: " << avg << " microseconds" << std::endl;
}
输出(用g++ example.cpp -std=c++14 -Wall -Wextra -O3
编译):
foo(1E6) takes 10073 microseconds
foo(1E6) takes 10 milliseconds
average of 100 runs: 10068.6 microseconds
我非常喜欢使用 RAII 包装器处理此类内容。
以下示例有点冗长,但它更灵活,因为它适用于任意范围,而不是仅限于单个函数调用:
class timing_context {
public:
std::map<std::string, double> timings;
};
class timer {
public:
timer(timing_context& ctx, std::string name)
: ctx(ctx),
name(name),
start(std::clock()) {}
~timer() {
ctx.timings[name] = static_cast<double>(std::clock() - start) / static_cast<double>(CLOCKS_PER_SEC);
}
timing_context& ctx;
std::string name;
std::clock_t start;
};
timing_context ctx;
int main() {
timer_total(ctx, "total");
{
timer t(ctx, "foo");
// Do foo
}
{
timer t(ctx, "bar");
// Do bar
}
// Access ctx.timings
}
缺点是您最终可能会得到很多仅用于破坏计时对象的作用域。
这可能会或可能不会满足您的要求,因为您的要求有点含糊,但它说明了如何使用 RAII 语义来制作一些非常好的可重用和干净的代码。它可能也可以修改得更好看!
你可以用 MatLab 的方式来做。它非常老派,但简单往往是好的:
tic();
a = f(c);
toc(); //print to stdout, or
auto elapsed = toc(); //store in variable
tic()
和 toc()
可以作用于全局变量。如果这还不够,您可以使用一些宏魔法创建局部变量:
tic(A);
a = f(c);
toc(A);
我对计算自由函数或成员函数(模板与否)的执行时间感兴趣。调用 TheFunc 有问题的函数,它的调用是
TheFunc(/*parameters*/);
或
ReturnType ret = TheFunc(/*parameters*/);
当然,我可以按如下方式包装这些函数调用:
double duration = 0.0 ;
std::clock_t start = std::clock();
TheFunc(/*parameters*/);
duration = static_cast<double>(std::clock() - start) / static_cast<double>(CLOCKS_PER_SEC);
或
double duration = 0.0 ;
std::clock_t start = std::clock();
ReturnType ret = TheFunc(/*parameters*/);
duration = static_cast<double>(std::clock() - start) / static_cast<double>(CLOCKS_PER_SEC);
但我想做一些比这更优雅的事情,即(从现在开始我将坚持 void return 类型)如下:
Timer thetimer ;
double duration = 0.0;
thetimer(*TheFunc)(/*parameters*/, duration);
其中 Timer 是我想设计的某个时间 class,这将允许我编写之前的代码,这样在执行之前代码的最后一行之后,双倍持续时间将包含
的执行时间TheFunc(/*parameters*/);
但我不知道该怎么做,也不知道我的目标 syntax/solution 是否是最佳的...
使用可变参数模板,您可以:
template <typename F, typename ... Ts>
double Time_function(F&& f, Ts&&...args)
{
std::clock_t start = std::clock();
std::forward<F>(f)(std::forward<Ts>(args)...);
return static_cast<double>(std::clock() - start) / static_cast<double>(CLOCKS_PER_SEC);
}
我真的很喜欢 boost::cpu_timer::auto_cpu_timer
,当我不能使用 boost 时,我就自己破解:
#include <cmath>
#include <string>
#include <chrono>
#include <iostream>
class AutoProfiler {
public:
AutoProfiler(std::string name)
: m_name(std::move(name)),
m_beg(std::chrono::high_resolution_clock::now()) { }
~AutoProfiler() {
auto end = std::chrono::high_resolution_clock::now();
auto dur = std::chrono::duration_cast<std::chrono::microseconds>(end - m_beg);
std::cout << m_name << " : " << dur.count() << " musec\n";
}
private:
std::string m_name;
std::chrono::time_point<std::chrono::high_resolution_clock> m_beg;
};
void foo(std::size_t N) {
long double x {1.234e5};
for(std::size_t k = 0; k < N; k++) {
x += std::sqrt(x);
}
}
int main() {
{
AutoProfiler p("N = 10");
foo(10);
}
{
AutoProfiler p("N = 1,000,000");
foo(1000000);
}
}
多亏了 RAII,这个计时器才起作用。当您在范围内构建对象时,您会在该时间点存储时间点。当您离开范围(即在相应的 }
处)时,计时器首先存储时间点,然后计算滴答数(您可以将其转换为人类可读的持续时间),最后将其打印到屏幕.
当然,boost::timer::auto_cpu_timer
比我的简单实现要复杂得多,但我经常发现我的实现对我的目的来说绰绰有余。
我电脑中的示例 运行:
$ g++ -o example example.com -std=c++14 -Wall -Wextra
$ ./example
N = 10 : 0 musec
N = 1,000,000 : 10103 musec
编辑
我非常喜欢@Jarod42 建议的实现。我对其进行了一些修改,以便为所需的 "units" 输出提供一些灵活性。
它默认返回经过的微秒数(一个整数,通常 std::size_t
),但您可以请求输出在您选择的任何持续时间内。
我认为这是一种比我之前建议的方法更灵活的方法,因为现在我可以做其他事情,比如进行测量并将它们存储在容器中(就像我在示例中所做的那样)。
感谢@Jarod42 的启发。
#include <cmath>
#include <string>
#include <chrono>
#include <algorithm>
#include <iostream>
template<typename Duration = std::chrono::microseconds,
typename F,
typename ... Args>
typename Duration::rep profile(F&& fun, Args&&... args) {
const auto beg = std::chrono::high_resolution_clock::now();
std::forward<F>(fun)(std::forward<Args>(args)...);
const auto end = std::chrono::high_resolution_clock::now();
return std::chrono::duration_cast<Duration>(end - beg).count();
}
void foo(std::size_t N) {
long double x {1.234e5};
for(std::size_t k = 0; k < N; k++) {
x += std::sqrt(x);
}
}
int main() {
std::size_t N { 1000000 };
// profile in default mode (microseconds)
std::cout << "foo(1E6) takes " << profile(foo, N) << " microseconds" << std::endl;
// profile in custom mode (e.g, milliseconds)
std::cout << "foo(1E6) takes " << profile<std::chrono::milliseconds>(foo, N) << " milliseconds" << std::endl;
// To create an average of `M` runs we can create a vector to hold
// `M` values of the type used by the clock representation, fill
// them with the samples, and take the average
std::size_t M {100};
std::vector<typename std::chrono::milliseconds::rep> samples(M);
for(auto & sample : samples) {
sample = profile(foo, N);
}
auto avg = std::accumulate(samples.begin(), samples.end(), 0) / static_cast<long double>(M);
std::cout << "average of " << M << " runs: " << avg << " microseconds" << std::endl;
}
输出(用g++ example.cpp -std=c++14 -Wall -Wextra -O3
编译):
foo(1E6) takes 10073 microseconds
foo(1E6) takes 10 milliseconds
average of 100 runs: 10068.6 microseconds
我非常喜欢使用 RAII 包装器处理此类内容。
以下示例有点冗长,但它更灵活,因为它适用于任意范围,而不是仅限于单个函数调用:
class timing_context {
public:
std::map<std::string, double> timings;
};
class timer {
public:
timer(timing_context& ctx, std::string name)
: ctx(ctx),
name(name),
start(std::clock()) {}
~timer() {
ctx.timings[name] = static_cast<double>(std::clock() - start) / static_cast<double>(CLOCKS_PER_SEC);
}
timing_context& ctx;
std::string name;
std::clock_t start;
};
timing_context ctx;
int main() {
timer_total(ctx, "total");
{
timer t(ctx, "foo");
// Do foo
}
{
timer t(ctx, "bar");
// Do bar
}
// Access ctx.timings
}
缺点是您最终可能会得到很多仅用于破坏计时对象的作用域。
这可能会或可能不会满足您的要求,因为您的要求有点含糊,但它说明了如何使用 RAII 语义来制作一些非常好的可重用和干净的代码。它可能也可以修改得更好看!
你可以用 MatLab 的方式来做。它非常老派,但简单往往是好的:
tic();
a = f(c);
toc(); //print to stdout, or
auto elapsed = toc(); //store in variable
tic()
和 toc()
可以作用于全局变量。如果这还不够,您可以使用一些宏魔法创建局部变量:
tic(A);
a = f(c);
toc(A);