Gracias a C ++ 11 recibimos la std::function
familia de envoltorios de functor. Desafortunadamente, sigo escuchando solo cosas malas sobre estas nuevas incorporaciones. Lo más popular es que son horriblemente lentos. Lo probé y realmente apestan 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 frente a 1241 ms. Supongo que esto se debe a que las plantillas pueden estar bien alineadas, mientras que function
las internas se cubren mediante llamadas virtuales.
Obviamente, las plantillas tienen sus problemas tal como los veo:
- deben proporcionarse como encabezados, lo cual no es algo que quizás no desee hacer al lanzar su biblioteca como código cerrado,
- pueden hacer que el tiempo de compilación sea mucho más largo a menos
extern template
que se introduzca una política similar, - no hay una forma limpia (al menos conocida por mí) de representar los requisitos (conceptos, ¿alguien?) de una plantilla, sin incluir un comentario que describa qué tipo de functor se espera.
¿Puedo suponer que function
s se puede usar como estándar de facto de los pasadores, y en lugares donde se espera que se usen plantillas de alto rendimiento?
Editar:
Mi compilador es Visual Studio 2012 sin CTP.
c++
templates
c++11
std-function
Rojo XIII
fuente
fuente
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).std::function
o plantillas". Creo que aquí el problema es simplemente envolver una lambda en lugar destd::function
no envolver una lambdastd::function
. Por el momento, su pregunta es como preguntar "¿debería preferir una manzana o un tazón?"Respuestas:
En general, si se enfrenta a una situación de diseño que le permite elegir, utilice plantillas . Hice hincapié en la palabra diseño porque creo que lo que debe centrarse es la distinción entre los casos de uso
std::function
y las plantillas, que son bastante diferentes.En general, la elección de plantillas es solo una instancia de un principio más amplio: intente especificar tantas restricciones como sea posible en tiempo de compilación . La razón es simple: si puede detectar un error o una falta de coincidencia de tipos, incluso antes de que se genere su programa, no enviará un programa defectuoso a su cliente.
Además, como señaló correctamente, las llamadas a las funciones de plantilla se resuelven estáticamente (es decir, en tiempo de compilación), por lo que el compilador tiene toda la información necesaria para optimizar y posiblemente alinear el código (lo que no sería posible si la llamada se realizara a través de un vtable).
Sí, es cierto que el soporte de plantillas no es perfecto, y C ++ 11 todavía carece de soporte para conceptos; Sin embargo, no veo cómo
std::function
podría salvarte a ese respecto.std::function
no es una alternativa a las plantillas, sino más bien una herramienta para situaciones de diseño donde las plantillas no se pueden usar.Uno de estos casos de uso surge cuando necesita resolver una llamada en tiempo de ejecución invocando un objeto invocable que se adhiere a una firma específica, pero cuyo tipo concreto es desconocido en tiempo de compilación. Este suele ser el caso cuando tiene una colección de devoluciones de llamada de tipos potencialmente diferentes , pero que necesita invocar de manera uniforme ; El tipo y el número de las devoluciones de llamada registradas se determinan en tiempo de ejecución en función del estado de su programa y la lógica de la aplicación. Algunas de esas devoluciones de llamada podrían ser functores, algunas podrían ser funciones simples, otras podrían ser el resultado de vincular otras funciones a ciertos argumentos.
std::function
ystd::bind
también ofrecen un lenguaje natural para habilitar la programación funcional en C ++, donde las funciones se tratan como objetos y se combinan y se combinan naturalmente para generar otras funciones. Aunque este tipo de combinación también se puede lograr con plantillas, una situación de diseño similar normalmente se combina con casos de uso que requieren determinar el tipo de los objetos invocables combinados en tiempo de ejecución.Finalmente, hay otras situaciones en las que
std::function
es inevitable, por ejemplo, si desea escribir lambdas recursivas ; sin embargo, estas restricciones están más dictadas por limitaciones tecnológicas que por distinciones conceptuales, creo.En resumen, concéntrese en el diseño e intente comprender cuáles son los casos de uso conceptual para estos dos constructos. Si los comparas de la manera en que lo hiciste, los estás forzando a una arena a la que probablemente no pertenecen.
fuente
std::function
en el extremo de almacenamiento y la plantillaFun
en la interfaz".unique_ptr<void>
llamar a destructores apropiados incluso para tipos sin destructores virtuales).Andy Prowl ha cubierto muy bien los problemas de diseño. Esto es, por supuesto, muy importante, pero creo que la pregunta original se refiere a más problemas de rendimiento relacionados
std::function
.En primer lugar, una observación rápida sobre la técnica de medición: los 11 ms obtenidos
calc1
no tienen ningún significado. De hecho, al observar el ensamblaje generado (o depurar el código de ensamblaje), se puede ver que el optimizador de VS2012 es lo suficientemente inteligente como para darse cuenta de que el resultado de la llamadacalc1
es independiente de la iteración y saca la llamada del bucle:Además, se da cuenta de que las llamadas
calc1
no tienen un efecto visible y las abandona por completo. Por lo tanto, los 111 ms es el tiempo que tarda el ciclo vacío en ejecutarse. (Me sorprende que el optimizador haya mantenido el bucle). Por lo tanto, tenga cuidado con las mediciones de tiempo en bucles. Esto no es tan simple como parece.Como se ha señalado, el optimizador tiene más problemas para comprender
std::function
y no mueve la llamada fuera del bucle. Entonces 1241ms es una medida justa paracalc2
.Tenga en cuenta que
std::function
puede almacenar diferentes tipos de objetos invocables. Por lo tanto, debe realizar alguna magia de borrado de tipo para el almacenamiento. En general, esto implica una asignación de memoria dinámica (por defecto a través de una llamada anew
). Es bien sabido que esta es una operación bastante costosa.El estándar (20.8.11.2.1 / 5) codifica implementaciones para evitar la asignación de memoria dinámica para objetos pequeños que, afortunadamente, VS2012 (en particular, para el código original).
Para tener una idea de cuánto más lento puede ser cuando la asignación de memoria está involucrada, he cambiado la expresión lambda para capturar tres
float
s. Esto hace que el objeto invocable sea demasiado grande para aplicar la optimización de objetos pequeños:Para esta versión, el tiempo es de aproximadamente 16000 ms (en comparación con 1241 ms para el código original).
Finalmente, observe que la vida útil 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 ella. Por "referencia" me refiero a unstd::reference_wrapper
que se construye fácilmente por funcionesstd::ref
ystd::cref
. Más precisamente, usando:el tiempo disminuye a aproximadamente 1860 ms.
Escribí sobre eso hace un tiempo:
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 escaso soporte para C ++ 11. En el momento de la redacción de este documento, solo estaba disponible una versión beta de VS2012, pero su compatibilidad con C ++ 11 ya era lo suficientemente buena para este asunto.
fuente
calc1
podría tomar unfloat
argumento que sería el resultado de la iteración anterior. Algo así comox = calc1(x, [](float arg){ return arg * 0.5f; });
. Además, debemos asegurarnos de que loscalc1
usosx
. Pero, esto aún no es suficiente. Necesitamos crear un efecto secundario. Por ejemplo, después de la medición, imprimirx
en la pantalla. Sin embargo, estoy de acuerdo en que el uso de códigos de juguetes para mediciones de tiempo no siempre puede dar una indicación perfecta de lo que sucederá con el código real / de producción.std::reference_wrapper
(forzar plantillas; no es solo para almacenamiento general), y es divertido ver que el optimizador de VS no descarta un bucle vacío ... como noté con este error de GCCvolatile
.Con Clang no hay diferencia de rendimiento entre los dos
Usando clang (3.2, trunk 166872) (-O2 en Linux), los binarios de los dos casos son realmente idénticos .
-Vuelvo a sonar al final del post. Pero primero, gcc 4.7.2:
Ya hay mucha información en curso, pero quiero señalar que el resultado de los cálculos de calc1 y calc2 no es el mismo, debido al revestimiento, etc. Compare, por ejemplo, la suma de todos los resultados:
con calc2 que se convierte
mientras que con calc1 se convierte
eso es un factor de ~ 40 en la diferencia de velocidad, y un factor de ~ 4 en los valores. El primero es una diferencia mucho más grande que lo que OP publicó (usando Visual Studio). En realidad, imprimir el valor al final también es una buena idea para evitar que el compilador elimine el código sin un resultado visible (como regla if). Cassio Neri ya dijo esto en su respuesta. Tenga en cuenta cuán diferentes son los resultados: se debe tener cuidado al comparar los factores de velocidad de los códigos que realizan diferentes cálculos.
Además, para ser justos, comparar varias formas de calcular repetidamente f (3.3) quizás no sea tan interesante. Si la entrada es constante, no debería estar en un bucle. (Es fácil que el optimizador lo note)
Si agrego un argumento de valor proporcionado por el usuario a calc1 y 2, el factor de velocidad entre calc1 y calc2 se reduce a un factor de 5, ¡de 40! Con Visual Studio, la diferencia es cercana a un factor de 2, y con el sonido metálico no hay diferencia (ver más abajo).
Además, como las multiplicaciones son rápidas, hablar de factores de desaceleración a menudo no es tan interesante. Una pregunta más interesante es, ¿qué tan pequeñas son sus funciones, y son estas llamadas el cuello de botella en un programa real?
Sonido metálico:
Clang (usé 3.2) en realidad produjo binarios idénticos cuando cambio entre calc1 y calc2 para el código de ejemplo (publicado a continuación). Con el ejemplo original publicado en la pregunta, ambos también son idénticos, pero no tardan en absoluto (los bucles se eliminan por completo como se describe anteriormente). Con mi ejemplo modificado, con -O2:
Número de segundos para ejecutar (mejor de 3):
Los resultados calculados de todos los archivos binarios son los mismos, y todas las pruebas se ejecutaron en la misma máquina. Sería interesante si alguien con un sonido más profundo de claxon o VS pudiera comentar qué optimizaciones pueden haberse realizado.
Mi código de prueba modificado:
Actualizar:
Añadido vs2015. También noté que hay conversiones de flotación doble-> en calc1, calc2. Eliminarlos no cambia la conclusión para Visual Studio (ambos son mucho más rápidos pero la proporción es casi la misma).
fuente
Diferente no es lo mismo.
Es más lento porque hace cosas que una plantilla no puede hacer. En particular, le permite llamar a cualquier función que se pueda llamar con los tipos de argumento dados y cuyo tipo de retorno es convertible al tipo de retorno dado desde el mismo código .
Tenga en cuenta que el mismo objeto de función
fun
, se pasa a ambas llamadas aeval
. Tiene dos funciones diferentes .Si no necesita hacer eso, entonces debería no utilizar
std::function
.fuente
Ya tiene algunas buenas respuestas aquí, por lo que no voy a contradecirlas, en resumen, comparar std :: function con plantillas es como comparar funciones virtuales con funciones. Nunca debe "preferir" las funciones virtuales a las funciones, sino que usa funciones virtuales cuando se ajusta al problema, moviendo las decisiones del tiempo de compilación al tiempo de ejecución. La idea es que, en lugar de tener que resolver el problema utilizando una solución a medida (como una tabla de salto), utilice algo que le brinde al compilador una mejor oportunidad de optimizarlo. También ayuda a otros programadores, si usa una solución estándar.
fuente
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:
Los resultados que obtengo son:
caso (a) (en línea) 1.3 nseg
todos los demás casos: 3,3 nseg.
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 --------------
----- segundo archivo fuente -------------
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):
(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:
fuente
Sus resultados me parecieron muy interesantes, así que investigué un poco para comprender lo que está sucediendo. En primer lugar, como muchos otros han dicho, sin tener los resultados del efecto de cálculo, el estado del programa, el compilador simplemente optimizará esto. En segundo lugar, teniendo un 3.3 constante dado como armamento para la devolución de llamada, sospecho que habrá otras optimizaciones en curso. Con eso en mente, cambié un poco su código de referencia.
Dado este cambio en el código, compilé con gcc 4.8 -O3 y obtuve un tiempo de 330 ms para calc1 y 2702 para calc2. Entonces, usar la plantilla fue 8 veces más rápido, este número me pareció sospechoso, la velocidad de una potencia de 8 a menudo indica que el compilador ha vectorizado algo. cuando miré el código generado para la versión de las plantillas, estaba claramente vectorizado
Donde como la versión std :: function no era. Esto tiene sentido para mí, ya que con la plantilla el compilador sabe con certeza que la función nunca cambiará a lo largo del ciclo, pero con la función std :: que se pasa podría cambiar, por lo tanto no se puede vectorizar.
Esto me llevó a probar algo más para ver si podía hacer que el compilador realizara la misma optimización en la versión std :: function. En lugar de pasar una función, hago una función std :: como var global, y hago que se llame.
Con esta versión, vemos que el compilador ahora ha vectorizado el código de la misma manera y obtengo los mismos resultados de referencia.
Entonces, mi conclusión es que la velocidad bruta de una función std :: vs un functor de plantilla es más o menos la misma. Sin embargo, hace que el trabajo del optimizador sea mucho más difícil.
fuente
calc3
caso no tiene sentido; calc3 ahora está codificado para llamar a f2. Por supuesto que se puede optimizar.