std::move einsetzen
-
Hi,
wann setzt ihr std::move von Hand ein, also wann kann man dadurch die Performance erhöhen? Nur bei eigenen Containern, Listen?
Oder macht das auch im Alltag Sinn, in irgendeinem Szenario, das man in C++03 immer so geschrieben hat, doch JETZT sollte man es anders machen?
Eigentlich nahm ich ja an, dass der Compiler automatisch optimiert, also das beispielsweise bei RVO nutzt, wenn die Rückgabe ein vector ist oder so (<- hat er vielleicht eh schon vorher gemacht, aber weiß ich eben nicht).
Klar ist, dass der Compiler es nicht an Stellen machen kann, an denen er nicht weiß, ob die "Quell"-Variable den Inhalt noch nutzt. Aber auch das erkennt der schlaue Compiler vermutlich und optimiert entsprechend?
Wollte das einfach Mal gefragt haben.
Vielen Dank und beste Grüße!
-
Grundsaetzlich ist es wichtig zu verstehen, dass std::move eigentlich gar nichts moved. Eigentlich sollte die Funktion eher std::make_movable heissen, oder so aehnlich. Und einsetzen tu' ich es ueberall, wo ich ein Objekt per Value Uebergebe, das gemoved werden kann. Die Ausnahme dafuer ist die Rueckgabe lokaler Variablen und Parameter - da wird ein automatischer Move vom Standard garantiert. Und natuerlich, wenn ich das Objekt nacher noch brauche.
-
Okay, super, so in etwa hatte ich mir das auch ausgemalt. Dankeschön
-
Eisflamme schrieb:
wann setzt ihr std::move von Hand ein
Immer dann, wenn es Sinn macht. Und so oft kommt das jetzt nicht vor, weil in vielen Situationen "implizit" gemoved wird -- nämlich dann, wenn sonst RVO oder eine andere "copy elision" erlaubt wäre.
Eisflamme schrieb:
also wann kann man dadurch die Performance erhöhen? Nur bei eigenen Containern, Listen?
Bei allem möglichen ... z.B. auch std::shared_ptr oder std::string. Wenn du einen shared_ptr von A nach B "movest", dann kann man sich das atomare Referenzzähler-Update sparen.
Eisflamme schrieb:
Oder macht das auch im Alltag Sinn, in irgendeinem Szenario, das man in C++03 immer so geschrieben hat, doch JETZT sollte man es anders machen?
Bei Typen von denen Du weißt, dass sie sehr schnell "moven", kann man sich hier und da das mit den Referenzen sparen und doch zu pass-by-value übergehen. Beispiel:
class person { std::string name; public: explicit person(std::string n) : name(std::move(n)) {} };
Eine (logische) Kopie vom Argument wird ja hier eh benötigt und das kann man dem Compiler per pass-by-value überlassen. Also: Weniger explizit kopieren, mehr pass-by-value -- zumindest für move-optimierte Typen.
oder
std::string flip(std::string text) { std::reverse(begin(text),end(text)); return text; }
Das hätte man früher mit Referenz-auf-const gemacht und man hätte explizit eine Kopie erzeugt. Hier wird nirgenswo etwas explizit kopiert und das ist gut so.
Eisflamme schrieb:
Klar ist, dass der Compiler es nicht an Stellen machen kann, an denen er nicht weiß, ob die "Quell"-Variable den Inhalt noch nutzt. Aber auch das erkennt der schlaue Compiler vermutlich und optimiert entsprechend?
Nein. Vom Compiler wird hier nicht viel Intelligenz abverlangt. Er analysiert nicht, wann und wo ob ein Objekt zuletzt verwendet wird, so dass implizit "gemoved" werden kann. Diese impliziten Moves sind wirklich nur auf die Situationen beschränkt, in denen dem Compiler schon immer eine "copy elision" erlaubt war, also bei RVO und immer dann, wenn man das Quellobjekt ein Rvalue ist -- wobei lezteres gar keine Optimierung mehr erfordert, weil da schon die Überladungsauflösung von allein den Move-Constructor wählen würde.
-
Okay, das ist wirklich Mal eine enorme Erkenntnis. Ich hantiere nämlich fast überall mit const string& rum und das ist an enorm vielen Stellen ja wirklich unnötig.
Der Vorteil an flip ist hier dann vor allem also die eindeutigere Code-Semantik, da eine Kopie auch als Kopie ausgewiesen werden kann, und weniger Schreibarbeit, richtig? Oder hat das in diesem Fall auch Performance-Vorteile ggü. der Referenz? Wenn der Code so lang ist, dass er nicht geinlined würde, dann hätte man bei einer const-Referenz bei jedem Zugriff ja noch die Indirektion, oder? Da könnte man sich ja vorstellen, dass bei einer Funktion mit sehr vielen Operationen auf den String sich eben diese weggefallene Indirektion bemerkbar machen könnte (marginal, ich weiß).
-
Das Blöde an
string flip(string const& text) { strint kopie = test; // <-- hier std::reverse(begin(kopie),end(kopie)); return kopie; }
ist, dass hier ggf eine unnötige Kopie erzeugt wird, wohingegen bei
string flip(string kopie) { std::reverse(begin(kopie),end(kopie)); return kopie; }
es die Sache des Compiler ist, woher "kopie" kommt. "kopie" entsteht hier entweder tatsächlich durch Aufruf eines Kopierkonstruktors (wenn das Argument ein Lvalue war) oder eben durch copy-elision bzw Move-Konstruktor (wenn das Argument ein Rvalue war).
Das kannst du eigentlich nur noch mit der const&/&&-Überladung toppen, weil dadurch noch ein Move gespart werden kann. Aber in diesem Fall lohnt sich das meiner Meinung nach nicht.
-
flip("abcdef");
führt im string const& Fall zu der Erzeugung von 2 Strings (einer wird erzeugt um die Funktion aufrufen zu können und ein zweiter wird von der Funktion als Kopie angelegt).
-
Das hätte man früher mit Referenz-auf-const gemacht und man hätte explizit eine Kopie erzeugt. Hier wird nirgenswo etwas explizit kopiert und das ist gut so.
string flip(string const& text) { string kopie = test; // <-- hier std::reverse(begin(kopie),end(kopie)); return kopie; }
Wieso text nicht direkt als Kopie uebergeben? Welchen Sinn hat denn die Uebergabe als Referenz, wenn man den String intern sowieso veraendern muss.
-
Wieso text nicht direkt als Kopie uebergeben? [...]
Das ist sowieso eine bescheuerte Methode.
string flip(string const& text) { return string(text.rbegin(), text.rend()); }
Edit: Das wird hier bestimmt so stark optimiert, dass es im Endeffekt sogar nicht langsamer sein dürfte als eine ganz normale Kopie.
Edit²: Geprüft, und leider ist es nicht der Fall.flip
ist 1,5 bis 2 mal Langsamer als eine Kopie...
Edit³: Ahh, aber es ist gleichauf mit einer normalen Start-End-Iteratorenpaar Kopie.
-
Ferris schrieb:
Wieso text nicht direkt als Kopie uebergeben? Welchen Sinn hat denn die Uebergabe als Referenz, wenn man den String intern sowieso veraendern muss.
In C++03 hätte ich
string flip(string x) { std::reverse(x.begin(),x.end()); return x; // Kein NRVO, aber immerhin ein Move ab C++11 }
nicht geschrieben (in C++11 aber schon!), weil kein C++ Compiler in diesem Fall NRVO durchführen kann. Und weil std::string vor C++11 keinen Move-Konstruktor hatte, wird der return-Wert der Funktion immer von x kopiert. Das ist jetzt nicht besser als
string flip(string const& c) { string x = c; std::reverse(x.begin(),x.end()); return x; // NRVO anwendbar! }
weil hier NRVO für x greift. Die optimale Lösung, die in C++03 in allen Situationen bei einen gescheiten Compiler die Zahl der dynamischen Speicherallozierungen minimiert sieht tatsächlich so aus:
string flip(string c) { string x; // Annahme: Default-Ctor kostet praktisch nichts x.swap(c); // Annahme: swap kostet praktisch nichts std::reverse(x.begin(),x.end()); return x; // NRVO anwendbar! }
Hier wird weder explizit kopiert noch NRVO ausgehebelt. Das Objekt
c
kann ggf durch per copy-elision erzeugt werden. Hier werden alle copy-elision Tricks ausgenutzt, die vom Standard erlaubt und von Compilern tatsächlich unterstützt werden.Sone schrieb:
Wieso text nicht direkt als Kopie uebergeben? [...]
Das ist sowieso eine bescheuerte Methode.
string flip(string const& text) { return string(text.rbegin(), text.rend()); }
Ich hoffe, dass das nur dein C++03-Ansatz ist; denn sonst würdest du die move-Optimierungen von string gar nicht ausnutzen. Aber in C++03 hättest du hier noch copy-elision für den Parameter ausnutzen können. Copy-Elision für Parameter zusammen mit RVO ist möglich (siehe swap-Trick).
-
Kurze Nachfrage:
Bei (trivialen) Gettern sollte man dann also auch nicht mehr mit const Typ& als Rückgabetyp arbeiten, sondern einfach nur Typ schreiben, wenn Typ moveable ist? Oder ist das egal, weil so oder so eben genau eine Kopie gemacht wird?
-
Eisflamme schrieb:
Bei (trivialen) Gettern sollte man dann also auch nicht mehr mit const Typ& als Rückgabetyp arbeiten, sondern einfach nur Typ schreiben, wenn Typ moveable ist?
Nö, nicht unbedingt.
Eisflamme schrieb:
Oder ist das egal, weil so oder so eben genau eine Kopie gemacht wird?
Egal ist das auch nicht. Der Nutzer will den String vielleicht nur ausgeben/untersuchen und braucht daher keine eigene Kopie
-
Oh... ist natürlich richtig. Danke!
-
Auch kann man folgendes machen:
struct foo { std::string const& s() const & { return s_; } std::string && s() && { return std::move(s_); } private: std::string s_; };
Das funktioniert dann auch mit Temporaries richtig.
-
Aber bei einem non-const Get der Klasse ihren s_-Inhalt zu klauen ist doch sicher nicht beabsichtigt? Oder missverstehe ich was? Ich dachte, && sei nur eine rvalue-Referenz, wieso benötigt es dann einen Move?
Und die Syntax:
`string&& func()
**&&**
{return move(s_);}` (also mit den && nach Funktionsnamen) habe ich auch noch nie gesehen, ist && mittlerweile auch ein Qualifizierer? Vielleicht muss ich mir einfach nochmal ein paar Artikel zu den rvalue-Referenzen durchlesen..
-
Eisflamme schrieb:
Ich dachte, && sei nur eine rvalue-Referenz, wieso benötigt es dann einen Move?
Aus demselben Grund, warum...
string blah = "hello"; string&& dies = blah; // nicht funktioniert (compile-fehler), string&& jenes = move(blah); // aber schon.
Beachte, dass hier gar nix "movt". move() ist nur ein rvalue-cast. Das Ergebnis von move bezieht sich immer noch auf dasselbe Objekt. Der Ausdruck ist aber kein Lvalue mehr sondern ein Rvalue. Hier wird das benötigt, weil man Rvalue-Referenzen nicht mit Lvalues initialisieren kann (es sei denn, da ist eine implizite Konvertierung dazwischen, so dass sich die Rvalue-Referenz auf ein neues temporäres Objekt beziehen würde).
Eisflamme schrieb:
ist && mittlerweile auch ein Qualifizierer?
Ja. Damit kannst du eben auch für Methoden zwischen L- und Rvalue unterscheiden. Das ging ja vorher nicht. Entweder haben die Überladungen alle einen Ref-Qualifizierer, oder sie haben alle keinen. Mischen darf man das nicht. Und falls es Ref-Qualifizierer gibt, sind die Überladungsauflösungsregeln genauso wie als wenn die Funktionen einen zusätzichen Referenz-Parameter "self" hätten, der entsprechend qualifiziert ist.
-
Hi,
ich habe nochmal ein paar Artikel/Forensachen gelesen und dadurch jetzt etwas mehr verstanden. Aber leider noch nicht alles, irgendwie geht das mehr in die Tiefe, als ich dachte.
Also die Varianten auf S.1 dieses Threads nutzen alle implizit aus, dass der Compiler optimieren kann, weil er RValue-Referenzen ausnutzen kann? Ohne Optimierung wäre der Code ja erstmal nicht besonders toll, weil der Parametertyp std::string ja immer eine Kopie erzeugt. Stimmt das so weit?
Dann habe ich mir nochmal diesen Code angeschaut:
struct foo { std::string const& s() const & { return s_; } std::string && s() && { return std::move(s_); } private: std::string s_; };
Die Erklärung ist wohl in diesem Zitat:
krümelkacker schrieb:
- Damit kannst du eben auch für Methoden zwischen L- und Rvalue unterscheiden. Das ging ja vorher nicht. 2) Entweder haben die Überladungen alle einen Ref-Qualifizierer, oder sie haben alle keinen. Mischen darf man das nicht. 3) Und falls es Ref-Qualifizierer gibt, sind die Überladungsauflösungsregeln genauso wie als wenn die Funktionen einen zusätzichen Referenz-Parameter "self" hätten, der entsprechend qualifiziert ist.
Da muss ich aber nochmal nachhaken:
- Heißt L/R-Value hier bezogen auf die Klasseninstanz (also wie auch der const-Qualifizierer der Methode sich ja eigentlich auf die Instanz richtet bzw. dafür sorgt, dass bei einer const-Instanz die const-Methodenüberladung aufgerufen wird)?
- Okay, man darf nicht "Kein Ref-Qualifizierer" und "Ref-Qualifizierer" mischen, wohl aber LRef- und RRef-Qualifizierer?
- Hier komm ich gerade nicht mit, was sind denn Überladungsauflösungsregeln? Den self-Parameter verstehe ich, glaube ich, der übernähme eben den Methoden-Qualifizierer als Parameter-Qualifizierer, genau so wie die Methoden-Qualifizierer ja eigentlich auch die Qualifizierer für das Objekt sind, richtig? Vermutlich drücke ich das zu umständlich aus, ich hoffe, man versteht, was ich meine (und dass es dasselbe ist, was Du meinst).
Und jetzt springe ich nochmal auf die erste Antwort zurück:
std::string flip(std::string text) { std::reverse(begin(text),end(text)); return text; }
[...]
Das kannst du eigentlich nur noch mit der const&/&&-Überladung toppen, weil dadurch noch ein Move gespart werden kann.Okay, also ohne die Überladung mit RValue "moved" er den RValue in text, reverse moved alles brav hin und her und er moved das Ergebnis von text wieder in den Empfänger zurück.
Und mit der &&-Überladung (also Parameter = std::string&& text) würde er in text nichts moven, weil text nur eine Referenz wäre, reverse würde genau brav alles hin- und hermoven, aber bei der Rückgabe erfolgt wieder der move. Also sparen wir den move bei Parameterübergabe, stimmt das so weit? Sparen wir auch in der const&-Variante (also für einen LValue) etwas mit der Überladung?
Und kann man die const&/&&-Überladung mit einer Methode hinbekommen, also das mit Perfect-Forwarding lösen? Also so was wie:
std::string&& reverse(std::string&& text) { std::reverse(begin(std::forward(text)), end(std::forward(text))); return std::forward(text); }
oder so?
-
Eisflamme schrieb:
- Heißt L/R-Value hier bezogen auf die Klasseninstanz (also wie auch der const-Qualifizierer der Methode sich ja eigentlich auf die Instanz richtet bzw. dafür sorgt, dass bei einer const-Instanz die const-Methodenüberladung aufgerufen wird)?
Genau. Gemeint ist der implizite Objektparameter der nichtstatischen Memberfunktion. Also das, was vor dem Punkt steht (oder -> - dann ist es immer ein lvalue, weil p->f als (*p)->f interpretiert wird).
Eisflamme schrieb:
- Okay, man darf nicht "Kein Ref-Qualifizierer" und "Ref-Qualifizierer" mischen, wohl aber LRef- und RRef-Qualifizierer?
(Offensichtlich) richtig.
Eisflamme schrieb:
- Hier komm ich gerade nicht mit, was sind denn Überladungsauflösungsregeln? Den self-Parameter verstehe ich, glaube ich, der übernähme eben den Methoden-Qualifizierer als Parameter-Qualifizierer, genau so wie die Methoden-Qualifizierer ja eigentlich auch die Qualifizierer für das Objekt sind, richtig? Vermutlich drücke ich das zu umständlich aus, ich hoffe, man versteht, was ich meine (und dass es dasselbe ist, was Du meinst).
Überladungsauflösung ist der letzte Schritt bei der Bestimmung der aufzurufenden Funktion bei einem Funktionsaufruf:
1. Zusammensuchen aller auffindbaren Deklarationen (Namelookup)
2. ggf. Bestimmen von nicht explizit angegeben Templateargumenten (Templateargumentdeduktion), ggf. Entfernen unpassender Deklarationen (SFINAE)
3. Auswahl der besten Funktion aus dem Set aller verbleibenden Deklarationen
(4. Zugriffskontrolle)Es sind im Prinzip 3 Fälle, die zu untersuchen sind:
1. Argument ist lvalue
2. Argument ist rvalue und ein temporäres Objekt (prvalue)
3. Argument ist rvalue aber kein temporäres Objekt (xvalue)Für
std::string flip(std::string text) { std::reverse(begin(text),end(text)); return text; }
ergibt sich
1. Aufruf: copy return: move
2. Aufruf: move(RVO) return: move
3. Aufruf: move return: moveFür
std::string flip(std::string&& text) { std::reverse(begin(text),end(text)); return std::move(text); } std::string flip(const std::string& s) { std::string text = s; std::reverse(begin(text),end(text)); return text; }
ergibt sich
1. Aufruf: - in Funktion: copy return: move(RVO)
2. Aufruf: - return: move
3. Aufruf: - return: moveIm Falle eines Aufrufes mit einem lvalue, wird also ein move eingespart (was im Vergleich zum verbleibenden Copy völlig irrelevant sein dürfte)
Im Falle eines Aufrufes mit einem rvalue, das kein temporäres Objekt darstellt, wird so nur ein move statt zweien durchgeführt. Das kann u.U. relevenat sein, im Regelfall allerdings vermutlich nicht.
-
Hi,
vielen Dank für die ausführliche Antwort, das hat schon eine ganze Menge für mich geklärt.
Ich sehe gerade nur nicht, wo wir eine Kopie bei lvalue-Aufruf sparen:
1. Aufruf: copy return: move
vs.
1. Aufruf: - in Funktion: copy return: move(RVO)
Vielleicht verstehe ich die Syntax dieser Beschreibungen auch nicht ganz, aber bis auf das (RVO) und das "- in Funktion" (verstehe auch nicht ganz diesen Zusatz) sieht das doch gleich aus.
-
Eisflamme schrieb:
Hi,
vielen Dank für die ausführliche Antwort, das hat schon eine ganze Menge für mich geklärt.
Ich sehe gerade nur nicht, wo wir eine Kopie bei lvalue-Aufruf sparen:
1. Aufruf: copy return: move
vs.
1. Aufruf: - in Funktion: copy return: move(RVO)
Vielleicht verstehe ich die Syntax dieser Beschreibungen auch nicht ganz, aber bis auf das (RVO) und das "- in Funktion" (verstehe auch nicht ganz diesen Zusatz) sieht das doch gleich aus.
Aufruf: -
bedeutet, dass keine Funktion aufgerufen werden muss, um das Funktionasargument an den formalen (Referenz-)Parameter zu binden (den Fall, dass das Argument kein string ist, brauchen wir hier nicht separat zu behandeln, dort wird einfach eine Konvertierungssequenz vorgeschaltet und man macht dann mit Fall2 (prvalue) weiter).in Funktion: copy
bedeutet, dass in der Funktion ein Aufruf des Kopierkonstruktors erfolgt (um text zu initialisieren).(RVO) bedeutet jeweils, dass der Compiler dieses move(copy) eleminieren darf (im Falle der const&-Überladung wäre NRVO präziser), und für unsere Zwecke gehen wir nat. davon aus, dass dies auch geschieht.
P.S. bei rvalues meine ich nat. modifizierbare rvalues - bei const-Qualifikation des Arguments wäre entsprechend zu kopieren statt zu moven (an der ggf. vorhanden Möglichkeit der Eleminierung ändert sich nichts).