Esta respuesta está destinada a contribuir, al conjunto de respuestas existentes, lo que creo que es un punto de referencia más significativo para el costo de tiempo de ejecución de las llamadas std :: function.
El mecanismo std :: function debe reconocerse por lo que proporciona: cualquier entidad invocable puede convertirse en una función std :: de firma apropiada. Suponga que tiene una biblioteca que ajusta una superficie a una función definida por z = f (x, y), puede escribirla para aceptar a std::function<double(double,double)>
, y el usuario de la biblioteca puede convertir fácilmente cualquier entidad invocable; ya sea una función ordinaria, un método de una instancia de clase, o una lambda, o cualquier cosa que sea compatible con std :: bind.
A diferencia de los enfoques de plantilla, esto funciona sin tener que volver a compilar la función de biblioteca para diferentes casos; en consecuencia, se necesita poco código compilado adicional para cada caso adicional. Siempre ha sido posible hacer que esto suceda, pero solía requerir algunos mecanismos incómodos, y el usuario de la biblioteca probablemente necesitaría construir un adaptador alrededor de su función para que funcione. std :: function construye automáticamente cualquier adaptador que sea necesario para obtener una interfaz de llamada de tiempo de ejecución común para todos los casos, que es una característica nueva y muy poderosa.
En mi opinión, este es el caso de uso más importante para std :: function en lo que respecta al rendimiento: estoy interesado en el costo de llamar a std :: function muchas veces después de que se haya construido una vez, y necesita puede ser una situación en la que el compilador no puede optimizar la llamada al conocer la función que realmente se llama (es decir, debe ocultar la implementación en otro archivo fuente para obtener un punto de referencia adecuado).
Hice la prueba a continuación, similar a los OP; pero los principales cambios son:
- Cada caso se repite mil millones de veces, pero los objetos std :: function se construyen solo una vez. Al observar el código de salida, descubrí que se llama 'operador nuevo' al construir llamadas std :: function reales (tal vez no cuando están optimizadas).
- La prueba se divide en dos archivos para evitar la optimización no deseada
- Mis casos son: (a) la función está en línea (b) la función pasa por un puntero de función ordinario (c) la función es una función compatible envuelta como std :: function (d) la función es una función incompatible compatible con std :: enlazar, envuelto como std :: function
Los resultados que obtengo son:
El caso (d) tiende a ser un poco más lento, pero la diferencia (aproximadamente 0.05 nseg) se absorbe en el ruido.
La conclusión es que la función std :: es una sobrecarga comparable (en el momento de la llamada) al uso de un puntero de función, incluso cuando hay una simple adaptación de 'vinculación' a la función real. El en línea es 2 ns más rápido que los otros, pero eso es una compensación esperada ya que el en línea es el único caso que está 'conectado' en tiempo de ejecución.
Cuando ejecuto el código de johan-lundberg en la misma máquina, veo unos 39 nseg por ciclo, pero hay mucho más en el ciclo allí, incluido el constructor y destructor real de la función std ::, que probablemente sea bastante alto ya que implica un nuevo y eliminar.
-O2 gcc 4.8.1, a x86_64 objetivo (core i5).
Tenga en cuenta que el código se divide en dos archivos, para evitar que el compilador expanda las funciones donde se llaman (excepto en el caso en que está destinado).
----- primer archivo fuente --------------
#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;
}
----- segundo archivo fuente -------------
#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;
}
Para aquellos interesados, aquí está el adaptador que el compilador construyó para hacer que 'mul_by' parezca un flotador (float): esto se 'llama' cuando se llama a la función creada como 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
(por lo que podría haber sido un poco más rápido si hubiera escrito 0.5f en el enlace ...) Tenga en cuenta que el parámetro 'x' llega en% xmm0 y simplemente permanece allí.
Aquí está el código en el área donde se construye la función, antes de llamar a test_stdfunc: ejecute a través de 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
si y solo si realmente necesita una colección heterogénea de objetos invocables (es decir, no hay más información discriminatoria disponible en tiempo de ejecución).