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::function
freigegeben 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->B
erstellt 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::function
Objekte zurück, also f
ein std::function
Objekt, 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.
operator()
. Es gibt kein "Heben", Lambdas sind nichts Besonderes. Sie sind nur eine Abkürzung für ein lokales Funktionsobjekt.std::function
der Status auf dem Haufen gespeichert ist oder nicht, und hat nichts mit Lambdas zu tun. Ist das richtig?std::function
!!auto
Rückgabetyp zurückgeben.Antworten:
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.
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:
lamb
ist ein Stapelobjekt. Es hat einen Konstruktor und einen Destruktor. Und dafür werden alle C ++ - Regeln befolgt. Derlamb
Testtyp 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
:In diesem Fall erhält es eine Kopie des Werts von
lamb
. Wennlamb
etwas nach Wert erfasst worden wäre, gäbe es zwei Kopien dieser Werte; eins reinlamb
und eins reinfunc_lamb
.Wenn der aktuelle Bereich endet,
func_lamb
wird er zerstört, gefolgt vonlamb
den Regeln zum Bereinigen von Stapelvariablen.Sie können genauso gut eine auf dem Heap zuweisen:
Wo genau der Speicher für den Inhalt eines
std::function
Go ist, ist implementierungsabhängig, aber die Typlöschung, die von verwendet wird,std::function
erfordert im Allgemeinen mindestens eine Speicherzuweisung. Aus diesem Grundstd::function
kann der Konstruktor einen Allokator verwenden.std::function
speichert eine Kopie seines Inhalts.function
Verwendet wie praktisch jeder Standardbibliothek-C ++ - Typ eine Wertsemantik . Somit ist es kopierbar; Beim Kopieren ist das neuefunction
Objekt 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".
quelle
std::function
ist 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 kopierenstd::function
, ja?std::function
Objekt ohne dynamischen Speicher gespeichert werden kann Zuteilung läuft.function
es 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.C ++ Lambda ist nur eine syntaktische Zucker um (anonyme) Functor-Klasse mit Überladung
operator()
undstd::function
ist 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:
es macht diese Ausgabe:
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.)
quelle