Rückgabeeffizienz/RVO
-
Man muss nicht mal Assembler benutzen, wenn man den MMX/SSE/3Dnow! Befehlssatz ausnutzen will. Dazu unterstützen die meisten Compiler an MMX/SSE/3Dnow! angelehnte "intrinsics".
Gruß,
SP
-
Danke für deine Antworten Sebastian Pizer.
Ein paar Sachen sind mir nicht klar:
Du schreibst in deinem Code: T const inv = sqrt(1/l2);
1. Wenn ich jetzt einen Vector3<int> anlege, gibt das doch einen Compiler-Error, oder?2. Du schreibst wenn es geht sollte man keinen CopyCtor und Dtor selber schreiben. Für mich hört sich das fast so an, als sei das eine notwendige Bedigung, damit RVO und NRVO greift. Ist das so? Wird NIE eine RVO/NRVO gemacht, wenn ich selber einen CopyCtor oder Dtor schreibe?
3. In meinem Normalize2 habe ich ja 2 returns und die geben unterschiedliche Objekte zurück. Ich dachte in so einem Fall ist NRVO (und RVO) unmöglich? Es klang bei dir so, als sollte es dennoch gehen?
4. Ich habe Normalize2 etwas umgeschrieben, so dass es nur noch ein return hat:
template <class T> Vector3<T> Vec3Normalize2(const Vector3<T>& v) { float length = sqrtf(v.x*v.x + v.y*v.y + v.z*v.z); Vector3<T> ret(Vector3<T>::ZERO); if(length > 0) { float invLength = 1.0f / length; ret = v * invLength; } return ret; } // Aufruf A: Vec3f v(2,3,4); Vec3f v2 = Vec3Normalize2(v); // Aufruf B: Vec3f v(2,3,4); Vec3f v2; v2 = Vec3Normalize2(v);Wenn ich die Funktion wie in "Aufruf A" aufrufe, dann ist die Ausgabe:
Ctor(x,y,z)
CopyCtor()
Ctor(x,y,z)
Dtor()NRVO scheint also zu greifen. Rufe ich den Code hingegen wie in "Aurfuf B" auf, so ergibt die Ausgabe:
Ctor(x,y,z)
Ctor()
CopyCtor()
Ctor(x,y,z)
Dtor()
Dtor()Ich finde das ist eine stattliche Anzahl von Aufrufen, nur für das Normalisieren eines Vektors!
Die Frage deshalb: Sollte ich nicht besser alle Rückgaben als Referenz im 1. Parameter zurückgeben? Also lieber das: "void MatrixInvert(m, m2)" anstatt von "m=MatrixInvert(m2)"?
-
1. ausprobieren
2. nö, ist keine bedingung
3. also der (2008er) msvc kann das auf jeden fall - hab ma nen bsp gemacht, siehe ende.point hatte ich gerade zur hand, weils in nem anderen thread darum ging ^^
__declspec(noinline)ist msvc spezifisch, weil ich nicht wollte, dass er die fkt inlined - dann wär es ja klar, dass copy ctor etc nich aufgerufen werdenbsp.:
#include <algorithm> #include <iostream> struct point { float x, y; explicit point(float _x = 0, float _y = 0) : x(_x), y(_y) { std::cout << "CTor" << std::endl; } point(const point &other) : x(other.x), y(other.y) { std::cout << "CopyCTor" << std::endl; } void swap(point &other) { using std::swap; swap(x, other.x); swap(y, other.y); } point& operator= (const point &other) { std::cout << "op=" << std::endl; point tmp(other); swap(tmp); return *this; } ~point() { std::cout << "DTor" << std::endl; } }; std::ostream& operator<< (std::ostream &s, const point &these) { return s << these.x << ' ' << these.y; } point __declspec(noinline) test(float a, float b) { if(b == 0) return point(a, a); if(a < 0) return point(a, b); else return point(b, a); } int main() { float a, b; std::cin >> a >> b; std::cout << test(a, b) << std::endl; point x; std::cout << (x = test(b, a)) << std::endl; }MSVC schrieb:
2
3
CTor
3 2
DTor
CTor
CTor
op=
CopyCTor
DTor
2 3
DTor->
// Eingaben // erste Variante: CTor //ausgabe DTor //zweite Variante: CTor //CTor mit (0,0) CTor //das eigentliche objekt, welches dem alten zugewiesen wird op= //zuweisung CopyCTor //tmp wird erstellt //geswapt DTor //tmp wird zerstört //ausgabe DTorPS:
wieso braucht man die extra klammern hier eigtl:
std::cout << (x = test(b, a)) << std::endl;
-
tomate~ schrieb:
Du schreibst in deinem Code: T const inv = sqrt(1/l2);
1. Wenn ich jetzt einen Vector3<int> anlege, gibt das doch einen Compiler-Error, oder?Möglich. Was soll denn das Ergebnis von normalize(Vector3<int>(1,2,3)) sein? :p
tomate~ schrieb:
2. Du schreibst wenn es geht sollte man keinen CopyCtor und Dtor selber schreiben. Für mich hört sich das fast so an, als sei das eine notwendige Bedigung, damit RVO und NRVO greift. Ist das so?
Nein. Das hat nichts damit zu tun. Bei (N)RVO oder noch allgemeiner "copy elision" geht es um das Reduzieren von unnötigen Kopien. Ob man beim Kopieren einen benutzerdefinierten Konstruktor aufrufen muss oder nicht, ist egal. "copy elision" ist gerade dann interessant, wenn die Objekte nicht-triviale Copy C'tors und D'tors haben.
tomate~ schrieb:
3. In meinem Normalize2 habe ich ja 2 returns und die geben unterschiedliche Objekte zurück. Ich dachte in so einem Fall ist NRVO (und RVO) unmöglich? Es klang bei dir so, als sollte es dennoch gehen?
Bei NRVO spielt das eine Rolle, ja. Stell Dir vor, der Compiler übergibt einfach einen Zeiger als zusätzlichen Parameter, der auf die Stelle zeigt, wo das Funktionsergebnis konstruiert werden soll. Du hast keine return-Anweisung benutzt, die eine lokale Variable benennt. Im ersten Fall wird ein temporäres Objekt erzeugt. Das kann direkt dort erzeugt werden, wo der Zeiger hinzeigt (RVO). Im zweiten Fall benennst Du ein lvalue-Objekt. Das ist kein besonders interessanter Fall: Das Ergebnisobjekt wird einfach kopiert.
tomate~ schrieb:
4. Ich habe Normalize2 etwas umgeschrieben, so dass es nur noch ein return hat:
template <class T> Vector3<T> Vec3Normalize2(const Vector3<T>& v) { float length = sqrtf(v.x*v.x + v.y*v.y + v.z*v.z); Vector3<T> ret(Vector3<T>::ZERO); if(length > 0) { float invLength = 1.0f / length; ret = v * invLength; } return ret; } // Aufruf A: Vec3f v(2,3,4); Vec3f v2 = Vec3Normalize2(v); // Aufruf B: Vec3f v(2,3,4); Vec3f v2; v2 = Vec3Normalize2(v);Wenn ich die Funktion wie in "Aufruf A" aufrufe, dann ist die Ausgabe:
Ctor(x,y,z) CopyCtor() Ctor(x,y,z) Dtor()NRVO scheint also zu greifen.
Wieso? Das "CopyCtor()" kommt sicherlich von Deiner Initialisierung von "ret" mit Vector3<T>::ZERO. Außerdem, was macht denn eigentlich "ret = v * invLength"? Überleg mal genau nach. Ich habe bisher keine Definition von operator* und/oder operator= gesehen, kann das also nicht beantworten. Es sieht aber verdächtig aus. Wenn ich raten müsste:
template<typename T> Vector3<T> operator*(Vector3<T> const& v, float f) { return Vector3<T>(v.x*f, v.y*f, v.z*f); }"v * invLength" liefert also ein temporäres Objekt, welches per operator= dem Objekt ret zugewiesen wird (copy assignment).
Rufe ich den Code hingegen wie in "Aurfuf B" auf, so ergibt die Ausgabe:
Ctor(x,y,z) Ctor() CopyCtor() Ctor(x,y,z) Dtor() Dtor()Ich finde das ist eine stattliche Anzahl von Aufrufen, nur für das Normalisieren eines Vektors!
Da bist Du selbst dran Schuld. Sehr wahrscheinlich sieht das so aus:
Ctor(x,y,z) // Konstruktion von v ----------------------------------------------------- Ctor() // Konstruktion von v2 ----------------------------------------------------- CopyCtor() // Copy-Konstruktion von "ret(ZERO)" ----------------------------------------------------- Ctor(x,y,z) // Konstruktion von v*invLength ("tmp1") // (RVO wurde angewendet) ----------------------------------------------------- // copy assignment: ret = tmp1 ----------------------------------------------------- Dtor() // tmp1 wird zerstört ----------------------------------------------------- // Das Ergebnis von Vec3Normalize2(v) ist // ein neues temporäres Objekt ("tmp2"). // Hier wurde NRVO angewendet, so dass // tmp2 DASSELBE Objekt wie ret ist. ----------------------------------------------------- // copy assignment: v2 = tmp2; ----------------------------------------------------- Dtor() // tmp2 wird zerstört ----------------------------------------------------- Dtor() // v2 wird zerstört Dtor() // v wird zerstörtCode so wie der hier
Vec3f v2; v2 = Vec3Normalize2(v);ist "bah pfui!"
weil Du hier eine Default-Initialisierung gefolgt von einem Copy-Assignment hast wobeiVec3f v2 = Vec3Normalize2(v);eine Copy-Initialisierung ist, bei der "copy elision" angewendet werden kann.
Die Frage deshalb: Sollte ich nicht besser alle Rückgaben als Referenz im 1. Parameter zurückgeben? Also lieber das: "void MatrixInvert(m, m2)" anstatt von "m=MatrixInvert(m2)"?
Für so ein "Popel-Objekt", welches -- wenn Du Deine ganzen cout<<"blah"-Methoden wieder rausschmeißt -- trivial kopierbar ist sofern T trivial kopierbar ist, schadet das nur. Du gewinnst rein gar nichts damit. Du bekommst nur unleserlichen Code dadurch.
Bei einem "fetten" Matrix-Objekt, welches seine Elemente zB intern in einem std::vector<double> verwaltet ist das etwas anderes -- aber auch nicht mehr lange, dank C++0x und Rvalue-Referenzen.
Es bleibt zu hoffen, dass die Leute mit Einzug von C++0x einen etwas "funktionaleren" Stil an den Tag legen. Wie Howard Hinnant so schön (sinngemäß) sagte: Es gibt jetzt keinen guten Grund mehr, Referenzen für Ergebnisse zu übergeben.
Gruß,
SP
-
unskilled schrieb:
PS:
wieso braucht man die extra klammern hier eigtl:
std::cout << (x = test(b, a)) << std::endl;Ich würde mal vermuten weil er sonst zunächst cout << x auswertet und dann dem zurückgegebenen stream einen point zuweisen will, was natürlich nicht geht.
Ok, das bedeutet ich kann dutzende returns haben und jedes return gibt ein anderes Objekt zurück und RVO/NRVO wird dennoch klappen? Ist das garantiert beim MSVC 2008?
Meine Frage:
Ich finde das ist eine stattliche Anzahl von Aufrufen, nur für das Normalisieren eines Vektors!
Die Frage deshalb: Sollte ich nicht besser alle Rückgaben als Referenz im 1. Parameter zurückgeben? Also lieber das: "void MatrixInvert(m, m2)" anstatt von "m=MatrixInvert(m2)"?steht übrigens noch im Raum;)
-
Ich finde das ist eine stattliche Anzahl von Aufrufen, nur für das Normalisieren eines Vektors!
Die Frage steht gar nicht mehr offen - weniger gehen ja nun gar nicht mehr...
@RVO/NRVO:
imho kann kein compiler garantieren, dass das immer klappt...bb
PS:
Ich würde mal vermuten weil er sonst zunächst cout << x auswertet und dann dem zurückgegebenen stream einen point zuweisen will, was natürlich nicht geht.
Nö, so ists nicht... Sonst hätte die Fehlermeldung anders gelautet. Damit ich sie dir nicht vorenthalte:
MSVC schrieb:
1>.\main.cpp(58) : error C2678: binary '<<' : no operator found which takes a left-hand operand of type 'point' (or there is no acceptable conversion)
1> .\main.cpp(37): could be 'std::ostream &operator <<(std::ostream &,const point &)'
1> while trying to match the argument list '(point, overloaded-function)'
-
tomate~ schrieb:
Meine Frage: [...] steht übrigens noch im Raum;)
Ich hoffe, Du hast meine Antwort nicht übersehen.

