用C++的std::chrono来优雅地测量成员函数的执行时间

4
我希望优化我的应用程序,特别是某些函数的执行速度。
想象一下有一个类,其中包含一些成员函数。
class Test
{
public:
    Test();
    virtual ~Test();
    int init(int arg1, double arg2);

private:
    [...]

而在我的构造函数中,我调用了其中一个方法。

Test::Test()
{
    [...]
    int value = init(1, 1.2);
}

我该如何以简洁明了的方式测量我的方法init(...)的执行时间,而不会破坏我的程序?
目前,我使用以下代码:
Test::Test()
{
    [...]
    auto start = std::chrono::high_resolution_clock::now();

    int value = init(1, 1.2);

    auto stop = std::chrono::high_resolution_clock::now();
    std::chrono::duration<double> duration = stop - start;
    std::cout << duration.count() * 1000 << "ms\n";
}

它的功能正常,但我认为它相当混乱,我想要一个“更清晰”的解决方案。
是否有一种方法可以使用某种函数来接受成员函数和其他参数,例如:
int value = countTime(function, arg1, arg2);

我不知道是否可以将function()的返回值传递到countTime()中,以便不会打断我的代码工作流程。

编辑: 这是我的TimeMeasure类

namespace tools 
{
    class TimeMeasure 
    {
    public:
        TimeMeasure() 
        {
            m_start = std::chrono::high_resolution_clock::now();
        }

        virtual ~TimeMeasure()
        {
            m_stop = std::chrono::high_resolution_clock::now();
            std::chrono::duration<double, std::milli> duration = m_stop - m_start;
            std::cout << duration.count() << "ms\n";
        }

    public:
        typedef std::chrono::time_point<std::chrono::high_resolution_clock> HighResClock;

    private:
        HighResClock m_start;
        HighResClock m_stop;
    };

    template <typename T, typename F, typename... Args>
    auto measure(T *t, F &&fn, Args... args)
    {
        tools::TimeMeasure timeMeasure;
        return (t->*fn)(std::forward<Args>(args)...);
    }
}

在我的构造函数Test()中,我这样使用函数measure

Test()
{
    [...]
    tools::measure(this, Test::init, filepath);
}

int init(const std::string& filepath) const需要一个文件路径的字符串。在我的情况下只有一个参数。

不幸的是,我得到了一个invalid use of non-static member function 'int init(const string&) const'错误。

我想知道构造函数是否不是成员函数。那么为什么会出现这个错误?

编辑2:

根据OznOg的答案,我只是忘记了提交指向我的函数的指针。

所以这将是正确的函数调用。

tools::measure(this, &Test::init, filepath);

2
stop - start 赋值给 std::chrono::duration<double, std::milli>,并且去掉 * 1000。让 <chrono> 帮你完成这些转换。如果你养成了这个习惯,在处理复杂的事情时,你会减少编码错误。 - Howard Hinnant
@HowardHinnant 感谢您的建议!我之前并不知道这个。 - linux_lover
@LightnessRacesinOrbit 我已经找到了这个工具。但很遗憾,这个工具对于我的需求来说有些过于复杂。我只是想测量特定的函数。另外, gprof 似乎会产生相对较大的开销。 - linux_lover
1
这不是“太多”,而是精确的工具来分析函数执行,而不是在应用程序中撒播时间测量,这容易出现各种错误。当然,它会减慢程序运行速度,因此您无法获得绝对速度,但绝对速度也无用(您的程序将在多台计算机上运行,对吧?)- 您需要知道哪个函数当前比所有其他函数都要慢,而这正是它的优势所在。 - Lightness Races in Orbit
@LightnessRacesinOrbit 或许你是对的。也许这个工具对很多人来说就是合适的。但我想在运行时进行测量,而不是在程序完成后。我想在我的程序中使用一些测量结果。正如我所说,gprof 会有明显的开销。 - linux_lover
显示剩余3条评论
3个回答

6
你可以创建一个类,如下所示:

你可以创建一个类,如下所示:

struct MeasureTime {
    MeasureTime() : _start(std::chrono::high_resolution_clock::now()) {}

    ~MeasureTime() {
        auto stop = std::chrono::high_resolution_clock::now();
        std::chrono::duration<double> duration = stop - _start;
        std::cout << duration.count() * 1000 << "ms\n";
    }
private:
    std::chrono::time_point<std::chrono::high_resolution_clock>  _start;
};

然后在您的代码中简单地使用它:

Test::Test()
{
    MeasureTime mt;
    [...]
    { //or even this for just the init call
    MeasureTime mt2;
    int value = init(1, 1.2);
    }
}

在我看来,它比你提出的方案更不具有侵入性。

如果你真的想要一个函数,可以尝试使用类似下面的包装器:

template <class T, class F, class... Args>
auto MeasureTimeFn(T *t, F &&fn, Args&&... args) {
    MeasureTime timer;
     return (t->*fn)(std::forward<Args>(args)...);
}

并且像这样调用:

int value = MeasureTimeFn(this, &Test::init, 1, 1.2);

但不确定它是否真的更好。

您可以尝试使用宏隐藏事物:

#define MEASURE(f, ...) \
  MeasureTimeFn(this, &std::remove_reference_t<decltype(*this)>::f, __VA_ARGS__)

这样你就可以编写代码了。
int value = MEASURE(init, 1, 1.2);

这与你所要求的非常相似,但仅适用于成员函数内部的成员函数(非静态)。

无论如何,这可能是一个很好的起点。

*编辑* 如果您可以修改类的继承,则可以尝试

template<class T>
struct MeasureTool {
    template <class F, class... Args>
    auto measure(F &&fn, Args&&... args) {
        tools::TimeMeasure timeMeasure;
        return (static_cast<T*>(this)->*fn)(std::forward<Args>(args)...);
    }
};

class Test : public MeasureTool<Test>
{
public:
    Test();
    virtual ~Test() {}
    int init(const std::string &filepath) { _path = filepath; return 0; }
    const auto &getPath() const { return _path; }
private:
    std::string _path;

};

Test::Test()
{
    std::string filepath("/some/where");
    int value = measure(&Test::init, filepath);
    measure(&Test::getPath);
}

而且,这次似乎符合你的第一个要求(但是相当侵入式...)

现在,一切都掌握在你手中 :)


