Я знаю, что 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.
Я использовал -O2
. Конечно, будут задействованы оптимизации компилятора, я хотел упомянуть об этом в основном вопросе, но потом забыл.
Проверьте сборку, сгенерированную вашими двумя программами. Они могут быть одинаковыми.
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
действительно выделяет кучу, некоторые компиляторы могут даже исключить эти распределения кучи.
И последнее, но не менее важное: хотя выделение кучи дороже, чем выделение стека, если вам нужно выделить что-то в куче только один раз на протяжении всей программы, вы, вероятно, не заметите никакой разницы во времени из-за этого выделения.
Реализация GCC std::function
не требует выделения кучи для вызываемых объектов размером до 16 байт (на x86-64), вместо этого используется внутренний предварительно выделенный буфер. Грубый набросок того, как это реализовано в GCC
Итак, итог: либо из-за оптимизации компилятора, либо из-за минимального влияния распределения кучи (если оно вообще есть, если вызываемый объект мал), std::function
так же быстр, как вызовы шаблона?
@Giogre было бы точнее сказать, что могу будет таким же быстрым. Нет гарантии, что так будет всегда.
@ Кайл Это решает, спасибо.
Интересный. Я видел, как многие люди говорят «std::function
дорого» на этом сайте. Ну, просто показывает, не верь всему, что читаешь.
Как вы скомпилировали свою программу? В частности, какие оптимизации включены? Умный оптимизатор может преобразовать ваш код, чтобы сделать разницу спорной, и никакая оптимизация ничего не говорит нам о производительности.