Pregunta Implementación de C ++ 11 lambda y modelo de memoria


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 cuidadosamente el impacto en el rendimiento de mis elecciones al escribir un nuevo código. También hago una buena cantidad de programación en tiempo real, p. 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 las lambdas C ++.

Mi comprensión 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 valor de cierre desde una función, uno lo envuelve std::function. ¿Qué sucede con la memoria de cierre en este caso? ¿Se copia de la pila al montón? Se libera cuando el std::function se libera, es decir, se cuenta de referencia como un std::shared_ptr?

Me imagino que en un sistema en tiempo real podría establecer una cadena de funciones lambda, pasando B como un argumento de continuación a A, para que una tubería de procesamiento A->B es creado. En este caso, los cierres A y B se asignarán una vez. Aunque no estoy seguro de si estos se asignarán en la pila o 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 sería asignada y desasignada repetidamente, lo que no sería aceptable para el uso en tiempo real.

En pseudocódigo, un bucle DSP, que creo que va a ser seguro en tiempo real. Quiero ejecutar el bloque de procesamiento A y luego B, donde A llama su argumento. Ambas funciones regresan std::function objetos, entonces f 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 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 este último caso, el cierre se construye en cada iteración del bucle, pero a diferencia del ejemplo anterior, es económico porque es como una llamada a función, no se realizan asignaciones de pila. Por otra parte, me pregunto si un compilador podría "levantar" el cierre y realizar optimizaciones internas.

¿Es esto correcto? Gracias.


75
2017-08-30 17:50


origen


Respuestas:


Mi comprensión 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.

No; es siempre un objeto C ++ con un tipo desconocido, creado en la pila. Un lambda sin captura puede ser convertido en un puntero de función (aunque si es adecuado para las convenciones de llamadas C depende de la implementación), pero eso no significa que es un puntero a la función.

Cuando se debe devolver un valor de cierre desde una función, uno lo envuelve en std :: function. ¿Qué sucede con la memoria de cierre en este caso?

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:

auto lamb = []() {return 5;};

lamb es un objeto de pila Tiene un constructor y un destructor. Y seguirá todas las reglas de C ++ para eso. El tipo de lamb contendrá los valores / referencias que se capturan; serán miembros de ese objeto, al igual que cualquier otro miembro del objeto de cualquier otro tipo.

Puedes dárselo a un std::function:

auto func_lamb = std::function<int()>(lamb);

En este caso, obtendrá un dupdo del valor de lamb. Si lamb había capturado cualquier cosa por valor, habría dos copias de esos valores; uno en lamby uno en func_lamb.

Cuando finaliza el alcance actual, func_lamb será destruido, seguido de lamb, según las reglas de limpieza de variables de pila.

Podrías asignar fácilmente uno en el montón:

auto func_lamb_ptr = new std::function<int()>(lamb);

Exactamente donde está la memoria para el contenido de un std::function va depende de la implementación, pero el borrado de tipo empleado por std::function generalmente requiere al menos una asignación de memoria. Esta es la razón por std::functionEl constructor puede tomar un asignador.

¿Se libera siempre que se libera la función std ::, es decir, se cuenta de referencia como un std :: shared_ptr?

std::function almacena una dupdo de su contenido. Como prácticamente cualquier biblioteca estándar de tipo C ++, function usos semántica de valores. Por lo tanto, es copiable; cuando se copia, el nuevo function el objeto está completamente separado. También se puede mover, por lo que cualquier asignación interna se puede transferir de forma adecuada sin necesidad de asignar ni copiar.

Por lo tanto, no hay necesidad de contar referencias.

Todo lo demás que diga es correcto, suponiendo que "asignación de memoria" equivale a "malo para usar en código en tiempo real".


82
2017-08-30 18:43