目前我是以类似的方式实现的。但如果我让析构函数为我完成工作,并将 int value = init(...) 放入作用域中,我将失去 value。我返回某些东西并将其保存在变量中是有原因的。我想在我的 Test() 构造函数中稍后对它进行操作。我认为最好尽可能少地更改我的生产代码。 - linux_lover
使用with语句表达式,您可以删除模板函数,但不确定这样做是否更好(可移植性较差)。 - OznOg
我喜欢这种更新的方式。如果你只想测量函数的时间而不想大量改变代码,我认为这样会更清晰易懂。不幸的是,在编译时我遇到了“无效使用非静态成员函数‘[...]’”的错误。我以为指向我的成员函数的指针足以调用它? - linux_lover
你应该在某个地方展示代码,我认为你没有从成员函数中调用宏。 - OznOg
好的,有一个打字错误;宏是正确的,但直接使用时缺少了__&__;你应该尝试使用int value = MeasureTimeFn(this, &Test::init, 1, 1.2); - OznOg
这是实现我的目标的好方法。谢谢你的帮助!我真的可以自己想到指向函数的指针 :) - linux_lover

0

我总是使用Boost.Timer

#include <boost/timer/timer.hpp>

...
boost::timer::cpu_timer timer;

for(int i = 0; i < 1000; ++i)
  funct();

std::cout << timer.format();

它显示了墙壁用户系统时间。


-1

您可以使用以下实现:

template<typename Fn, typename... Args>
typename std::enable_if<!std::is_void<typename std::result_of<Fn&&(Args...)>::type>::value, 
    std::tuple<std::chrono::duration<double>, typename std::result_of<Fn&&(Args...)>::type>
>::type
countTime(Fn&& fn, Args&&... args) {
    auto start = std::chrono::high_resolution_clock::now();
    auto fnret = std::forward<Fn>(fn)(std::forward<Args>(args)...);
    auto stop = std::chrono::high_resolution_clock::now();
    return std::make_tuple(stop - start, fnret);
}

template<typename Fn, typename... Args>
typename std::enable_if<std::is_void<typename std::result_of<Fn&&(Args...)>::type>::value, 
    std::tuple<std::chrono::duration<double>>
>::type
countTime(Fn&& fn, Args&&... args) {
    auto start = std::chrono::high_resolution_clock::now();
    std::forward<Fn>(fn)(std::forward<Args>(args)...);
    auto stop = std::chrono::high_resolution_clock::now();
    return std::make_tuple(stop - start);
}

template<typename R, class C, typename... Args>
typename std::enable_if<!std::is_same<R, void>::value,
    std::tuple<std::chrono::duration<double>, R>
>::type
countTime(R (C::*fn)(Args...), C& obj, Args&&... args) {
    auto start = std::chrono::high_resolution_clock::now();
    auto fnret = (obj.*fn)(std::forward<Args>(args)...);
    auto stop = std::chrono::high_resolution_clock::now();
    return std::make_tuple(stop - start, fnret);
}

template<class C, typename... Args>
std::tuple<std::chrono::duration<double>>
countTime(void (C::*fn)(Args...), C& obj, Args&&... args) {
    auto start = std::chrono::high_resolution_clock::now();
    (obj.*fn)(std::forward<Args>(args)...);
    auto stop = std::chrono::high_resolution_clock::now();
    return std::make_tuple(stop - start);
}

这将返回一个元组,其中包含 void (*)(...) 函数的一个成员 duration,以及返回 smth 的函数的 duration 和返回类型。它适用于 std::bind 和成员函数。可能会将重载合并为一到两个实现,但我不知道如何做到这一点。

调用示例:

auto ret = countTime([](int a) -> int { 
    std::this_thread::sleep_for(std::chrono::milliseconds(a)); 
    return a * 2;
}, 10);
std::cout << "function executed in: " <<
    std::chrono::duration_cast<std::chrono::milliseconds>(std::get<0>(ret)).count() <<
    " milliseconds." << std::endl;
std::cout << "function returned: " << std::get<1>(ret) << std::endl;

简单的示例调用:

auto ret = countTime(&Test::init, *this, 1, 1.2);
int value = std::get<1>(ret);

std::cout << std::chrono::duration_cast<std::chrono::milliseconds>(std::get<0>(ret)).count() << "ms" << std::endl;

这是一个简单的调用示例,假设有一个int init(int, int)函数:

auto ret = countTime(init, 1, 1.2);
int value = std::get<1>(ret);

std::cout << std::chrono::duration_cast<std::chrono::milliseconds>(std::get<0>(ret)).count() << "ms" << std::endl;

在线版本请访问onlinegdb


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接