Поведение шаблонного класса, заключающего вызов функции-члена - PullRequest
0 голосов
/ 26 мая 2019

У меня есть функция класса, которую я хотел бы итеративно вызывать внутри цикла, и, хотя цикл исправлен, я хочу иметь возможность предоставлять различные функции (из данного объекта). Чтобы приблизиться к этому, я создал шаблонную структуру 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)) с теми же соображениями оптимизации, что и при прямой оценке, если сигнатура и функция задаются при компиляции время

1 Ответ

1 голос
/ 26 мая 2019

Параметр функции является собственной переменной, которая инициализируется из аргумента вызова функции. Таким образом, когда аргумент функции в вызывающей функции является lvalue, таким как имя ранее определенного объекта, и параметр функции является типом объекта, а не ссылочным типом, параметр и аргумент являются двумя разными объектами. Если параметр имеет тип класса, это означает, что должен быть выполнен конструктор для этого типа (если инициализация не является агрегированной инициализацией из списка {} инициализатора).

Другими словами, каждый звонок на

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() членом.

#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);
...