Ця відповідь покликана сприяти набору існуючих відповідей, що, на мою думку, є більш значущим орієнтиром для вартості виконання викликів std :: function.
Механізм std :: функції повинен бути розпізнаний для того, що він надає: Будь-який об'єкт, що викликається, може бути перетворений на std :: функцію відповідної підпису. Припустимо, у вас є бібліотека, яка відповідає поверхні для функції, визначеної z = f (x, y), ви можете записати її для прийому a std::function<double(double,double)>
, і користувач бібліотеки може легко перетворити на це будь-яке об'єкт, що дзвонить; будь то звичайна функція, метод екземпляра класу, або лямбда, або все, що підтримується std :: bind.
На відміну від шаблонного підходу, це працює без необхідності перекомпілювати функцію бібліотеки для різних випадків; відповідно, небагато зайвого складеного коду потрібно для кожного додаткового випадку. Це завжди можна було зробити, але це вимагало деяких незручних механізмів, і користувачеві бібліотеки, ймовірно, доведеться побудувати адаптер навколо своєї функції, щоб змусити його працювати. std :: функція автоматично створює необхідний адаптер, щоб отримати загальний інтерфейс для виклику виконання для всіх випадків, що є новою та дуже потужною функцією.
На мою думку, це найважливіший випадок використання std :: функції щодо продуктивності: мене цікавлять витрати на виклик std :: функції багато разів після того, як вона була побудована один раз, і їй потрібно бути ситуацією, коли компілятор не в змозі оптимізувати виклик, знаючи функцію, яка фактично викликається (тобто потрібно приховати реалізацію в інший вихідний файл, щоб отримати належний орієнтир).
Я зробив тест нижче, подібний до ОП; але основні зміни:
- Кожен випадок замикається в 1 мільярд разів, але об'єкти функції std :: функціонують лише один раз. Я виявив, дивлячись на вихідний код, що "оператор новий" викликається при побудові фактичних викликів std ::: (можливо, не, коли вони оптимізовані).
- Тест розділений на два файли, щоб запобігти небажаній оптимізації
- Мої випадки: (a) функція вбудована (b) функція передається звичайним покажчиком функції (c) функція є сумісною функцією, оберненою як std :: function (d) функція є несумісною функцією, сумісною з std :: прив’язувати, завернути як std :: function
Отримані результати:
випадок (а) (вбудований) 1,3 нсек
всі інші випадки: 3,3 нсек.
Справа (d), як правило, трохи повільніше, але різниця (приблизно 0,05 нсек) поглинається шумом.
Висновок полягає в тому, що функція std :: може бути порівняною накладними (під час виклику) з використанням функціонального вказівника, навіть коли є проста адаптація на прив'язку до фактичної функції. Вбудована лінія на 2 нс швидша, ніж інші, але це очікуваний компроміс, оскільки інлайнер - єдиний випадок, який є "жорстким" під час виконання.
Коли я запускаю код Йохана-Лундберга на тій же машині, я бачу близько 39 нс на цикл, але в циклі є набагато більше, включаючи власне конструктор і деструктор функції std ::, який, ймовірно, досить високий оскільки це передбачає нове та видалення.
-O2 gcc 4.8.1 до цілі x86_64 (ядро i5).
Зауважте, що код розбивається на два файли, щоб запобігти розширенню компілятора функцій, де вони викликаються (за винятком того випадку, коли він призначений).
----- перший вихідний файл --------------
#include <functional>
// simple funct
float func_half( float x ) { return x * 0.5; }
// func we can bind
float mul_by( float x, float scale ) { return x * scale; }
//
// func to call another func a zillion times.
//
float test_stdfunc( std::function<float(float)> const & func, int nloops ) {
float x = 1.0;
float y = 0.0;
for(int i =0; i < nloops; i++ ){
y += x;
x = func(x);
}
return y;
}
// same thing with a function pointer
float test_funcptr( float (*func)(float), int nloops ) {
float x = 1.0;
float y = 0.0;
for(int i =0; i < nloops; i++ ){
y += x;
x = func(x);
}
return y;
}
// same thing with inline function
float test_inline( int nloops ) {
float x = 1.0;
float y = 0.0;
for(int i =0; i < nloops; i++ ){
y += x;
x = func_half(x);
}
return y;
}
----- другий вихідний файл -------------
#include <iostream>
#include <functional>
#include <chrono>
extern float func_half( float x );
extern float mul_by( float x, float scale );
extern float test_inline( int nloops );
extern float test_stdfunc( std::function<float(float)> const & func, int nloops );
extern float test_funcptr( float (*func)(float), int nloops );
int main() {
using namespace std::chrono;
for(int icase = 0; icase < 4; icase ++ ){
const auto tp1 = system_clock::now();
float result;
switch( icase ){
case 0:
result = test_inline( 1e9);
break;
case 1:
result = test_funcptr( func_half, 1e9);
break;
case 2:
result = test_stdfunc( func_half, 1e9);
break;
case 3:
result = test_stdfunc( std::bind( mul_by, std::placeholders::_1, 0.5), 1e9);
break;
}
const auto tp2 = high_resolution_clock::now();
const auto d = duration_cast<milliseconds>(tp2 - tp1);
std::cout << d.count() << std::endl;
std::cout << result<< std::endl;
}
return 0;
}
Для тих, хто цікавиться, ось адаптер, який побудував компілятор, щоб зробити "mul_by" схожим на float (float) - це "називається", коли функція, створена як bind (mul_by, _1,0.5), викликається:
movq (%rdi), %rax ; get the std::func data
movsd 8(%rax), %xmm1 ; get the bound value (0.5)
movq (%rax), %rdx ; get the function to call (mul_by)
cvtpd2ps %xmm1, %xmm1 ; convert 0.5 to 0.5f
jmp *%rdx ; jump to the func
(тож, можливо, було б трохи швидше, якби я записав 0,5f у прив'язці ...) Зауважте, що параметр 'x' надходить у% xmm0 і просто залишається там.
Ось код в області, де побудована функція, перед викликом test_stdfunc - запустіть через c ++ filt:
movl $16, %edi
movq $0, 32(%rsp)
call operator new(unsigned long) ; get 16 bytes for std::function
movsd .LC0(%rip), %xmm1 ; get 0.5
leaq 16(%rsp), %rdi ; (1st parm to test_stdfunc)
movq mul_by(float, float), (%rax) ; store &mul_by in std::function
movl $1000000000, %esi ; (2nd parm to test_stdfunc)
movsd %xmm1, 8(%rax) ; store 0.5 in std::function
movq %rax, 16(%rsp) ; save ptr to allocated mem
;; the next two ops store pointers to generated code related to the std::function.
;; the first one points to the adaptor I showed above.
movq std::_Function_handler<float (float), std::_Bind<float (*(std::_Placeholder<1>, double))(float, float)> >::_M_invoke(std::_Any_data const&, float), 40(%rsp)
movq std::_Function_base::_Base_manager<std::_Bind<float (*(std::_Placeholder<1>, double))(float, float)> >::_M_manager(std::_Any_data&, std::_Any_data const&, std::_Manager_operation), 32(%rsp)
call test_stdfunc(std::function<float (float)> const&, int)
std::function
якщо і лише тоді, коли вам справді потрібна неоднорідна колекція об'єктів, що дзвоняться (тобто додаткова дискримінаційна інформація недоступна під час виконання).