Gracias a C++11 hemos recibido la std::function de la familia de functor contenedores. Por desgracia, de la que he oído sólo cosas malas acerca de estas nuevas adiciones. La más popular es que son terriblemente lentos. He probado y de verdad se chupan en comparación con las plantillas.

#include <iostream>
#include <functional>
#include <string>
#include <chrono>

template <typename F>
float calc1(F f) { return -1.0f * f(3.3f) + 666.0f; }

float calc2(std::function<float(float)> f) { return -1.0f * f(3.3f) + 666.0f; }

int main() {
    using namespace std::chrono;

    const auto tp1 = system_clock::now();
    for (int i = 0; i < 1e8; ++i) {
        calc1([](float arg){ return arg * 0.5f; });
    }
    const auto tp2 = high_resolution_clock::now();

    const auto d = duration_cast<milliseconds>(tp2 - tp1);  
    std::cout << d.count() << std::endl;
    return 0;
}

111 ms vs 1241 ms. Supongo que esto es debido a que las plantillas pueden ser muy bien entre líneas, mientras que functionla tapa de la parte interna a través de virtual llamadas.

Obviamente plantillas tienen sus problemas como las veo:

  • tienen que ser siempre como encabezados de que no es algo que usted puede no querer hacer cuando la liberación de su biblioteca como un código cerrado,
  • pueden hacer que la compilación de tiempo mucho más largo, a menos que extern template-como la política se introduce,
  • no hay (al menos para mí) limpieza de la manera de representar los requisitos (conceptos, ¿alguien?) de una plantilla, la barra de un comentario que describe qué tipo de functor que se espera.

Puedo asumir así, que functions puede ser utilizado como de facto estándar de pasar functors, y en lugares de alto rendimiento se espera que las plantillas deben ser utilizados?


Edición:

