C ++ 11 Lambda-Implementierung und Speichermodell

92

Ich möchte einige Informationen darüber, wie man richtig über C ++ 11-Schließungen nachdenkt und std::function wie sie implementiert werden und wie mit Speicher umgegangen wird.

Obwohl ich nicht an vorzeitige Optimierung glaube, habe ich die Angewohnheit, die Auswirkungen meiner Auswahl auf die Leistung beim Schreiben von neuem Code sorgfältig zu berücksichtigen. Ich programmiere auch ziemlich viel in Echtzeit, z. B. auf Mikrocontrollern und für Audiosysteme, bei denen nicht deterministische Speicherzuweisungs- / Freigabepausen vermieden werden sollen.

Daher möchte ich ein besseres Verständnis dafür entwickeln, wann C ++ - Lambdas verwendet werden sollen oder nicht.

Mein derzeitiges Verständnis ist, dass ein Lambda ohne erfassten Verschluss genau wie ein C-Rückruf ist. Wenn die Umgebung jedoch entweder nach Wert oder nach Referenz erfasst wird, wird ein anonymes Objekt auf dem Stapel erstellt. Wenn ein Wertabschluss von einer Funktion zurückgegeben werden muss, wird er eingebunden std::function. Was passiert in diesem Fall mit dem Schließspeicher? Wird es vom Stapel auf den Heap kopiert? Wird es freigegeben, wenn das std::functionfreigegeben wird, dh wird es wie ein Referenzzähler gezählt std::shared_ptr?

Ich stelle mir vor, dass ich in einem Echtzeitsystem eine Kette von Lambda-Funktionen einrichten und B als Fortsetzungsargument an A übergeben könnte, damit eine Verarbeitungspipeline A->Berstellt wird. In diesem Fall würden die A- und B-Schließungen einmal zugewiesen. Obwohl ich nicht sicher bin, ob diese auf dem Stapel oder dem Heap zugeordnet werden würden. Im Allgemeinen scheint dies jedoch in einem Echtzeitsystem sicher zu sein. Wenn B andererseits eine Lambda-Funktion C konstruiert, die es zurückgibt, wird der Speicher für C wiederholt zugewiesen und freigegeben, was für die Echtzeitnutzung nicht akzeptabel wäre.

Im Pseudocode eine DSP-Schleife, die meiner Meinung nach in Echtzeit sicher sein wird. Ich möchte den Verarbeitungsblock A und dann B ausführen, wobei A sein Argument aufruft. Beide Funktionen geben std::functionObjekte zurück, also fein std::functionObjekt, in dem seine Umgebung auf dem Heap gespeichert ist:

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

Und eine, von der ich denke, dass sie im Echtzeitcode schlecht zu verwenden ist:

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

Und einer, bei dem meiner Meinung nach der Stapelspeicher wahrscheinlich für den Abschluss verwendet wird:

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

Im letzteren Fall wird der Abschluss bei jeder Iteration der Schleife erstellt, aber im Gegensatz zum vorherigen Beispiel ist er billig, da er wie ein Funktionsaufruf ist und keine Heap-Zuweisungen vorgenommen werden. Außerdem frage ich mich, ob ein Compiler den Verschluss "aufheben" und Inlining-Optimierungen vornehmen könnte.

Ist das richtig? Danke dir.

Steve
quelle
4
Bei Verwendung eines Lambda-Ausdrucks entsteht kein Overhead. Die andere Möglichkeit wäre, ein solches Funktionsobjekt selbst zu schreiben, was genau dasselbe wäre. Übrigens, in der Inline-Frage, da der Compiler alle Informationen hat, die er benötigt, kann er den Aufruf an einfach sicher inline operator(). Es gibt kein "Heben", Lambdas sind nichts Besonderes. Sie sind nur eine Abkürzung für ein lokales Funktionsobjekt.
Xeo
Dies scheint eine Frage zu sein, ob std::functionder Status auf dem Haufen gespeichert ist oder nicht, und hat nichts mit Lambdas zu tun. Ist das richtig?
Mooing Duck
8
Nur um es zu buchstabieren im Fall von Missverständnissen: Ein Lambda - Ausdruck ist nicht ein std::function!!
Xeo
1
Nur ein Nebenkommentar: Seien Sie vorsichtig, wenn Sie ein Lambda von einer Funktion zurückgeben, da alle durch Referenz erfassten lokalen Variablen nach Verlassen der Funktion, die das Lambda erstellt hat, ungültig werden.
Giorgio
2
@Steve seit C ++ 14 können Sie ein Lambda von einer Funktion mit einem autoRückgabetyp zurückgeben.
Oktalist

Antworten:

100

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

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

Wenn ein Wertabschluss von einer Funktion zurückgegeben werden muss, wird er in die Funktion std :: eingeschlossen. Was passiert in diesem Fall mit dem Schließspeicher?

