Implementazione lambda C ++ 11 e modello di memoria


92

Vorrei alcune informazioni su come pensare correttamente alle chiusure C ++ 11 e std::functionin termini di come vengono implementate e come viene gestita la memoria.

Anche se non credo nell'ottimizzazione prematura, ho l'abitudine di considerare attentamente l'impatto sulle prestazioni delle mie scelte durante la scrittura di nuovo codice. Faccio anche una discreta quantità di programmazione in tempo reale, ad esempio su microcontrollori e sistemi audio, dove si devono evitare pause di allocazione / deallocazione della memoria non deterministiche.

Pertanto, mi piacerebbe sviluppare una migliore comprensione di quando utilizzare o meno i lambda C ++.

La mia comprensione attuale è che un lambda senza chiusura acquisita è esattamente come un callback C. Tuttavia, quando l'ambiente viene acquisito in base al valore o al riferimento, viene creato un oggetto anonimo nello stack. Quando una chiusura di valore deve essere restituita da una funzione, la si avvolge std::function. Cosa succede alla memoria di chiusura in questo caso? È stato copiato dallo stack all'heap? Viene liberato ogni volta che std::functionviene liberato, ovvero viene contato come riferimento come a std::shared_ptr?

Immagino che in un sistema in tempo reale potrei impostare una catena di funzioni lambda, passando B come argomento di continuazione ad A, in modo che A->Bvenga creata una pipeline di elaborazione . In questo caso, le chiusure A e B sarebbero assegnate una volta. Anche se non sono sicuro se questi sarebbero allocati sullo stack o sull'heap. Tuttavia, in generale, questo sembra sicuro da usare in un sistema in tempo reale. D'altra parte se B costruisce una funzione lambda C, che restituisce, la memoria per C verrebbe allocata e deallocata ripetutamente, il che non sarebbe accettabile per l'utilizzo in tempo reale.

In pseudo-codice, un loop DSP, che penso sarà sicuro in tempo reale. Voglio eseguire il blocco di elaborazione A e poi B, dove A chiama il suo argomento. Entrambe queste funzioni restituiscono std::functionoggetti, quindi fsarà un std::functionoggetto, dove il suo ambiente è memorizzato nell'heap:

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

E uno che penso potrebbe essere cattivo da usare nel codice in tempo reale:

for (t=0; t<1000; t++) {
    y = A(B)(t);
}

E uno in cui penso che la memoria dello stack sia probabilmente utilizzata per la chiusura:

freq = 220;
A = 2;
for (t=0; t<1000; t++) {
    y = [=](int t){ return sin(t*freq)*A; }
}

In quest'ultimo caso la chiusura viene costruita ad ogni iterazione del ciclo, ma a differenza dell'esempio precedente è economica perché è proprio come una chiamata di funzione, non vengono effettuate allocazioni di heap. Inoltre, mi chiedo se un compilatore potrebbe "sollevare" la chiusura e fare ottimizzazioni in linea.

È corretto? Grazie.


4
Non vi è alcun sovraccarico quando si utilizza un'espressione lambda. L'altra scelta sarebbe quella di scrivere tu stesso un tale oggetto funzione, che sarebbe esattamente lo stesso. A proposito, sulla domanda in linea, poiché il compilatore ha tutte le informazioni di cui ha bisogno, di sicuro può semplicemente inline la chiamata al file operator(). Non c'è "sollevamento" da fare, i lambda non sono niente di speciale. Sono solo una scorciatoia per un oggetto funzione locale.
Xeo

Questa sembra essere una domanda se std::functionmemorizza o meno il proprio stato sull'heap e non ha nulla a che fare con i lambda. È giusto?
Mooing Duck

8
Giusto per spiegarlo in caso di malintesi: un'espressione lambda non è una std::function!!
Xeo

1
Solo un commento a margine: fai attenzione quando restituisci un lambda da una funzione, poiché tutte le variabili locali catturate per riferimento diventano non valide dopo aver lasciato la funzione che ha creato lambda.
Giorgio

2
@Steve da C ++ 14 puoi restituire un lambda da una funzione con un autotipo di ritorno.
Oktalist

Risposte:


100

La mia comprensione attuale è che un lambda senza chiusura acquisita è esattamente come un callback C. Tuttavia, quando l'ambiente viene acquisito in base al valore o al riferimento, viene creato un oggetto anonimo nello stack.

