Me gustaría obtener información sobre cómo pensar correctamente sobre los cierres de C ++ 11 y std::function
en términos de cómo se implementan y cómo se maneja la memoria.
Aunque no creo en la optimización prematura, tengo la costumbre de considerar detenidamente el impacto en el rendimiento de mis elecciones al escribir código nuevo. También hago una buena cantidad de programación en tiempo real, por ejemplo, en microcontroladores y para sistemas de audio, donde se deben evitar las pausas de asignación / desasignación de memoria no deterministas.
Por lo tanto, me gustaría desarrollar una mejor comprensión de cuándo usar o no usar lambdas de C ++.
Mi entendimiento actual es que una lambda sin cierre capturado es exactamente como una devolución de llamada C. Sin embargo, cuando el entorno se captura por valor o por referencia, se crea un objeto anónimo en la pila. Cuando se debe devolver un cierre de valor desde una función, uno lo envuelve std::function
. ¿Qué pasa con la memoria de cierre en este caso? ¿Se copia de la pila al montón? ¿Se libera siempre que std::function
se libera, es decir, se cuenta como referencia como a std::shared_ptr
?
Imagino que en un sistema en tiempo real podría configurar una cadena de funciones lambda, pasando B como un argumento de continuación a A, de modo que A->B
se cree una canalización de procesamiento . En este caso, los cierres A y B se asignarían una vez. Aunque no estoy seguro de si estos se asignarían en la pila o en el montón. Sin embargo, en general, esto parece seguro de usar en un sistema en tiempo real. Por otro lado, si B construye alguna función lambda C, que devuelve, entonces la memoria para C se asignaría y desasignaría repetidamente, lo que no sería aceptable para el uso en tiempo real.
En pseudocódigo, un bucle DSP, que creo que será seguro en tiempo real. Quiero realizar el procesamiento del bloque A y luego B, donde A llama a su argumento. Ambas funciones devuelven std::function
objetos, por f
lo que será un std::function
objeto, donde su entorno se almacena en el montón:
auto f = A(B); // A returns a function which calls B
// Memory for the function returned by A is on the heap?
// Note that A and B may maintain a state
// via mutable value-closure!
for (t=0; t<1000; t++) {
y = f(t)
}
Y uno que creo que podría ser malo para usar en código en tiempo real:
for (t=0; t<1000; t++) {
y = A(B)(t);
}
Y uno en el que creo que la memoria de pila probablemente se use para el cierre:
freq = 220;
A = 2;
for (t=0; t<1000; t++) {
y = [=](int t){ return sin(t*freq)*A; }
}
En el último caso, el cierre se construye en cada iteración del ciclo, pero a diferencia del ejemplo anterior, es barato porque es como una llamada de función, no se realizan asignaciones de montón. Además, me pregunto si un compilador podría "levantar" el cierre y realizar optimizaciones en línea.
¿Es esto correcto? Gracias.
operator()
. No hay que hacer ningún "levantamiento", las lambdas no son nada especial. Son solo una abreviatura de un objeto de función local.std::function
almacena su estado en el montón o no, y no tiene nada que ver con lambdas. ¿Está bien?std::function
!!auto
tipo de retorno.Respuestas:
No; siempre es un objeto C ++ con un tipo desconocido, creado en la pila. Una lambda sin captura se puede convertir en un puntero de función (aunque si es adecuada para las convenciones de llamada de C depende de la implementación), pero eso no significa que sea un puntero de función.
Una lambda no es nada especial en C ++ 11. Es un objeto como cualquier otro objeto. Una expresión lambda da como resultado un temporal, que se puede usar para inicializar una variable en la pila:
lamb
es un objeto de pila. Tiene constructor y destructor. Y seguirá todas las reglas de C ++ para eso. El tipo delamb
contendrá los valores / referencias que se capturan; serán miembros de ese objeto, al igual que cualquier otro miembro de objeto de cualquier otro tipo.Puedes dárselo a un
std::function
:En este caso, obtendrá una copia del valor de
lamb
. Silamb
hubiera capturado algo por valor, habría dos copias de esos valores; uno enlamb
y uno enfunc_lamb
.Cuando finalice el alcance actual,
func_lamb
se destruirá, seguido delamb
, según las reglas de limpieza de las variables de la pila.Con la misma facilidad, podría asignar uno en el montón:
Exactamente donde la memoria para el contenido de un
std::function
va depende de la implementación, pero el borrado de tipo empleado porstd::function
generalmente requiere al menos una asignación de memoria. Ésta es la razón porstd::function
la que el constructor de 'puede tomar un asignador.std::function
almacena una copia de su contenido. Como prácticamente todos los tipos de biblioteca estándar de C ++,function
utiliza semántica de valores . Por tanto, es copiable; cuando se copia, el nuevofunction
objeto está completamente separado. También es movible, por lo que las asignaciones internas se pueden transferir de manera adecuada sin necesidad de más asignaciones y copias.Por tanto, no hay necesidad de contar referencias.
Todo lo demás que indica es correcto, asumiendo que "asignación de memoria" equivale a "mal para usar en código en tiempo real".
fuente
std::function
es el punto en el que se asigna y copia la memoria. Parece deducirse que no hay forma de devolver un cierre (ya que están asignados en la pila), sin copiar primero en unstd::function
, ¿sí?std::function
objeto sin memoria dinámica asignación en curso.function
tiene un constructor de movimiento no excepto. El objetivo de decir "generalmente requiere" es que no estoy diciendo " siempre requiere": que hay circunstancias en las que no se realizará ninguna asignación.C ++ lambda es solo un azúcar sintáctico alrededor de la clase Functor (anónima) con sobrecargado
operator()
ystd::function
es solo un envoltorio alrededor de las llamadas (es decir, functors, lambdas, funciones c, ...) que copia por valor el "objeto lambda sólido" del actual alcance de la pila - al montón .Para probar la cantidad de constructores / relocalizaciones reales, hice una prueba (usando otro nivel de envoltura para shared_ptr pero no es el caso). Ver por ti mismo:
hace esta salida:
¡Se llamaría exactamente el mismo conjunto de ctors / dtors para el objeto lambda asignado a la pila! (Ahora llama a Ctor para la asignación de pila, Copy-ctor (+ heap alloc) para construirlo en std :: function y otro para hacer la asignación de heap shared_ptr + construcción de la función)
fuente