Я знаю, что 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++ обеспечивает возможность множественного наследования классов.
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
действительно выделяет кучу, некоторые компиляторы могут даже исключить эти распределения кучи.
И последнее, но не менее важное: хотя выделение кучи дороже, чем выделение стека, если вам нужно выделить что-то в куче только один раз на протяжении всей программы, вы, вероятно, не заметите никакой разницы во времени из-за этого выделения.