-
Sebastian Pizer schrieb:
tomate~ schrieb:
Meine Frage: [...] steht übrigens noch im Raum;)
Ich hoffe, Du hast meine Antwort nicht übersehen.

Hehe, nein.
Aalso. Ein paar Dinge finde ich seltsam die du schreibst. In meinem Beispiel "Aufruf A" schreibe ich, dass hier NVRO offenbar angewandt wird, was du mit "Wieso?" in Frage stellst. Aber im weiteren Text bestätigst du dann doch, dass es NVRO ist.
Mein operator* sieht genau so aus, wie du es dir gedacht hast. Ergo kann man diese Ausgabe:Vec3f v(2,3,4); Vec3f v2 = Vec3Normalize2(v); Ctor(x,y,z) // v wird erzeugt CopyCtor() // Vector3<T> ret(Vector3<T>::ZERO); Ctor(x,y,z) // das ist das Objekt, das in operator* erzeugt wird Dtor() // nach operator= wird das temp. Objekt erzeugt.genau so erklären wie du es beschrieben hast (siehe Kommentare)
Aber es gibt eben KEINEN weiteren CopyCtor und Dtor, was ja darauf hinweist, DASS NVRO angewandt wurde. Richtig?Ich habe jetzt mal deinen Ansatz genommen, sprich:
template <class T> Vector3<T> Vec3Normalize(const Vector3<T>& v) { Vector3<T> ret(v); ret.normalize(); return ret; } // Aufruf: Vec3f v(2,3,4); Vec3f v2 = Vec3Normalize(v); // Ausgabe: Ctor(x,y,z) CopyCtor()Das ist jetzt so ziemlich das Optimum, oder?
Was mich als "Anfänger" einfach verunsichert: Überall höre ich, dass der Compiler (N)RVO benutzen KANN (und auch nur dann, wenn ich passenden Code schreibe). Ich will aber nicht ständig für den Compiler programmieren, so dass er (N)RVO betreiben kann. Außerdem will ich auch nicht vor Zeilen a la
Matrix m; ... m = MatrixRotY(45);Angst haben müssen.
Ich weiß auch nicht wie böse so eine Zeile ist (Meine Matrix hat 16 floats).
Würdet ihr bei einer Matrix mit 16 floats ebenfalls die Ergebnisse per value zurückgeben?
-
tomate~ schrieb:
Aalso. Ein paar Dinge finde ich seltsam die du schreibst. In meinem Beispiel "Aufruf A" schreibe ich, dass hier NVRO offenbar angewandt wird, was du mit "Wieso?" in Frage stellst.
Entschuldige. Ich hatte "offenbar nicht angewandt" gelesen.
tomate~ schrieb:
...
// Ausgabe: Ctor(x,y,z) CopyCtor()Das ist jetzt so ziemlich das Optimum, oder?
Ja, besser geht's nicht, ne?