Ein Lambda ist in C ++ 11 nichts Besonderes. Es ist ein Objekt wie jedes andere Objekt. Ein Lambda-Ausdruck führt zu einem temporären Ausdruck, mit dem eine Variable auf dem Stapel initialisiert werden kann:

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

lambist ein Stapelobjekt. Es hat einen Konstruktor und einen Destruktor. Und dafür werden alle C ++ - Regeln befolgt. Der lambTesttyp enthält die Werte / Referenzen, die erfasst werden. Sie werden Mitglieder dieses Objekts sein, genau wie alle anderen Objektmitglieder eines anderen Typs.

Sie können es geben an std::function:

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

In diesem Fall erhält es eine Kopie des Werts von lamb. Wenn lambetwas nach Wert erfasst worden wäre, gäbe es zwei Kopien dieser Werte; eins rein lambund eins rein func_lamb.

Wenn der aktuelle Bereich endet, func_lambwird er zerstört, gefolgt von lambden Regeln zum Bereinigen von Stapelvariablen.

Sie können genauso gut eine auf dem Heap zuweisen:

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

Wo genau der Speicher für den Inhalt eines std::functionGo ist, ist implementierungsabhängig, aber die Typlöschung, die von verwendet wird, std::functionerfordert im Allgemeinen mindestens eine Speicherzuweisung. Aus diesem Grund std::functionkann der Konstruktor einen Allokator verwenden.

Wird es freigegeben, wenn die std :: -Funktion freigegeben wird, dh wird es wie ein std :: shared_ptr referenzgezählt?

std::functionspeichert eine Kopie seines Inhalts. functionVerwendet wie praktisch jeder Standardbibliothek-C ++ - Typ eine Wertsemantik . Somit ist es kopierbar; Beim Kopieren ist das neue functionObjekt vollständig getrennt. Es ist auch beweglich, sodass interne Zuordnungen entsprechend übertragen werden können, ohne dass weitere Zuordnungen und Kopierungen erforderlich sind.

Somit besteht keine Notwendigkeit für eine Referenzzählung.

Alles andere, was Sie angeben, ist korrekt, vorausgesetzt, "Speicherzuweisung" entspricht "schlecht in Echtzeitcode zu verwenden".

Nicol Bolas
quelle
1
Hervorragende Erklärung, danke. Die Erstellung von std::functionist also der Punkt, an dem der Speicher zugewiesen und kopiert wird. Es scheint zu folgen, dass es keine Möglichkeit gibt, einen Abschluss zurückzugeben (da sie auf dem Stapel zugeordnet sind), ohne zuerst in einen zu kopieren std::function, ja?
Steve
3
@Steve: Ja; Sie müssen ein Lambda in eine Art Behälter einwickeln, damit es den Bereich verlässt.
Nicol Bolas
Wird der Code der gesamten Funktion kopiert oder wird die Kompilierungszeit der ursprünglichen Funktion zugewiesen und die geschlossenen Werte übergeben?
Llamageddon
Ich möchte hinzufügen, dass der Standard mehr oder weniger indirekt vorschreibt (§ 20.8.11.2.1 [func.wrap.func.con] ¶ 5), dass ein Lambda, wenn es nichts erfasst, in einem std::functionObjekt ohne dynamischen Speicher gespeichert werden kann Zuteilung läuft.
5gon12eder
2
@ Yakk: Wie definierst du "groß"? Ist ein Objekt mit zwei Zustandszeigern "groß"? Wie wäre es mit 3 oder 4? Auch die Objektgröße ist nicht das einzige Problem. Wenn das Objekt nicht beweglich ist, muss es in einer Zuordnung gespeichert werden, da functiones einen Konstruktor für die Bewegung ohne Ausnahme gibt. Der springende Punkt bei "allgemein erforderlich" ist, dass ich nicht " immer erforderlich" sage : Es gibt Umstände, unter denen keine Zuordnung durchgeführt wird.
Nicol Bolas
0

C ++ Lambda ist nur eine syntaktische Zucker um (anonyme) Functor-Klasse mit Überladung operator()und std::functionist nur ein Wrapper um Callables (dh Funktoren, Lambdas, C-Funktionen, ...), die das "feste Lambda-Objekt" aus dem aktuellen Wert nach Wert kopieren Stapelbereich - auf den Heap .

Um die Anzahl der tatsächlichen Konstruktoren / Relocatons zu testen, habe ich einen Test durchgeführt (unter Verwendung einer anderen Umhüllungsstufe für shared_ptr, aber dies ist nicht der Fall). Überzeugen Sie sich selbst:

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

es macht diese Ausgabe:

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

Für das vom Stapel zugewiesene Lambda-Objekt würde genau derselbe Satz von ctors / dtors aufgerufen! (Jetzt ruft es Ctor für die Stapelzuweisung auf, Copy-ctor (+ Heap-Zuweisung), um es in std :: function zu erstellen, und einen anderen, um die Heap-Zuweisung shared_ptr + die Funktionskonstruktion durchzuführen.)

Barney
quelle