简体   繁体   English

封装成员函数调用的模板化类的行为

[英]Behaviour of templated class wrapping a member function call

I have a function of a class I would like to iteratively call inside a loop, and while the loop is fixed, I want to be able to provide different functions (from the given object). 我有一个要在循环内迭代调用的类的功能,并且在循环固定后,我希望能够提供不同的功能(来自给定的对象)。 To approach this, I created a templated struct MyWrapper to take the object whose function I want to call, the function itself, and data for which to evaluate the function. 为了解决这个问题,我创建了一个模板化的结构MyWrapper来接收要调用其函数的对象,函数本身以及要对其进行评估的数据。 (In that sense, the member function will always have the same signature) (从这个意义上说,成员函数将始终具有相同的签名)

What I found though, was that using a member function pointer incurs a huge performance cost, even though at compile time, I know the function I want to call. 但是我发现,使用成员函数指针会导致巨大的性能损失,即使在编译时,我也知道要调用的函数。 So I was messing around to try and fix this, and (while I'm still unclear why the first situation happens), I've experienced another interesting behaviour. 因此,我在四处乱动尝试解决此问题,并且(虽然我仍然不清楚为什么会出现第一种情况),但我经历了另一种有趣的行为。

In the following situation, every call to the wrapper function MyWrapper::eval will actually attempt to copy my whole Grid object into the parameter to the given function it has to wrap, f , even though the call to MyEquation::eval will know not to copy it every time (because of optimization). 在以下情况下,对包装函数MyWrapper::eval 每次调用实际上都会尝试将我的整个Grid对象复制到必须包装的给定函数f ,即使对MyEquation::eval的调用MyEquation::eval知道每次复制(由于优化)。


template<typename T>
double neighbour_average(T *v, int n)
{
    return v[-n] + v[n] - 2 * v[0];
}

template<typename T>
struct MyEquation
{
    T constant;
    int n;
    T eval(Grid<T, 2> v, int i)
    {
        return rand() / RAND_MAX + neighbour_average(v.values + i, n) + constant;
    }
};


template<typename T, typename R, typename A>
struct MyWrapper
{
    MyWrapper(T &t, R(T::*f)(A, int), A a) : t{ t }, f{ f }, a{ a } {}
    auto eval(int i)
    {
        return (t.*f)(a, i);
    }

protected:
    A a;
    T &t;
    R(T::*f)(A, int);
};


int main(int argc, char *argv[])
{

    srand((unsigned int)time(NULL));
    for (iter_type i = 0; i < config().len_; ++i)
    {
        op.values[i] = rand() / RAND_MAX;
    }

    srand((unsigned int)time(NULL));
    double constant = rand() / RAND_MAX;
    int n = 2;
    int test_len = 100'000, 
    int test_run = 100'000'000;

    Grid<double, 2> arr(100, 1000);
    MyEquation<double> eq{ constant, n };
    MyWrapper weq(eq, &MyEquation<double>::eval, arr); // I'm wrapping what I want to do

    {
        // Time t0("wrapper thing");
        for (int i = 0; i < test_run; ++i)
        {
            arr.values[n + i % (test_len - n)] += weq.eval(n + i % (test_len - n)); // a call to the wrapping class to evaluate
        }
    }
    {
        // Time t0("regular thing");
        for (int i = 0; i < test_run; ++i)
        {
            arr.values[n + i % (test_len - n)] += rand() / RAND_MAX + neighbour_average(arr.values + n + i % (test_len - n), n) + constant; // a usage of the neighbour function without the wrapping call
        }
    }

    {
        // Time t0("function thing");
        for (int i = 0; i < test_run; ++i)
        {
            arr.values[n + i % (test_len - n)] += eq.eval(arr, n + i % (test_len - n)); // raw evaluation of my equation
        }
    }

}

Some context: 一些背景:

Grid is just a glorified dynamic array Grid::values with a few helper functions. Grid只是一个精美的动态数组Grid::values ,带有一些辅助函数。

I've retained some of the (seemingly unnecessary) templates to my function and object, because it closely parallels how my code is actually set up. 我保留了一些(看似不必要的)模板到我的函数和对象中,因为它与我的代码的实际设置非常相似。

The Time class will give me the duration of the object lifetime, so its a quick and dirty way of measuring certain blocks of code. Time类将为我提供对象生命周期的持续时间,因此它是一种测量某些代码块的快速而肮脏的方法。

So anyways... 所以无论如何...

If the following code is changed so the signature of the function taken by MyWrapper is R(T::*f)(A&, int) , then the execution time of MyWrapper::eval will be almost identical to the other calls (which is what I want anyways). 如果更改以下代码,以使MyWrapper接受的函数的签名为R(T::*f)(A&, int) ,则MyWrapper::eval的执行时间将与其他调用几乎相同(即无论如何我想要的)。

Why doesn't the compiler (msvc 2017) know it should treat the call weq.eval(n) (and consequently (t.*f)(a, n) ) the with the same optimization considerations way as the direct evaluation, if the signature and function is given at compile time? 为什么编译器(msvc 2017)不知道它应该以与直接评估相同的优化考虑方式来对待调用weq.eval(n) (因此(t.*f)(a, n) )签名和功能是在编译时给出的?

A function parameter is its own variable, which gets initialized from a function call argument. 函数参数是其自己的变量,该变量从函数调用参数进行初始化。 So when a function argument in the calling function is an lvalue such as the name of an object previously defined, and the function parameter is an object type, not a reference type, the parameter and the argument are two different objects. 因此,当调用函数中的函数参数是左值(例如先前定义的对象的名称),并且函数参数是对象类型而不是引用类型时,参数和参数是两个不同的对象。 If the parameter has a class type, this means a constructor for that type has to be executed (unless the initialization is an aggregate initialization from a {} initializer list). 如果参数具有类类型,则意味着必须执行该类型的构造函数(除非初始化是来自{}初始化程序列表的聚合初始化)。

In other words, every call to 换句话说,每次致电

T eval(Grid<T, 2> v, int i);

needs to create a new Grid<T, 2> object called v , whether it's called via function pointer or by the member name eval . 无论是通过函数指针还是通过成员名称eval调用,都需要创建一个名为v的新Grid<T, 2>对象。

But in many cases, initialization of a reference doesn't create a new object. 但是在许多情况下,引用的初始化不会创建新的对象。 It appears your eval doesn't need to modify v or the MyEquation , so it would be better to declare that eval as: 看来您的eval不需要修改vMyEquation ,因此最好将该eval声明为:

T eval(const Grid<T, 2> &v, int i) const;

This would mean the function pointer in Wrapper needs to be R (T::*f)(const A&, int) const . 这意味着Wrapper的函数指针需要为R (T::*f)(const A&, int) const

But another change you might want to make, especially since Wrapper is already a template: Just make the function used a generic type, so that it can hold non-member function pointers, wrappers to member function pointers with any signature, lambdas, or any other class type with an operator() member. 但是,您可能需要进行另一项更改,特别是因为Wrapper已经是模板:只需使该函数使用泛型类型,以便它可以容纳非成员函数指针,具有任何签名,lambda或任何其他形式的成员函数指针的包装器。具有operator()成员的其他类类型。

#include <utility>

template<typename F, typename A>
struct MyWrapper
{
    MyWrapper(F f, A a) : f{ std::move(f) }, a{ std::move(a) } {}
    auto eval(int i)
    {
        return f(a, i);
    }

protected:
    A a;
    F f;
};

Then two ways to create your Wrapper weq; 然后用两种方法创建Wrapper weq; are: 是:

Wrapper weq([&eq](const auto &arr, int i) {
    return eq.eval(arr, i);
}, arr);

or (requires #include <functional> ): 或(需要#include <functional> ):

using namespace std::placeholders;
Wrapper weq(
    std::bind(std::mem_fn(&MyEquation<double>::eval), _1, _2),
    arr);

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM