Скорость выполнения кода с объектом «функция» по сравнению с использованием шаблонных функций

Я знаю, что std::function реализован с помощью идиомы стирание типа. Стирание типов — удобный метод, но его недостатком является необходимость хранения в куче регистра (своего рода массива) базовых объектов.

Следовательно, при создании или копировании объекта function необходимо выполнить распределение, и, как следствие, процесс должен быть медленнее, чем простое манипулирование функциями как типами шаблонов.

Чтобы проверить это предположение, я запустил тестовую функцию, которая накапливает n = cycles последовательных целых чисел, а затем делит сумму на количество приращений n. Сначала закодировано как шаблон:

#include <iostream>
#include <functional>
#include <chrono>
using std::cout;
using std::function;
using std::chrono::system_clock;
using std::chrono::duration_cast;
using std::chrono::milliseconds;

double computeMean(const double start, const int cycles) {
    double tmp(start);
    for (int i = 0; i < cycles; ++i) {
        tmp += i;
    }
    return tmp / cycles;
}

template<class T>
double operate(const double a, const int b, T myFunc) {
    return myFunc(a, b);
}

и main.cpp:

int main()
{
    double init(1), result;
    int increments(1E9);
    // start clock
    system_clock::time_point t1 = system_clock::now();

    result = operate(init, increments, computeMean);
    // stop clock
    system_clock::time_point t2 = system_clock::now();

    cout << "Input: " << init << ", " << increments << ", Output: " << result << '\n';
    cout << "Time elapsed: " << duration_cast<milliseconds>(t2 - t1).count() << " ms\n";
    return 0;
}

Это было запущено сто раз и получило средний результат 10024.9 ms.

Затем я ввожу объект function в main, а также специализацию шаблона для operate, чтобы приведенный выше код можно было переработать:

\\ as above, just add the template specialization
template<>
double operate(const double a, const int b, function<double (const double, const int)> myFunc) {
    cout << "nontemplate called\n";
    return myFunc(a, b);
}

\\ and inside the main
int main()
{
    //...
    // start clock
    system_clock::time_point t1 = system_clock::now();

    // new lines
    function<double (const double, const int)> computeMean =
        [](const double init, const int increments) {
            double tmp(init);
            for (int i = 0; i < increments; ++i) {
                tmp += i;
            }
            return tmp / increments;
        };
    // rest as before
    // ...
}

Я ожидал, что версия function будет быстрее, но в среднем примерно то же самое, на самом деле даже медленнее, result = 9820.3 ms. Проверил стандартные отклонения, они примерно одинаковые, 1233.77 против 1234.96.

Какой в ​​этом смысл? Я ожидал, что вторая версия с объектом function будет медленнее, чем версия шаблона.

Здесь весь тест можно запустить на GDB.

🤔 А знаете ли вы, что...
Язык C++ обеспечивает возможность множественного наследования классов.


53
1

Ответ:

Решено

I know that std::function is implemented with the type erasure idiom. Type erasure is a handy technique, but as a drawback it needs to store on the heap a register (some kind of array) of the underlying objects.

Стирание типа не обязательно требует выделения кучи. В этом случае, вероятно, реализация std::function не должна будет выполнять выделение кучи, поскольку лямбда не фиксирует никаких переменных. Следовательно, std::function должен хранить указатель на функцию только в самом объекте, а не в памяти, выделенной в куче.

Кроме того, даже если std::function действительно выделяет кучу, некоторые компиляторы могут даже исключить эти распределения кучи.

И последнее, но не менее важное: хотя выделение кучи дороже, чем выделение стека, если вам нужно выделить что-то в куче только один раз на протяжении всей программы, вы, вероятно, не заметите никакой разницы во времени из-за этого выделения.