Vorteil: Du kannst mit ".normalize()" einen Vektor "in-place" normalisieren. 2 Funktionen sind hier also recht praktisch. Manchmal hat man schon einen Vektor, den man in-place normalisieren will und manchmal will man eine normalisierte Kopie haben. Allerdings sollte die Elementfunktion ".normalize" gut dokumentiert sein. Verändert sie Objekt und liefert eine Referenz auf sich oder erzeugt sie eine normalisierte Kopie und verändert das Objekt nicht? Deswegen hatte ich einen Namen gewählt, der das verdeutlichen sollte ("normalize_equals" in Anlehnung an die Operatoren +=, -=, *=, /=, ...). operator+ definiere ich auch immer gerne mittels operator+= ...inline foo operator+(foo const& a, foo const& b) { foo r (a); r += b; return r; // setzt auf NRVO }tomate~ schrieb:
Was mich als "Anfänger" einfach verunsichert: Überall höre ich, dass der Compiler (N)RVO benutzen KANN (und auch nur dann, wenn ich passenden Code schreibe). Ich will aber nicht ständig für den Compiler programmieren, so dass er (N)RVO betreiben kann. Außerdem will ich auch nicht vor Zeilen a la
Matrix m; ... m = MatrixRotY(45);Angst haben müssen.
Ich weiß auch nicht wie böse so eine Zeile ist (Meine Matrix hat 16 floats).
Würdet ihr bei einer Matrix mit 16 floats ebenfalls die Ergebnisse per value zurückgeben?Die Sorgen und Einwände sind berechtigt. Ich verlasse mich auch nicht 100%ig darauf, dass (N)RVO durchgeführt wird. In den Fällen, wo es einen richtig großen Unterschied macht, setzte ich NICHT darauf. Ich erwarte es aber trotzdem von einem guten Compiler.
Gruß,
SP
-
Schon mal vielen Dank für deine Antworten. Sie waren sehr hilfreich!

Das Problem ist halt nur, dass wenn ich jetzt "kleine" Objekte (dazu zähle ich einen Vector3) per Value zurückgebe, aber eine Matrix by reference per Parameter, dann ist das inkonsistent und damit auch unschön:
Vec3f v(2,3,4); Vec3f v2 = Vec3Normalize(v); // Rückgabe per value Matrix4 m ; Mat4RotationY(m, 90); // Rückgabe per parameterJetzt muss der Anwender (und ich) meines Codes immer genau hinschauen, wie jede Funktion funktioniert. Würde ich hingegen IMMER non-built-in Typen im 1. Parameter per Reference zurückgeben, wäre es vllt syntaktisch nicht sonderlich schön, aber einheitlich:
Vec3f v(2,3,4); Vec3f v2; Vec3Normalize(v2, v); Matrix4 m; Mat4RotationY(m, 90);Ich denke du weißt was ich meine. Ich kann mich einfach nicht entscheiden, wie ich es machen soll

-
Du könntest es so machen:
struct matrix4f { float elements[4][4]; }; void make_rotation(vec3f axis, float angle, matrix4f & out); inline matrix4f make_rotation(vec3f axis, float angle) { matrix4f m; make_rotation(axis,angle,m); return m; // setzt auf NRVO }Wenn NRVO hier nicht angewendet wird, ist das kein Beinbruch. Man hat dann immer noch die Wahl, statt
matrix4f m = make_rotation(......);einfach
matrix4f m; make_rotation(......,m);zu schreiben. Das würde ich aber erst dann machen, wenn ich festgestellt habe, dass diese Änderung auch etwas bringt.
Wenn Du schon ein Matrix-Objekt hast, dann solltest Du
make_rotation(......,m);statt
m = make_rotation(......);schreiben, weil Du damit -- unabhängig von (N)RVO -- eine Kopie sparst.
Gruß,
SP