110 Stimmen

C++11 Lambda-Implementierung und Speichermodell

Ich hätte gerne Informationen darüber, wie man C++11 closures und std::function in Bezug auf ihre Implementierung und Speicherverwaltung korrekt betrachtet.

Auch wenn ich nicht an voreilige Optimierungen glaube, pflege ich doch den Performance-Einfluss meiner Entscheidungen beim Schreiben neuer Codes sorgfältig zu überdenken. Ich betreibe außerdem eine beträchtliche Menge an echtzeitfähiger Programmierung, z.B. auf Mikrocontrollern und für Audiosysteme, bei denen nicht-deterministische Speicherzuweisungs-Pausen vermieden werden sollten.

Daher möchte ich ein besseres Verständnis dafür entwickeln, wann man C++ Lambdas verwenden sollte oder nicht.

Mein derzeitiges Verständnis ist, dass eine Lambda-Funktion ohne gefangenen Closure genau wie ein C-Callback ist. Wenn jedoch die Umgebung entweder per Wert oder per Referenz eingefangen wird, wird ein anonymer Objekt auf dem Stack erstellt. Wenn ein Value-Closure von einer Funktion zurückgegeben werden muss, wird es in std::function verpackt. Was passiert in diesem Fall mit dem Closure-Speicher? Wird er vom Stack auf den Heap kopiert? Wird er freigegeben, sobald die std::function freigegeben wird, d.h. wird er referenziert wie ein std::shared_ptr?

Ich stelle mir vor, dass ich in einem Echtzeitsystem eine Kette von Lambda-Funktionen einrichten könnte, indem ich B als Fortsetzungsargument an A übergebe, sodass eine Verarbeitungspipeline A->B erstellt wird. In diesem Fall würden die A- und B-Closures einmal zugeordnet. Ob diese auf dem Stack oder dem Heap zugeordnet werden, bin ich mir jedoch nicht sicher. Doch im Allgemeinen scheint dies in einem Echtzeitsystem sicher zu sein. Andererseits, wenn B eine Lambda-Funktion C erstellt, die es zurückgibt, würde der Speicher für C wiederholt zugeordnet und freigegeben werden, was für eine Echtzeitverwendung nicht akzeptabel wäre.

In Pseudocode eine DSP-Schleife, die ich für echtzeitsicher halte. Ich möchte die Verarbeitungsblöcke A und dann B durchführen, wobei A das Argument aufruft. Beide Funktionen geben std::function-Objekte zurück, sodass f ein std::function-Objekt sein wird, dessen Umgebung im Heap gespeichert ist:

auto f = A(B);  // A gibt eine Funktion zurück, die B aufruft
                // Memory für die von A zurückgegebene Funktion liegt auf dem Heap?
                // Beachten Sie, dass A und B möglicherweise einen Zustand beibehalten
                // über mutable Value-Closure!
for (t=0; t<1000; t++) {
    y = f(t)
}

Und einer, den ich für ungeeignet in Echtzeitcode halte:

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

Und einer, bei dem ich davon ausgehe, dass wahrscheinlich der Stackspeicher für das Closure verwendet wird:

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

In diesem Fall wird das Closure bei jeder Iteration der Schleife konstruiert, aber anders als im vorherigen Beispiel ist es günstig, weil es genau wie ein Funktionsaufruf ist, es werden keine Heap-Zuweisungen gemacht. Darüber hinaus frage ich mich, ob ein Compiler das Closure "anheben" und Inline-Optimierungen durchführen könnte.

Ist das korrekt? Vielen Dank.

117voto

Nicol Bolas Punkte 409659

Mein aktuelles Verständnis ist, dass ein Lambda ohne erfassten Closure genau wie ein C-Callback ist. Wenn jedoch die Umgebung entweder nach Wert oder nach Referenz erfasst wird, wird ein anonymes Objekt auf dem Stapel erstellt.

Nein; es handelt sich immer um ein C++-Objekt mit unbekanntem Typ, das auf dem Stapel erstellt wird. Ein lambda ohne Erfassung kann in einen Funktionszeiger umgewandelt werden (ob dies jedoch für C-Aufrufkonventionen geeignet ist, hängt von der Implementierung ab), aber das bedeutet nicht, dass es ein Funktionszeiger ist.