Mi compilador es el Visual Studio 2012 sin CTP.

  • Uso std::function si y sólo si usted realmente necesita un conjunto heterogéneo de los que se puede llamar de objetos (i.e no más discriminar la información está disponible en tiempo de ejecución).
  • Usted está comparando las cosas mal. Las plantillas son usadas en ambos casos – no se trata de «std::function o plantillas». Creo que aquí la cuestión es simplemente un ajuste de una lambda en std::function vs no envolver una lambda en std::function. En el momento en que su pregunta es como preguntar «debo yo prefiero una manzana, o un plato?»
  • Si 1 ns o 10ns, ambos no es nada.
  • 1000% no es nada, aunque. Como el OP identifica, que empezar a cuidar cuando escalabilidad entra en ella para cualquier propósito práctico.
  • Puede ser horriblemente lento en MSVC 10 y por debajo. Gran error que provocaba que los 10 copia de las construcciones.
  • ¿Cómo se mide? Lo que compilar indicadores fueron utilizados?
  • Es 10 veces más lento, que es enorme. La velocidad debe ser comparado con la línea de base; es engañar a pensar que no importa sólo porque es nanosegundos.
  • quizá relevante stackoverflow.com/questions/13722426/…
  • con boost::function, sólo se necesita el 100% más que la versión de la plantilla (2 veces) (con GCC. Clang tomó 0ns para la versión de la plantilla. parece que se ha optimizado lejos). Sospecho que usted debe especificar la aplicación que se utiliza en el punto de referencia.
  • afortunadamente, usted puede tener la revisión si usted paga por VC11.
  • Puede importar si es llamado muchas veces, como en std::sort. En tales casos, prefiero las plantillas (que luego son los que más participan de todos modos). Pero en otros casos, realmente no importa,
  • esa es la razón por la que no voy a utilizar VC11.
  • En la final que resume algunos de los casos donde las plantillas por si sólo no es suficiente. Así que use un std::function cuando usted necesita uno. Un montón de desventajas de la llanura de la plantilla functor argumentos no por arte de magia hacer std::function (que tiene sus propias desventajas, como ustedes han visto a sí mismo) el «estándar de facto».
  • Sí, esa es básicamente la pregunta. Debo envolver el lambda/functor, o no?
  • las plantillas por si sólo no es suficiente. Así que use un std::function cuando usted necesita uno» – El problema es que esto es una tontería, porque std::function es una plantilla de clase.
  • Sí, lo sé. Yo estaba hablando en los términos de la OP, el uso de «plantilla» de una forma arbitraria de plantilla functor argumento vs uno envuelto en una (más especializados) std::function, ignorando que este uso de «plantilla» era un poco impreciso. Por supuesto, un std::function es una plantilla, también, pero al final es más «especializados» / «menos de plantilla» que una simple argumento de plantilla.
  • Sí, mucho menos con plantilla. Se puede declarar una variable a ser, por ejemplo, std::función<float(flotación)>, y, a continuación, almacenar todo tipo de diferentes «funciones» en que una variable (mediante la asignación de cualquier compatible std::se unen a ella, por ejemplo) Los aplicables a los adaptadores están diseñados para hacer de todos ellos compatibles en tiempo de ejecución con una sola ‘de la llamada’ operación. Que la hace muy diferente del uso de una plantilla de función o de plantilla de método de una clase de plantilla o lo que sea, que haga todo su trabajo a la hora de compilar y generar diferentes llamar a código de acuerdo a lo que está siendo llamado. Puse una respuesta a ilustrar.
  • Y como se muestra en varias respuestas, el tiempo en este ejemplo no es significativa. Las cosas están siendo optimizados; y el costo de la construcción de la std::función es la de ser incurridos por llamada, que no es típico. La mayoría de los casos donde la atención acerca de la velocidad, va a ser construido de una vez llamado muchas veces. El costo de la cto r/dtor puede ser mucho más que el costo de la llamada.
  • Por alguna razón no se informó de que los relojes son diferentes. Ambos now() debe estar en high_resolution_clock. Este error se ha propagado a todos los fragmentos que a continuación!
  • en efecto, en el hecho de que el OP del código no compilará en mi Clang, a menos que ambos relojes son el mismo reloj!
  • Hay dos problemas de rendimiento: la ejecución de la plantilla vs std::función y la llamada al constructor de std::función. En el modo de disparador VS2017 los resultados son 1000 vs 7400. Cuando me tire de la std::función de la construcción fuera del bucle (y el uso de una referencia const argumento) la diferencia es de 1000 vs 4477. El último número es entonces la diferencia en la ejecución sólo.

InformationsquelleAutor Red XIII | 2013-02-03

