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

92

Me gustaría obtener información sobre cómo pensar correctamente sobre los cierres de C ++ 11 y std::functionen 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::functionse 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->Bse 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::functionobjetos, por flo que será un std::functionobjeto, 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.

Steve
fuente
4
No hay sobrecarga cuando se usa una expresión lambda. La otra opción sería escribir ese objeto de función usted mismo, que sería exactamente lo mismo. Por cierto, en la pregunta en línea, dado que el compilador tiene toda la información que necesita, seguro que puede simplemente en línea la llamada a 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.
Xeo
Esto parece ser una pregunta sobre si std::functionalmacena su estado en el montón o no, y no tiene nada que ver con lambdas. ¿Está bien?
Mooing Duck
8
Solo para deletrearlo en caso de malentendidos: ¡¡ Una expresión lambda no es un std::function!!
Xeo
1
Solo un comentario lateral: tenga cuidado al devolver un lambda de una función, ya que cualquier variable local capturada por referencia se vuelve inválida después de dejar la función que ha creado el lambda.
Giorgio
2
@Steve desde C ++ 14 puede devolver una lambda de una función con un autotipo de retorno.
Oktalist

Respuestas:

100

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.

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.

Cuando una función debe devolver un cierre de valor, se envuelve en std :: function. ¿Qué pasa 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;};

lambes un objeto de pila. Tiene constructor y destructor. Y seguirá todas las reglas de C ++ para eso. El tipo de lambcontendrá 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:

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

En este caso, obtendrá una copia del valor de lamb. Si lambhubiera capturado algo por valor, habría dos copias de esos valores; uno en lamby uno en func_lamb.

Cuando finalice el alcance actual, func_lambse destruirá, seguido de lamb, según las reglas de limpieza de las variables de la pila.

Con la misma facilidad, podría asignar uno en el montón:

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

Exactamente donde la memoria para el contenido de un std::functionva depende de la implementación, pero el borrado de tipo empleado por std::functiongeneralmente requiere al menos una asignación de memoria. Ésta es la razón por std::functionla que el constructor de 'puede tomar un asignador.

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

std::functionalmacena una copia de su contenido. Como prácticamente todos los tipos de biblioteca estándar de C ++, functionutiliza semántica de valores . Por tanto, es copiable; cuando se copia, el nuevo functionobjeto 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".

Nicol Bolas
fuente
1
Excelente explicación, gracias. Entonces, la creación de std::functiones 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 un std::function, ¿sí?
Steve
3
@Steve: Sí; tienes que envolver un lambda en algún tipo de contenedor para que salga del alcance.
Nicol Bolas
¿Se copia todo el código de la función o se asigna el tiempo de compilación de la función original y se pasan los valores cerrados?
Llamageddon
Quiero agregar que el estándar exige más o menos indirectamente (§ 20.8.11.2.1 [func.wrap.func.con] ¶ 5) que si una lambda no captura nada, se puede almacenar en un std::functionobjeto sin memoria dinámica asignación en curso.
5gon12eder
2
@Yakk: ¿Cómo se define "grande"? ¿Un objeto con dos punteros de estado es "grande"? ¿Qué tal 3 o 4? Además, el tamaño del objeto no es el único problema; si el objeto no es movible por no tirar, debe almacenarse en una asignación, ya que functiontiene 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.
Nicol Bolas
0

C ++ lambda es solo un azúcar sintáctico alrededor de la clase Functor (anónima) con sobrecargado operator()y std::functiones 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:

#include <memory>
#include <string>
#include <iostream>

class Functor {
    std::string greeting;
public:

    Functor(const Functor &rhs) {
        this->greeting = rhs.greeting;
        std::cout << "Copy-Ctor \n";
    }
    Functor(std::string _greeting="Hello!"): greeting { _greeting } {
        std::cout << "Ctor \n";
    }

    Functor & operator=(const Functor & rhs) {
        greeting = rhs.greeting;
        std::cout << "Copy-assigned\n";
        return *this;
    }

    virtual ~Functor() {
        std::cout << "Dtor\n";
    }

    void operator()()
    {
        std::cout << "hey" << "\n";
    }
};

auto getFpp() {
    std::shared_ptr<std::function<void()>> fp = std::make_shared<std::function<void()>>(Functor{}
    );
    (*fp)();
    return fp;
}

int main() {
    auto f = getFpp();
    (*f)();
}

hace esta salida:

Ctor 
Copy-Ctor 
Copy-Ctor 
Dtor
Dtor
hey
hey
Dtor

¡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)

barney
fuente