我有一个要在循环内迭代调用的类的函数,并且在循环固定后,我希望能够提供不同的函数(与给定对象不同)。为了解决这个问题,我创建了一个模板化的结构MyWrapper
来获取要调用其函数的对象,函数本身以及要对其求值的数据。 (从这个意义上说,成员函数将始终具有相同的签名)
但是我发现,使用成员函数指针会导致巨大的性能损失,即使在编译时,我也知道要调用的函数。因此,我在四处乱动尝试解决此问题,并且(虽然我仍然不清楚为什么会出现第一种情况),但我经历了另一种有趣的行为。
在以下情况下,对包装函数MyWrapper::eval
的每次调用实际上都会尝试将我的整个Grid
对象复制到必须包装的给定函数f
的参数中,即使对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
}
}
}
一些背景:
Grid
只是一个精美的动态数组Grid::values
,其中包含一些辅助函数。我保留了一些(看似不必要的)模板到我的函数和对象中,因为它与我的代码的实际设置非常相似。
Time
类将为我提供对象生命周期的持续时间,因此它是一种快速而肮脏的方法,用于测量某些代码块。所以无论如何...
如果更改以下代码,以使
MyWrapper
所采用的函数的签名为R(T::*f)(A&, int)
,则MyWrapper::eval
的执行时间将与其他调用几乎相同(无论如何,这都是我想要的)。如果在编译时给出了签名和函数,为什么编译器(msvc 2017)不知道应该以与直接求值相同的优化考虑方式对待调用
weq.eval(n)
(因此是(t.*f)(a, n)
)? 最佳答案
函数参数是其自己的变量,该变量从函数调用参数进行初始化。因此,当调用函数中的函数参数是左值(例如先前定义的对象的名称),并且函数参数是对象类型而不是引用类型时,参数和参数是两个不同的对象。如果参数具有类类型,则意味着必须执行该类型的构造函数(除非初始化是来自{}
初始化程序列表的聚合初始化)。
换句话说,每次致电
T eval(Grid<T, 2> v, int i);
无论是通过函数指针还是通过成员名称
Grid<T, 2>
调用,都需要创建一个名为v
的新eval
对象。但是在许多情况下,引用的初始化不会创建新的对象。看来您的
eval
不需要修改v
或MyEquation
,因此最好将eval
声明为:T eval(const Grid<T, 2> &v, int i) const;
这意味着
Wrapper
中的函数指针需要为R (T::*f)(const A&, int) const
。但是,您可能还想进行另一项更改,尤其是由于
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;
};
然后,创建
Wrapper weq;
的两种方法是:Wrapper weq([&eq](const auto &arr, int i) {
return eq.eval(arr, i);
}, arr);
或(需要
#include <functional>
):using namespace std::placeholders;
Wrapper weq(
std::bind(std::mem_fn(&MyEquation<double>::eval), _1, _2),
arr);
关于c++ - 封装成员函数调用的模板化类的行为,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/56314423/