Frage C ++ 11 Lambda-Implementierung und Speichermodell


Ich hätte gerne Informationen darüber, wie man richtig über C ++ 11-Schließungen denkt und std::function im Hinblick darauf, wie sie implementiert sind und wie Speicher gehandhabt wird.

Obwohl ich nicht an eine vorzeitige Optimierung glaube, habe ich die Angewohnheit, beim Schreiben von neuem Code sorgfältig die Auswirkungen meiner Entscheidungen auf die Leistung zu berücksichtigen. Ich mache auch eine beträchtliche Menge an Echtzeitprogrammierung, z. auf Mikrocontrollern und für Audiosysteme, wo nicht-deterministische Speicherzuweisungs- / Freigabe-Pausen vermieden werden sollen.

Daher möchte ich ein besseres Verständnis darüber entwickeln, wann C ++ - lambdas zu verwenden sind oder nicht.

Mein derzeitiges Verständnis ist, dass ein Lambda ohne erfassten Verschluss genau wie ein C-Callback ist. Wenn die Umgebung jedoch entweder als Wert oder als Verweis erfasst wird, wird ein anonymes Objekt auf dem Stapel erstellt. Wenn eine Wertschließung von einer Funktion zurückgegeben werden muss, wird sie eingeschlossen std::function. Was passiert in diesem Fall mit dem Schließspeicher? Wird es vom Stapel auf den Heap kopiert? Wird es immer wieder befreit? std::function wird freigegeben, d. h. es wird wie ein Referenzzähler gezählt std::shared_ptr?

Ich stelle mir vor, dass ich in einem Echtzeitsystem eine Kette von Lambda-Funktionen aufbauen könnte, die B als Fortsetzungsargument an A weitergibt, so dass eine Verarbeitungspipeline entsteht A->B geschaffen. In diesem Fall würden die A- und B-Sperrungen einmal vergeben. Obwohl ich nicht sicher bin, ob diese auf dem Stapel oder dem Haufen zugeordnet werden würden. Im Allgemeinen scheint dies jedoch in einem Echtzeitsystem sicher zu sein. Auf der anderen Seite, wenn B irgendeine Lambda-Funktion C konstruiert, die es zurückgibt, würde der Speicher für C wiederholt zugewiesen und freigegeben werden, was für eine Echtzeitverwendung nicht akzeptabel wäre.

Im Pseudo-Code eine DSP-Schleife, von der ich denke, dass sie in Echtzeit sicher ist. Ich möchte Verarbeitungsblock A und dann B ausführen, wobei A sein Argument aufruft. Beide Funktionen kehren zurück std::function Objekte, so f wird ein ... sein std::function Objekt, dessen 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 eines, von dem ich denke, dass es in Echtzeitcode schlecht sein könnte:

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

Und eines, bei dem ich denke, Stack-Speicher wird wahrscheinlich für die Schließung verwendet:

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

Im letzteren Fall wird die Schließung bei jeder Iteration der Schleife konstruiert, aber im Gegensatz zu dem vorherigen Beispiel ist es billig, da es wie bei einem Funktionsaufruf keine Heap-Zuordnungen vorgenommen werden. Außerdem frage ich mich, ob ein Compiler die Schließung "aufheben" und Inlining-Optimierungen vornehmen könnte.

Ist das richtig? Vielen Dank.


75
2017-08-30 17:50


Ursprung


Antworten:


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

Nein; es ist immer ein C ++ - Objekt mit unbekanntem Typ, das auf dem Stapel erstellt wurde. Ein Capture-less Lambda kann sein umgewandelt in einen Funktionszeiger (ob es für C-Aufrufkonventionen geeignet ist, ist implementierungsabhängig), aber das bedeutet es nicht ist ein Funktionszeiger.

Wenn eine Wertschließung von einer Funktion zurückgegeben werden muss, wird sie in std :: function umbrochen. 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 Wert, mit dem eine Variable auf dem Stack initialisiert werden kann:

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

lamb ist ein Stapelobjekt. Es hat einen Konstruktor und einen Destruktor. Und es wird alle C ++ Regeln dafür befolgen. Die Art von lamb enthält die Werte / Referenzen, die erfasst werden; Sie werden Mitglieder dieses Objekts sein, genau wie jedes andere Objekt eines anderen Typs.

Du kannst es einem geben std::function:

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

In diesem Fall wird es ein Kopieren von dem Wert von lamb. Ob lamb hatte etwas werthaltig erfasst, es würde zwei Kopien dieser Werte geben; ein in lambund eins in func_lamb.

Wenn der aktuelle Bereich endet, func_lamb wird zerstört, gefolgt von lamb, gemäß den Regeln zum Aufräumen von Stapelvariablen.

Sie könnten genauso gut einen auf den Haufen zuweisen:

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

Genau wo die Erinnerung für den Inhalt eines std::function geht ist implementierungsabhängig, aber die Typ-Löschung verwendet von std::function erfordert im Allgemeinen mindestens eine Speicherzuweisung. Deshalb std::functionDer Konstruktor kann einen Allokator nehmen.

Wird es freigegeben, wenn die std :: -Funktion freigegeben wird, d. H., Wird die Referenz wie ein std :: shared_ptr gezählt?

std::function speichert a Kopieren von seinem Inhalt. Wie praktisch jeder Standardbibliothek C ++ Typ, function Verwendet Wert Semantik. So ist es kopierbar; wenn es kopiert wird, das neue function Objekt ist komplett getrennt. Es ist auch beweglich, so dass alle internen Zuweisungen angemessen übertragen werden können, ohne dass mehr Zuweisungen und Kopien erforderlich sind.

Somit ist eine Referenzzählung nicht erforderlich.

Alles andere, was Sie angeben, ist richtig, vorausgesetzt, dass "Speicherzuweisung" gleichbedeutend mit "schlecht in Echtzeit zu verwenden" ist.


82
2017-08-30 18:43