7 Comentarios

  1. 166

    En general, si se enfrenta a un diseño situación que te da la opción, el uso de plantillas. He subrayado la palabra diseño porque creo que lo que usted necesita para centrarse en la distinción entre los casos de uso de std::function y plantillas, que son bastante diferentes.

    En general, la elección de plantillas es sólo un ejemplo de un principio más amplio: tratar de especificar tantas restricciones como sea posible en tiempo de compilación. La razón es simple: si usted puede detectar un error, o un tipo de desajuste, incluso antes de que su programa se genera, no enviar un buggy programa a su cliente.

    Además, como se señaló correctamente, las llamadas a la plantilla de funciones se resuelven de forma estática (es decir, en tiempo de compilación), por lo que el compilador tiene toda la información necesaria para optimizar y posiblemente en línea el código (lo cual no sería posible si la llamada se realiza a través de una vtable).

    Sí, es cierto que la plantilla de apoyo no es perfecto, y C++11 todavía falta un apoyo para los conceptos; sin embargo, no veo cómo std::function ahorraría en ese sentido. std::function no es una alternativa a las plantillas, sino una herramienta para el diseño de situaciones donde las plantillas no pueden ser utilizados.

    Uno de estos casos de uso se presenta cuando usted necesita para resolver una llamada en tiempo de ejecución mediante la invocación de un objeto invocable que se adhiere a una firma, pero cuyo tipo concreto es desconocido en tiempo de compilación. Este suele ser el caso cuando se tiene una colección de devoluciones de llamada de potencialmente diferentes tipos de, pero que necesita invocar de manera uniforme; el tipo y número de los registrados devoluciones de llamada se determina en tiempo de ejecución basado en el estado de su programa y la lógica de la aplicación. Algunas de esas devoluciones de llamada podría ser functors, algunos podrían ser de llanura de funciones, algunas de ellas podrían ser el resultado de la unión de otras funciones a ciertos argumentos.

    std::function y std::bind también ofrecen una frase hecha para la habilitación de programación funcional en C++, donde las funciones son tratadas como objetos y obtener de forma natural al curry y se combinan para generar otras funciones. Aunque este tipo de combinación se puede lograr con plantillas, con un diseño similar situación que normalmente viene junto con los casos de uso que requieren para determinar el tipo de la combinación que se puede llamar de objetos en tiempo de ejecución.

    Por último, hay otras situaciones donde std::function es inevitable, por ejemplo, si desea escribir recursiva de lambdas; sin embargo, estas restricciones son más dictadas por las limitaciones tecnológicas que por distinciones conceptuales creo.

    Para resumir, se centran en el diseño y tratar de entender lo que son los conceptuales casos de uso para estas dos construcciones. Si se ponen en comparación de la forma en que lo hizo, que están obligando a ellos en una arena que es probable que no le pertenecen.

    • Creo que «Este suele ser el caso cuando se tiene una colección de devoluciones de llamada de potencialmente diferentes tipos, pero la que usted necesita para invocar de manera uniforme;» es poco importante. Mi regla de oro es: «Prefiero std::function en el almacenamiento final y la plantilla Fun en la interfaz».
    • Estoy de acuerdo con usted, aunque lo que he tratado de transmitir en mi respuesta es que el OP debería centrarse en los casos de uso para el que las dos construcciones no se superponen, en vez de en los que se superponen y pueden ser comparados con resultados evidentes. Técnicamente, la clave del diseño discriminante es siempre el mismo OMI: si la dinámica de polimorfismo es necesario o no. Y sí, el poco que mencionar es probablemente la más representativa de las situaciones de diseño que requieren dinámica de polimorfismo.
    • Nota: la técnica de ocultar tipos de hormigones, se llama tipo de borrado (que no debe confundirse con el tipo de borrado en lenguajes administrados). Se implementa a menudo en términos de la dinámica de polimorfismo, pero es más potente (por ejemplo, unique_ptr<void> llamar apropiado destructores incluso para los tipos sin destructores virtuales).
    • Estoy de acuerdo en la sustancia, a pesar de que estamos un poco desalineada en la terminología. Dinámica polimorfismo significa para mí «, asumiendo diferentes formas en tiempo de ejecución», en contraposición a la electricidad estática de polimorfismo que yo interpreto como «asumiendo diferentes formas en tiempo de compilación»; la segunda, no pueden ser alcanzados a través de plantillas. Para mí, el tipo de borrado es, el diseño inteligente, una especie de condición previa para poder lograr dinámica polimorfismo en todo: necesitas algún uniforme de la interfaz para interactuar con los objetos de diferentes tipos, y el tipo de borrado es una manera de resumen de distancia, el tipo específico de información.
    • Así, en una forma dinámica de polimorfismo es el concepto de patrón, mientras que el tipo de borrado es una técnica que permite darse cuenta de ello.
    • Yo sería curioso oír lo que se encuentra mal en esta respuesta.

  2. 87

    Andy Acechar tiene bien cubierta problemas de diseño. Este es, por supuesto, muy importante, pero creo que la pregunta original preocupaciones más problemas de rendimiento relacionados con std::function.

    Primero de todo, una rápida observación de la técnica de medición: El 11 ms obtenidos para calc1 no tiene ningún significado en absoluto. De hecho, mirando el ensamblado generado (o depurar el código de la asamblea), se puede ver que VS2012 del optimizador es lo suficientemente inteligente como para darse cuenta de que el resultado de llamar a calc1 es independiente de la iteración y se mueve la llamada fuera del bucle:

    for (int i = 0; i < 1e8; ++i) {
    }
    calc1([](float arg){ return arg * 0.5f; });

    Además, se da cuenta de que llamar a calc1 no tiene ningún efecto visible y cae de la convocatoria por completo. Por lo tanto, la 111ms es el momento en que el vacío bucle tarda en ejecutarse. (Me sorprende que el optimizador se ha mantenido el circuito). Así, tener cuidado con el tiempo de las mediciones en los nudos. Esto no es tan simple como podría parecer.

    Como se ha señalado, el optimizador tiene más problemas para entender std::function y no se mueve la llamada fuera del bucle. Así 1241ms es una medición justa para calc2.

    Cuenta de que, std::function es capaz de almacenar diferentes tipos de callable objetos. Por lo tanto, se debe realizar algún tipo de borrado de magia para el almacenamiento. Generalmente, esto implica una asignación dinámica de memoria (por defecto a través de una llamada a new). Es bien sabido que este es un muy costosa operación.

    El estándar (20.8.11.2.1/5) encorages implementaciones para evitar la asignación dinámica de memoria para los objetos más pequeños que, afortunadamente, VS2012 hace (en particular, para el código original).

    Para tener una idea de cómo mucho más lento se puede conseguir cuando la asignación de memoria está involucrado, he cambiado la expresión lambda para la captura de tres floats. Esto hace que el objeto invocable demasiado grande para aplicar el pequeño objeto de optimización:

    float a, b, c; //never mind the values
    //...
    calc2([a,b,c](float arg){ return arg * 0.5f; });

    Para esta versión, el tiempo es de aproximadamente 16000ms (en comparación con 1241ms para el código original).

    Por último, observe que el tiempo de vida de la lambda encierra la de la std::function. En este caso, en lugar de almacenar una copia de la lambda, std::function podría almacenar una «referencia» a la misma. Por «referencia» me refiero a un std::reference_wrapper que es fácil de construir por funciones std::ref y std::cref. Más precisamente, mediante el uso de:

    auto func = [a,b,c](float arg){ return arg * 0.5f; };
    calc2(std::cref(func));

    el tiempo se reduce aproximadamente 1860ms.

    Escribí acerca de que hace un rato:

    http://www.drdobbs.com/cpp/efficient-use-of-lambda-expressions-and/232500059

    Como dije en el artículo, los argumentos no se aplican para VS2010 debido a su pobre soporte para C++11. En el momento de la escritura, sólo una versión beta de VS2012 estaba disponible, pero su soporte para C++11 ya estaba lo suficientemente bueno para este asunto.

    • Me parece interesante, de hecho, con ganas de hacer una prueba de un código de velocidad usando juguete ejemplos que conseguir optimizado por el compilador, porque no tiene efectos secundarios. Yo diría que rara vez se puede hacer una apuesta en estos tipos de mediciones, sin una real/código de producción.
    • Ghita: En este ejemplo, para impedir que el código optimizado de distancia, calc1 podría tomar un float argumento de que sería el resultado de la iteración anterior. Algo así como x = calc1(x, [](float arg){ return arg * 0.5f; });. Además, debemos asegurarnos de que calc1 utiliza x. Pero, esto no es suficiente todavía. Necesitamos crear un efecto secundario. Por ejemplo, después de la medición, la impresión x en la pantalla. Aunque, estoy de acuerdo en que el uso de juguetes códigos para timimg mediciones no siempre se puede dar un perfecto indicación de lo que va a suceder con el real/código de producción.
    • A mí me parece, también, que el punto de referencia se construye el std::objeto de la función dentro del bucle, y las llamadas calc2 en el bucle. Independientemente de que el compilador puede o no puede optimizar esto, (y que el constructor podría ser tan simple como guardar un vptr), me gustaría estar más interesado en un caso donde la función se construye de una vez, y pasa a otra función que la llama en un bucle. I. e. la sobrecarga de la llamada en lugar de la construcción del concepto de tiempo (y la llamada de la ‘f’ y no de calc2). También estaría interesado en caso de llamar a f en un bucle (en calc2), más de una vez, se beneficiarán de cualquier elevación.
    • Gran respuesta. 2 cosas: un buen ejemplo de un uso válido para std::reference_wrapper (para obligar a las plantillas; no es sólo para el almacenamiento general), y es divertido ver VS optimizador de no descartar un bucle vacío… como me di cuenta de con este GCC bug re volatile.
  3. 37

    Con Clang no hay diferencia de rendimiento entre los dos

    El uso de clang (3.2, tronco 166872) (-O2 en Linux), los binarios de los dos casos son idénticos.

    -Voy a volver a sonar al final del post. Pero primero, gcc 4.7.2:

    Ya hay una gran cantidad de información, pero quiero señalar que el resultado de los cálculos de calc1 y calc2 no son los mismos, debido a la guarnición etc. Comparar por ejemplo la suma de todos los resultados:

    float result=0;
    for (int i = 0; i < 1e8; ++i) {
      result+=calc2([](float arg){ return arg * 0.5f; });
    }

    con calc2 que se convierte en

    1.71799e+10, time spent 0.14 sec

    mientras que con calc1 se convierte en

    6.6435e+10, time spent 5.772 sec

    que es un factor de ~40 en la diferencia de velocidad, y un factor de ~4 en los valores. La primera es mucho más grande diferencia de lo OP publicado (utilizando visual studio). En realidad, la impresión del valor al final también es una buena idea para evitar que el compilador para la eliminación de código con ningún resultado visible (como si la regla). Cassio Neri ya dijo esto en su respuesta. Nota cuán diferentes son los resultados: Uno debe tener cuidado al comparar los factores de velocidad de códigos que realizar diferentes cálculos.

    También, para ser justos, la comparación de diversas formas de calcular repetidamente f(3.3) es, quizás, no tan interesante. Si la entrada es constante no debe estar en un bucle. (Es fácil para que el optimizador aviso)

    Si puedo agregar un usuario proporcionado el argumento de valor para calc1 y 2 el factor de velocidad entre calc1 y calc2 se reduce a un factor de 5, a partir de los 40! Con visual studio, la diferencia es de cerca de un factor de 2, y con clang no hay diferencia (ver más abajo).

    También, como las multiplicaciones son rápidos, hablando acerca de los factores de la desaceleración no es a menudo que interesante. Una pregunta más interesante es, ¿cuán pequeños son sus funciones, y son estas llamadas el cuello de botella en un programa real?

    Clang:

    Clang (yo usé 3.2) que se produce en realidad idénticos binarios cuando le doy la vuelta entre calc1 y calc2 para el código de ejemplo (publicado a continuación). Con el ejemplo original publicado en la pregunta de ambos también son idénticos, pero tomar ningún momento a todos (los bucles son completamente eliminado, como se describe más arriba). Con mi modificado ejemplo, con -O2:

    Número de segundos (mejor de 3):

    clang:        calc1:           1.4 seconds
    clang:        calc2:           1.4 seconds (identical binary)
    
    gcc 4.7.2:    calc1:           1.1 seconds
    gcc 4.7.2:    calc2:           6.0 seconds
    
    VS2012 CTPNov calc1:           0.8 seconds 
    VS2012 CTPNov calc2:           2.0 seconds 
    
    VS2015 (14.0.23.107) calc1:    1.1 seconds 
    VS2015 (14.0.23.107) calc2:    1.5 seconds 
    
    MinGW (4.7.2) calc1:           0.9 seconds
    MinGW (4.7.2) calc2:          20.5 seconds 

    Los resultados calculados de todos los binarios son los mismos, y todas las pruebas se ejecutan en la misma máquina. Sería interesante si alguien con más ruido o VS conocimiento podría comentar sobre lo optimizaciones pueden haber hecho.

    Mi modificado el código de la prueba:

    #include <functional>
    #include <chrono>
    #include <iostream>
    
    template <typename F>
    float calc1(F f, float x) { 
      return 1.0f + 0.002*x+f(x*1.223) ; 
    }
    
    float calc2(std::function<float(float)> f,float x) { 
      return 1.0f + 0.002*x+f(x*1.223) ; 
    }
    
    int main() {
        using namespace std::chrono;
    
        const auto tp1 = high_resolution_clock::now();
    
        float result=0;
        for (int i = 0; i < 1e8; ++i) {
          result=calc1([](float arg){ 
              return arg * 0.5f; 
            },result);
        }
        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;
    }

    Actualización:

    Añadido vs2015. También me di cuenta de que hay un doble->float conversiones en calc1,calc2. La eliminación de ellos no cambia la conclusión de visual studio (ambos son mucho más rápidos, pero la relación es aproximadamente la misma).

    • Que podría decirse que sólo muestra el punto de referencia es incorrecta. En mi humilde opinión el uso interesante del caso es donde el código de llamada recibe un objeto de la función de algún otro lugar, por lo que el compilador no sabe el origen de la std::función de la hora de elaborar la llamada. Aquí, el compilador sabe exactamente la composición de la std::función cuando se llama a la misma, por la expansión de calc2 en línea en principal. Fácilmente fijado por hacer calc2 ‘extern», en sep. archivo de origen. Entonces usted está comparando manzanas con w/naranjas; calc2 está haciendo algo calc1 no puede. Y, el bucle puede ser dentro calc (muchas llamadas a f); no todo el cto r de la función objeto.
    • Buen punto. Por favor, inténtelo si usted está interesado 🙂
    • Cuando puedo llegar a un compilador adecuado. Se puede decir por ahora que (a) cto r para un real std::función de las llamadas ‘nuevas’; (b) la llamada en sí es bastante magra cuando el objetivo es coincidente función real; (c) en los casos de unión, hay un trozo de código que hace la adaptación, seleccionados por un código de ptr en la función obj, y que recoge datos (obligado parámetros) de la función obj (d) la ‘atados’ función puede estar en línea en ese adaptador, si el compilador puede ver.
    • Nueva respuesta añadido con la describe el programa de instalación.
    • Por CIERTO, El punto de referencia no está mal, la pregunta («std::función vs plantilla») sólo es válida en el ámbito de aplicación de la misma unidad de compilación. Si mueve la función a otra unidad, la plantilla no es posible, así que no hay nada para comparar.
  4. 13

    Diferentes no es lo mismo.

    Es más lento porque hace cosas que una plantilla no puede hacer. En particular, se le permite llamar a cualquier función que puede ser llamado con los tipos de argumento y cuyo tipo de retorno es convertible al tipo de retorno desde el mismo código.

    void eval(const std::function<int(int)>& f) {
        std::cout << f(3);
    }
    
    int f1(int i) {
        return i;
    }
    
    float f2(double d) {
        return d;
    }
    
    int main() {
        std::function<int(int)> fun(f1);
        eval(fun);
        fun = f2;
        eval(fun);
        return 0;
    }

    Tenga en cuenta que el mismo objeto de la función, fun, se pasa a las dos llamadas a eval. Tiene dos diferentes funciones.

    Si usted no necesita hacer eso, entonces usted debe no uso std::function.

    • Quiero señalar que cuando ‘fun=f2’ es de hecho, la ‘diversión’ el objeto termina apuntando a un oculto función que convierte a int a double, llamadas f2, y convierte el doble resultado de vuelta a int.(en el ejemplo, ‘f2’ podría obtener en línea en esa función). Si asigna un std::se unen a la diversión, la ‘diversión’ objeto final que contiene los valores a ser utilizados para los parámetros vinculados. para apoyar esta flexibilidad, una de asignar la ‘diversión’ ( o init de) puede implicar asignar/desasignar memoria, y puede tomar más tiempo que la sobrecarga de la llamada.
  5. 8

    Ya tiene algunas buenas respuestas aquí, así que no voy a contradecir, en breve comparación de std::función a las plantillas es como comparar las funciones virtuales a las funciones.
    Usted nunca debe «prefieren» virtual funciones a funciones, pero en lugar de utilizar las funciones virtuales cuando se ajusta el problema, moviendo las decisiones de tiempo de compilación en tiempo de ejecución. La idea es que en lugar de tener que resolver el problema con una solución a medida (como un salto de mesa) se usa algo que te da el compilador de una mejor oportunidad de optimización para usted. También ayuda a otros programadores, si el uso de una solución estándar.

  6. 6

    Esta respuesta es la intención de contribuir, para el conjunto de las respuestas existentes, lo que yo creo que para ser una más significativo punto de referencia para el costo de tiempo de ejecución de std::las llamadas de función.

    El std::función de mecanismo debe ser reconocida por lo que proporciona: que se puede llamar de Cualquier entidad puede ser convertido a un std::la función de la firma apropiada. Supongamos que usted tiene una biblioteca que se ajusta a una superficie a una función definida por z = f(x,y), se puede escribir a aceptar un std::function<double(double,double)>, y el usuario de la biblioteca puede convertir fácilmente cualquier exigible entidad a la que; sea ordinaria de la función, un método de una instancia de una clase, o una lambda, o cualquier cosa que es compatible con std::bind.

    A diferencia de la plantilla de enfoques, esto funciona sin tener que volver a compilar la función de biblioteca para los diferentes casos; en consecuencia, poco más de código compilado es necesario para cada caso. Siempre ha sido posible para que esto suceda, pero se utiliza para requerir algún torpe mecanismos, y el usuario de la biblioteca es probable que la necesidad de construir un adaptador en torno a su función de hacer que funcione. std::función que crea automáticamente cualquier adaptador es necesario para obtener un común tiempo de ejecución interfaz de llamada para todos los casos, lo cual es una nueva y muy potente característica.

    A mi punto de vista, este es el más importante caso de uso para std::función que respecta rendimiento es: estoy interesado en el costo de llamar a un std::función muchas veces después de que se ha construido una vez, y debe ser una situación en la que el compilador no es capaz de optimizar la llamada por el conocimiento de la función en realidad se llama (es decir, usted necesita para ocultar la aplicación en otro archivo de origen para conseguir un correcto punto de referencia).

    Hice el test a continuación, similar a la OP; pero los principales cambios son:

    1. Cada caso bucles de 1 mil millones de veces, pero el std::la función de los objetos se construyen sólo una vez. He encontrado mirando el código de salida que «operador nueva’ se llama cuando la construcción real de std::las llamadas de función (tal vez no cuando están optimizadas).
    2. Prueba está dividida en dos archivos para evitar la indeseada optimización
    3. Mis casos son: (a) la función está alineada (b) la función pasa por una simple función de puntero (c) la función es una función compatible envuelto como std::function (d) la función es incompatible la función de hacer compatible con un std::bind, envuelto como std::función

    Los resultados que obtengo son:

    • caso (a) (en línea) 1.3 ns

    • todos los demás casos: 3.3 nsec.

    Caso (d) tiende a ser un poco más lento, pero la diferencia (aproximadamente 0,05 ns) es absorbida en el ruido.

    Conclusión es que el std::la función es comparable a la cabeza (a la hora de la llamada) para el uso de un puntero a función, incluso cuando no simple ‘enlazar’ adaptación a la función real. La línea es de 2 ns más rápido que los demás, pero que espera una desventaja ya que la línea es el único caso que es el ‘cableado’ en tiempo de ejecución.

    Cuando ejecuto johan-lundberg del código en la misma máquina, estoy viendo unos 39 nsec por bucle, pero hay mucho más en el circuito, incluyendo el real constructor y destructor de la std::función, que es, probablemente, bastante alto, ya que implica un nuevo y eliminar.

    -O2 gcc 4.8.1, para x86_64 destino (core i5).

    Nota, el código está dividido en dos archivos, para evitar que el compilador de la expansión de las funciones para las que están llamadas (excepto en el caso donde se pretende).

    —– primer archivo de origen ————–

    #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 de origen ————-

    #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 los interesados, aquí está el adaptador de que el compilador construido para hacer ‘mul_by’ mira como un flotante(float) – esto es llamado » cuando la función creada como bind(mul_by,_1,0.5) se llama:

    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 yo la hubiera escrito 0.5 f en el enlace…)
    Tenga en cuenta que la ‘x’ parámetro llega en %xmm0 y se queda allí.

    Aquí está el código en el área donde la función es construido, antes de llamar a la test_stdfunc – ejecutar 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)
    • Con clang 3.4.1 x64 los resultados son: (a) 1.0, (b) 0.95, (c) 2.0, (d) 5.0.
  7. 4

    He encontrado tus resultados muy interesante, así que hice un poco de excavación para entender lo que está pasando. En primer lugar, como muchos otros han dicho, sin tener los resultados de la computación efecto el estado del programa, el compilador sólo optimizar esta distancia. En segundo lugar, tener una constante 3.3 dado como un armamento para la devolución de llamada sospecho que habrá otras optimizaciones pasando. Con eso en mente, me cambió su punto de referencia código un poco.

    template <typename F>
    float calc1(F f, float i) { return -1.0f * f(i) + 666.0f; }
    float calc2(std::function<float(float)> f, float i) { return -1.0f * f(i) + 666.0f; }
    int main() {
        const auto tp1 = system_clock::now();
        for (int i = 0; i < 1e8; ++i) {
            t += calc2([&](float arg){ return arg * 0.5f + t; }, i);
        }
        const auto tp2 = high_resolution_clock::now();
    }

    Dado este cambio en el código que se compila con gcc 4.8 -O3 y tiene un tiempo de 330ms para calc1 y 2702 para calc2. Así que, usando la plantilla era 8 veces más rápido, este número se veía a los sospechosos para mí, la velocidad de una potencia de 8 a menudo indica que el compilador ha vectorizados algo. cuando he mirado el código generado para las plantillas de la versión de que fue claramente vectoreized

    .L34:
    cvtsi2ss        %edx, %xmm0
    addl    $1, %edx
    movaps  %xmm3, %xmm5
    mulss   %xmm4, %xmm0
    addss   %xmm1, %xmm0
    subss   %xmm0, %xmm5
    movaps  %xmm5, %xmm0
    addss   %xmm1, %xmm0
    cvtsi2sd        %edx, %xmm1
    ucomisd %xmm1, %xmm2
    ja      .L37
    movss   %xmm0, 16(%rsp)

    Donde como std::función de la versión no fue. Esto tiene sentido para mí, ya que con la plantilla que el compilador sabe que la función nunca va a cambiar todo el bucle, pero con el std::función que se pasa en ella podía cambiar, y por ello no puede ser vectorizadas.

    Esto me llevó a intentar algo más para ver si podía conseguir el compilador para realizar la misma optimización en el std::función de la versión. En lugar de pasar en una función que realizar un std::función global var, y este llamado.

    float calc3(float i) {  return -1.0f * f2(i) + 666.0f; }
    std::function<float(float)> f2 = [](float arg){ return arg * 0.5f; };
    
    int main() {
        const auto tp1 = system_clock::now();
        for (int i = 0; i < 1e8; ++i) {
            t += calc3([&](float arg){ return arg * 0.5f + t; }, i);
        }
        const auto tp2 = high_resolution_clock::now();
    }

    Con esta versión vemos que el compilador tiene ahora vectorizados el código de la misma manera y me da los mismos resultados de referencia.

    • plantilla : 330ms
    • std::función : 2702ms
    • global std::función: 330ms

    Así que mi conclusión es la materia prima de la velocidad de un std::función vs una plantilla functor es prácticamente el mismo. Sin embargo, hace que el trabajo de la optimizador mucho más difícil.

    • El punto es pasar un functor como un parámetro. Su calc3 caso no tiene sentido; calc3 está ahora codificado para llamar a f2. Por supuesto que pueden ser optimizados.
    • de hecho, esto es lo que yo estaba tratando de mostrar. Que calc3 es equivalente a la plantilla, y en esa situación es, efectivamente, un tiempo de compilación construir como una plantilla.

Dejar respuesta

Please enter your comment!
Please enter your name here