No; è sempre un oggetto C ++ di tipo sconosciuto, creato nello stack. Un lambda senza cattura può essere convertito in un puntatore a funzione (anche se il fatto che sia adatto alle convenzioni di chiamata C dipende dall'implementazione), ma ciò non significa che sia un puntatore a funzione.

Quando una chiusura di valore deve essere restituita da una funzione, la si avvolge in std :: function. Cosa succede alla memoria di chiusura in questo caso?

Un lambda non è niente di speciale in C ++ 11. È un oggetto come qualsiasi altro oggetto. Un'espressione lambda restituisce una temporanea, che può essere utilizzata per inizializzare una variabile nello stack:

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

lambè un oggetto stack. Ha un costruttore e un distruttore. E seguirà tutte le regole C ++ per questo. Il tipo di lambconterrà i valori / riferimenti che vengono catturati; saranno membri di quell'oggetto, proprio come qualsiasi altro oggetto membro di qualsiasi altro tipo.

Puoi darlo a std::function:

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

In questo caso, riceverà una copia del valore di lamb. Se lambavesse catturato qualcosa in base al valore, ci sarebbero due copie di quei valori; uno dentro lambe uno dentro func_lamb.

Quando l'ambito corrente termina, func_lambverrà distrutto, seguito da lamb, secondo le regole di pulizia delle variabili dello stack.

Potresti allo stesso modo facilmente allocarne uno sull'heap:

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

Esattamente dove la memoria per i contenuti di un std::functionva dipende dall'implementazione, ma la cancellazione del tipo impiegata da std::functiongeneralmente richiede almeno un'allocazione di memoria. Questo è il motivo std::functionper cui il costruttore di può accettare un allocatore.

Viene liberato ogni volta che la funzione std :: viene liberata, cioè viene conteggiata come riferimento come un std :: shared_ptr?

std::functionmemorizza una copia del suo contenuto. Come praticamente ogni tipo C ++ di libreria standard, functionutilizza la semantica dei valori . Pertanto, è copiabile; quando viene copiato, il nuovo functionoggetto è completamente separato. È anche mobile, quindi qualsiasi allocazione interna può essere trasferita in modo appropriato senza bisogno di ulteriori allocazioni e copie.

Quindi non è necessario il conteggio dei riferimenti.

Tutto il resto che dichiari è corretto, assumendo che "allocazione della memoria" equivale a "cattivo da usare nel codice in tempo reale".


1
Ottima spiegazione, grazie. Quindi la creazione di std::functionè il punto in cui la memoria viene allocata e copiata. Sembra che non ci sia modo di restituire una chiusura (dato che sono allocate in pila), senza prima copiarla in una std::function, sì?
Steve

3
@ Steve: Sì; devi avvolgere un lambda in una sorta di contenitore in modo che esca dall'ambito.
Nicol Bolas

L'intero codice della funzione è stato copiato o la funzione originale è stata assegnata in tempo di compilazione e ha passato i valori di chiusura?
Llamageddon

Voglio aggiungere che lo standard impone più o meno indirettamente (§ 20.8.11.2.1 [func.wrap.func.con] ¶ 5) che se un lambda non cattura nulla, può essere memorizzato in un std::functionoggetto senza memoria dinamica allocazione in corso.
5gon12eder

2
@Yakk: come definisci "grande"? Un oggetto con due puntatori di stato è "grande"? Che ne dici di 3 o 4? Inoltre, la dimensione dell'oggetto non è l'unico problema; se l'oggetto non è spostabile, deve essere memorizzato in un'allocazione, poiché functionha un costruttore di spostamenti noexcept. Il punto centrale del dire "richiede generalmente" è che non sto dicendo " richiede sempre ": che ci sono circostanze in cui non verrà eseguita alcuna allocazione.
Nicol Bolas

0

C ++ lambdaèsolo uno zucchero sintattico attorno (anonimo) alla classe Functor con sovraccarico operator()ed std::functionè solo un wrapper intorno ai chiamabili (cioè funtori, lambda, funzioni c, ...) che copia per valore l '"oggetto lambda solido" dall'attuale stack scope - nell'heap .

Per testare il numero di costruttori / riposizionamenti effettivi ho effettuato un test (utilizzando un altro livello di avvolgimento su shared_ptr ma non è il caso). Guarda tu stesso:

#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)();
}

produce questo output:

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

Esattamente lo stesso insieme di ctor / dtor verrebbe chiamato per l'oggetto lambda allocato nello stack! (Ora chiama Ctor per l'allocazione dello stack, Copy-ctor (+ heap alloc) per costruirlo in std :: function e un altro per fare shared_ptr allocazione heap + costruzione della funzione)

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.