Этот ответ призван внести в набор существующих ответов то, что я считаю более значимым эталоном стоимости времени выполнения вызовов std :: function.
Механизм std :: function должен быть распознан за то, что он обеспечивает: любую вызываемую сущность можно преобразовать в std :: function соответствующей сигнатуры. Предположим, у вас есть библиотека, которая подгоняет поверхность к функции, определенной с помощью z = f (x, y), вы можете написать ее, чтобы принять a std::function<double(double,double)>
, и пользователь библиотеки может легко преобразовать в нее любую вызываемую сущность; будь то обычная функция, метод экземпляра класса, или лямбда, или все, что поддерживается std :: bind.
В отличие от шаблонных подходов, это работает без перекомпиляции библиотечной функции для разных случаев; соответственно, для каждого дополнительного случая требуется немного дополнительного скомпилированного кода. Это всегда было возможно сделать, но раньше требовалось несколько неуклюжих механизмов, и пользователю библиотеки, вероятно, понадобилось бы создать адаптер вокруг своей функции, чтобы он работал. std :: function автоматически создает любой адаптер, необходимый для получения общего интерфейса вызова во время выполнения для всех случаев, что является новой и очень мощной функцией.
На мой взгляд, это наиболее важный вариант использования std :: function с точки зрения производительности: меня интересует стоимость вызова std :: function много раз после того, как она была создана один раз, и она должна может быть ситуация, когда компилятор не может оптимизировать вызов, зная функцию, которая фактически вызывается (т.е. вам нужно скрыть реализацию в другом исходном файле, чтобы получить надлежащий тест).
Я сделал тест ниже, похожий на ОП; но основные изменения:
- Каждый случай повторяется 1 миллиард раз, но объекты std :: function создаются только один раз. Посмотрев в выходной код, я обнаружил, что «оператор new» вызывается при построении реальных вызовов std :: function (возможно, не тогда, когда они оптимизированы).
- Тест разбит на два файла, чтобы предотвратить нежелательную оптимизацию
- Мои случаи: (а) функция встроена (б) функция передается обычным указателем на функцию (в) функция является совместимой функцией, заключенной в std :: function (d) функция является несовместимой функцией, сделанной совместимой с std :: связать, обернутый как std :: function
Результаты, которые я получаю:
Случай (d) имеет тенденцию быть немного медленнее, но разница (около 0,05 нсек) поглощается шумом.
Вывод состоит в том, что std :: function сравнимо по накладным расходам (во время вызова) с использованием указателя на функцию, даже когда есть простая адаптация «привязки» к реальной функции. Встроенный на 2 нс быстрее, чем другие, но это ожидаемый компромисс, так как встроенный - единственный случай, который «запрограммирован» во время выполнения.
Когда я запускаю код Йохана-Лундберга на той же машине, я вижу около 39 нсек на цикл, но в цикле есть намного больше, включая фактический конструктор и деструктор функции std :: function, которая, вероятно, довольно высока так как включает в себя новый и удалить.
-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 ++ фильтром:
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
тогда и только тогда, когда вам действительно нужна гетерогенная коллекция вызываемых объектов (т. Е. Во время выполнения дополнительная информация не доступна).