我有一个要在循环内迭代调用的类的函数,并且在循环固定的同时,我希望能够提供不同的功能(与给定的对象不同)。为了解决这个问题,我创建了一个模板化结构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)
)(如果签名和函数)是在编译时给出的?
答案 0 :(得分:1)
函数参数是其自己的变量,该变量从函数调用参数进行初始化。因此,当调用函数中的函数参数是左值(例如先前定义的对象的名称),并且函数参数是对象类型而不是引用类型时,参数和参数是两个不同的对象。如果参数具有类类型,则意味着必须执行该类型的构造函数(除非初始化是来自{}
初始化程序列表的聚合初始化)。
换句话说,每次致电
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
已经是模板:只需使函数使用泛型类型,以便它可以容纳非成员函数指针,并使用以下方法包装成员函数指针:任何具有operator()
成员的签名,lambda或任何其他类类型。
#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);