Wenn ein Wert-Closure aus einer Funktion zurückgegeben werden muss, wird es in std::function verpackt. Was passiert in diesem Fall mit dem Closure-Speicher?

Ein Lambda ist in C++11 nichts Besonderes. Es ist ein Objekt wie jedes andere. Ein Lambda-Ausdruck führt zu einem temporären Objekt, das zur Initialisierung einer Variablen auf dem Stapel verwendet werden kann:

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

lamb ist ein Stapelobjekt. Es hat einen Konstruktor und Destruktor. Und es wird allen C++-Regeln dafür folgen. Der Typ von lamb wird Werte/Referenzen enthalten, die erfasst wurden; diese werden Mitglieder dieses Objekts sein, genau wie Mitglieder anderer Objekte anderer Typen.

Sie können es einer std::function übergeben:

auto func_lamb = std::function(lamb);

In diesem Fall wird es eine Kopie des Werts von lamb erhalten. Wenn lamb etwas nach Wert erfasst hatte, gäbe es zwei Kopien dieser Werte; eine in lamb und eine in func_lamb.

Wenn der aktuelle Bereich endet, wird zuerst func_lamb zerstört, gefolgt von lamb, gemäß den Regeln für das Bereinigen von Stapelvariablen.

Sie könnten genauso gut eins auf dem Heap allozieren:

auto func_lamb_ptr = new std::function(lamb);

Wo der Speicher für den Inhalt einer std::function hingeht, ist implementationsabhängig, aber die Typentfernung, die von std::function verwendet wird, erfordert im Allgemeinen mindestens eine Speicherzuweisung. Deshalb kann der Konstruktor von std::function einen Allocator akzeptieren.

Wird es jedes Mal freigegeben, wenn die std::function freigegeben wird, d. h. ist es wie ein std::shared_ptr reference-gezählt?

std::function speichert eine Kopie ihres Inhalts. Wie praktisch jeder Standardbibliotheks-C++-Typ verwendet function Wertssemantik. Daher ist es kopierbar; wenn es kopiert wird, ist das neue function-Objekt völlig separat. Es ist auch verschiebbar, sodass interne Zuweisungen angemessen übertragen werden können, ohne zusätzliche Zuweisungen und Kopien zu benötigen.

Daher ist keine Referenzzählung erforderlich.

Alles andere, was Sie angeben, ist korrekt, vorausgesetzt, dass "Speicherzuweisung" mit "schlecht für Echtzeitcode" gleichzusetzen ist.

5voto

barney Punkte 1994

Das C++-Lambda ist nur ein syntaktischer Zucker für eine (anonyme) Funktorklasse mit überladenen operator() und std::function ist nur ein Wrapper um Aufrufbare (d.h. Funktoren, Lambdas, C-Funktionen, ...), der das "feste Lambda-Objekt" vom aktuellen Stackbereich in den Heap kopiert.

Um die Anzahl der tatsächlichen Konstruktoren/Umsiedlungen zu testen, habe ich einen Test durchgeführt (unter Verwendung einer weiteren Ebene des Wrappings zu shared_ptr, aber das ist nicht der Fall). Überzeugen Sie sich selbst:

#include 
#include 
#include 

class Funktor {
    std::string gruß;
public:

    Funktor(const Funktor &rhs) {
        this->gruß = rhs.gruß;
        std::cout << "Copy-Ctor \n";
    }
    Funktor(std::string _gruß="Hallo!"): gruß { _gruß } {
        std::cout << "Ctor \n";
    }

    Funktor & operator=(const Funktor & rhs) {
        gruß = rhs.gruß;
        std::cout << "Kopie-zugewiesen\n";
        return *this;
    }

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

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

auto getFpp() {
    std::shared_ptr> fp = std::make_shared>(Funktor{}
    );
    (*fp)();
    return fp;
}

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

Das erzeugt diese Ausgabe:

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

Genau dieselbe Reihe von Konstruktoren/Destruktoren würde für das auf dem Stapel allokierte lambda-Objekt aufgerufen werden! (Jetzt ruft es den Konstruktor für die Speicherallokation auf dem Stapel auf, Copy-ktor (+ Heap-Allok) - um es in std::function zu konstruieren - und einen weiteren für die Erstellung von shared_ptr-Heap-Allokation + Function-Konstruktion)

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X