Wird NRVO durch "const" verhindert?



  • hustbaer schrieb:

    RVO/NRVO schrieb:

    Shlo schrieb:

    Der Standard kennt sowas wie RVO oder NRVO nicht. Ob und welche Optimierungen durchgeführt werden, hängt von deinem Compiler ab.

    Was ist der Unterschied zwischen RVO und NRVO?

    MyType RVO()
    {
        return MyType("Salatbar");
    }
    
    MyType NRVO()
    {
        MyType dasKindHatEinenNamen("Buffet");
        return dasKindHatEinenNamen;
    }
    

    thx

    Aber was für einen Unterschied macht es denn optimierungstechnisch, ob
    eine Variable einen Namen hat oder nicht?



  • Wie kommst du eigentlich auf die Frage, ob es verhindert wird?
    Hast du einen Anhaltspunkt dafür?



  • Wie ich bereits erklärt habe, sind das Optimierungen die im Standard explizit erlaubt sind, auch wenn sich dadurch das beobachtbare Verhalten eines Programms ändert.

    Im ersen Fall ist das was man dem "return" Statement füttert eine rvalue. Im zweiten Fall eine lvalue.
    Das ist schon ein wichtiger Unterschied.

    Im Endeffekt erlaubt der Standard beides, aber er könnte sich genau so darauf beschränken das eine zu erlauben und das andere nicht.

    RVO/NRVO schrieb:

    Wie kommst du eigentlich auf die Frage, ob es verhindert wird?
    Hast du einen Anhaltspunkt dafür?

    Natürlich hab' ich einen Anhaltspunkt dafür.
    Die lokale Variable ist "const". Der Returnwert ist nicht const.
    Das beisst sich irgendwo.

    Nachdem ein Objekt keine mir bekannte Möglichkeit hat rauszubekommen ob es "const" instanziert wurde, sollte es IMO egal sein, wenn man es einfach "umwidmet". Von daher könnte es erlaubt sein (und sollte IMO auch, wenn ich mich nicht irre bezüglich kein anderes Verhalten mit oder ohne const möglich).

    Nur so 100% sicher bin ich mir da nicht. Ist auch leicht möglich dass der Standard dazu nichts sagt, und (wie so oft) Formulierungen verwendet die man - mehr oder weniger eindeutig - auslegen kann.



  • So. Es macht mit MSVC 9 (2008) keinen Unterschied.

    Für alle die es mit nem anderen Compiler ausprobieren möchten:

    #include <iostream>
    #include <ctime>
    
    class Ding
    {
    public:
    	explicit Ding()
    		: m_i(0)
    	{
    		std::cout << "    Ding::Ding()  [" << m_i << "]\n";
    	}
    
    	explicit Ding(char const*)
    		: m_i(0)
    	{
    		std::cout << "    Ding::Ding(char const*)  [" << m_i << "]\n";
    	}
    
    	Ding(Ding const& other)
    		: m_i(other.m_i)
    	{
    		std::cout << "    Ding::Ding(Ding const&)  [" << m_i << "]\n";
    	}
    
    	~Ding()
    	{
    		std::cout << "    Ding::~Ding()  [" << m_i << "]\n";
    	}
    
    	Ding& operator = (Ding const& other)
    	{
    		m_i = other.m_i;
    		std::cout << "    Ding::operator = (Ding const&)  [" << m_i << "]\n";
    		return *this;
    	}
    
    	void Mutator()
    	{
    		std::cout << "    Ding::Mutator()  [" << m_i << "]";
    		m_i++;
    		std::cout << " -> [" << m_i << "]\n";
    	}
    
    	Ding& GetMe()
    	{
    		return *this;
    	}
    
    	int m_i;
    };
    
    Ding RVO()
    {
    	return Ding("dong");
    }
    
    Ding NRVO()
    {
    	Ding ding("dong");
    	return ding;
    }
    
    Ding NCRVO()
    {
    	Ding const ding("dong");
    	return ding;
    }
    
    Ding Nested4()
    {
    	Ding ding = NCRVO();
    	ding.Mutator();
    	return ding;
    }
    
    Ding Nested3()
    {
    	Ding ding = Nested4();
    	ding.Mutator();
    	return ding;
    }
    
    Ding Nested2()
    {
    	Ding ding = Nested3();
    	ding.Mutator();
    	return ding;
    }
    
    Ding Nested()
    {
    	Ding ding = Nested2();
    	ding.Mutator();
    	return ding;
    }
    
    void CallMutator(Ding& d)
    {
    	 d.Mutator();
    }
    
    int main()
    {
    	std::cout << "\n----------------------------------------------\n";
    	std::cout << "discard\n\n";
    
    	std::cout << "RVO();\n";
    	{RVO();}
    	std::cout << "\n";
    
    	std::cout << "NRVO();\n";
    	{NRVO();}
    	std::cout << "\n";
    
    	std::cout << "NCRVO();\n";
    	{NCRVO();}
    	std::cout << "\n";
    
    	std::cout << "\n----------------------------------------------\n";
    	std::cout << "use\n\n";
    
    	std::cout << "RVO(.Mutator());\n";
    	{RVO().Mutator();}
    	std::cout << "\n";
    
    	std::cout << "NRVO().Mutator();\n";
    	{NRVO().Mutator();}
    	std::cout << "\n";
    
    	std::cout << "NCRVO().Mutator();\n";
    	{NCRVO().Mutator();}
    	std::cout << "\n";
    
    	std::cout << "\n----------------------------------------------\n";
    	std::cout << "copy\n\n";
    
    	std::cout << "Ding d(RVO()); d.Mutator();\n";
    	{Ding d(RVO()); d.Mutator();}
    	std::cout << "\n";
    
    	std::cout << "Ding d(NRVO()); d.Mutator();\n";
    	{Ding d(NRVO()); d.Mutator();}
    	std::cout << "\n";
    
    	std::cout << "Ding d(NCRVO()); d.Mutator();\n";
    	{Ding d(NCRVO()); d.Mutator();}
    	std::cout << "\n";
    
    	std::cout << "\n----------------------------------------------\n";
    	std::cout << "copy to const\n\n";
    
    	std::cout << "Ding const d(RVO());\n";
    	{Ding const d(RVO());}
    	std::cout << "\n";
    
    	std::cout << "Ding const d(NRVO());\n";
    	{Ding const d(NRVO());}
    	std::cout << "\n";
    
    	std::cout << "Ding const d(NCRVO());\n";
    	{Ding const d(NCRVO());}
    	std::cout << "\n";
    
    	std::cout << "\n----------------------------------------------\n";
    	std::cout << "assign\n\n";
    
    	std::cout << "Ding d; d = RVO(); d.Mutator();\n";
    	{Ding d; d = RVO(); d.Mutator();}
    	std::cout << "\n";
    
    	std::cout << "Ding d; d = NRVO(); d.Mutator();\n";
    	{Ding d; d = NRVO(); d.Mutator();}
    	std::cout << "\n";
    
    	std::cout << "Ding d; d = NCRVO(); d.Mutator();\n";
    	{Ding d; d = NCRVO(); d.Mutator();}
    	std::cout << "\n";
    
    	std::cout << "\n----------------------------------------------\n";
    	std::cout << "forward\n\n";
    
    	std::cout << "CallMutator(RVO().GetMe());\n";
    	{CallMutator(RVO().GetMe());}
    	std::cout << "\n";
    
    	std::cout << "CallMutator(NRVO().GetMe());\n";
    	{CallMutator(NRVO().GetMe());}
    	std::cout << "\n";
    
    	std::cout << "CallMutator(NCRVO().GetMe());\n";
    	{CallMutator(NCRVO().GetMe());}
    	std::cout << "\n";
    
    	std::cout << "\n----------------------------------------------\n";
    	std::cout << "nested\n\n";
    
    	std::cout << "CallMutator(Nested().GetMe());\n";
    	{CallMutator(Nested().GetMe());}
    	std::cout << "\n";
    
    	return 0;
    }
    

    MSVC im Release-Mode:

    ----------------------------------------------
    discard
    
    RVO();
        Ding::Ding(char const*)  [0]
        Ding::~Ding()  [0]
    
    NRVO();
        Ding::Ding(char const*)  [0]
        Ding::~Ding()  [0]
    
    NCRVO();
        Ding::Ding(char const*)  [0]
        Ding::~Ding()  [0]
    
    ----------------------------------------------
    use
    
    RVO(.Mutator());
        Ding::Ding(char const*)  [0]
        Ding::Mutator()  [0] -> [1]
        Ding::~Ding()  [1]
    
    NRVO().Mutator();
        Ding::Ding(char const*)  [0]
        Ding::Mutator()  [0] -> [1]
        Ding::~Ding()  [1]
    
    NCRVO().Mutator();
        Ding::Ding(char const*)  [0]
        Ding::Mutator()  [0] -> [1]
        Ding::~Ding()  [1]
    
    ----------------------------------------------
    copy
    
    Ding d(RVO()); d.Mutator();
        Ding::Ding(char const*)  [0]
        Ding::Mutator()  [0] -> [1]
        Ding::~Ding()  [1]
    
    Ding d(NRVO()); d.Mutator();
        Ding::Ding(char const*)  [0]
        Ding::Mutator()  [0] -> [1]
        Ding::~Ding()  [1]
    
    Ding d(NCRVO()); d.Mutator();
        Ding::Ding(char const*)  [0]
        Ding::Mutator()  [0] -> [1]
        Ding::~Ding()  [1]
    
    ----------------------------------------------
    copy to const
    
    Ding const d(RVO());
        Ding::Ding(char const*)  [0]
        Ding::~Ding()  [0]
    
    Ding const d(NRVO());
        Ding::Ding(char const*)  [0]
        Ding::~Ding()  [0]
    
    Ding const d(NCRVO());
        Ding::Ding(char const*)  [0]
        Ding::~Ding()  [0]
    
    ----------------------------------------------
    assign
    
    Ding d; d = RVO(); d.Mutator();
        Ding::Ding()  [0]
        Ding::Ding(char const*)  [0]
        Ding::operator = (Ding const&)  [0]
        Ding::~Ding()  [0]
        Ding::Mutator()  [0] -> [1]
        Ding::~Ding()  [1]
    
    Ding d; d = NRVO(); d.Mutator();
        Ding::Ding()  [0]
        Ding::Ding(char const*)  [0]
        Ding::operator = (Ding const&)  [0]
        Ding::~Ding()  [0]
        Ding::Mutator()  [0] -> [1]
        Ding::~Ding()  [1]
    
    Ding d; d = NCRVO(); d.Mutator();
        Ding::Ding()  [0]
        Ding::Ding(char const*)  [0]
        Ding::operator = (Ding const&)  [0]
        Ding::~Ding()  [0]
        Ding::Mutator()  [0] -> [1]
        Ding::~Ding()  [1]
    
    ----------------------------------------------
    forward
    
    CallMutator(RVO().GetMe());
        Ding::Ding(char const*)  [0]
        Ding::Mutator()  [0] -> [1]
        Ding::~Ding()  [1]
    
    CallMutator(NRVO().GetMe());
        Ding::Ding(char const*)  [0]
        Ding::Mutator()  [0] -> [1]
        Ding::~Ding()  [1]
    
    CallMutator(NCRVO().GetMe());
        Ding::Ding(char const*)  [0]
        Ding::Mutator()  [0] -> [1]
        Ding::~Ding()  [1]
    
    ----------------------------------------------
    nested
    
    CallMutator(Nested().GetMe());
        Ding::Ding(char const*)  [0]
        Ding::Mutator()  [0] -> [1]
        Ding::Mutator()  [1] -> [2]
        Ding::Mutator()  [2] -> [3]
        Ding::Mutator()  [3] -> [4]
        Ding::Mutator()  [4] -> [5]
        Ding::~Ding()  [5]
    


  • Danke für die ausführliche Auflistung deiner Ergebnisse!!!
    👍



  • Es ist zwar schon eine Weile her, wo ich das nachgeguckt hatte, aber ich hatte die gleichen Bedenken. Ich hatte gedacht, dass ein const wie hier

    const ClassType source();
    
    ...
      ClassType dings = source();
    ...
    

    die erlaubten "copy elisions" vermeiden könnte. Dem ist aber nicht so. Nachzulesen in Abschnitt 12.8 des C++ Standards, wenn ich mich richtig erinnere. cv-Qualifikation spielt hier gar keine Rolle.



  • hustbaer schrieb:

    Natürlich kennt der Standard die RVO und NRVO, nur heissen sie dort vielleicht nicht so.

    Würde er sie nicht kennen, könnte der Compiler keinerlei Kopien eliminieren, wenn der betroffene Ctor oder Dtor beobachtbare Effekte hat.
    Solche Ctor/Dtor sind keine Seltenheit (durch Logging, sonstige IO, Memory-Allocations etc.), und sie werden trotzdem brav wegoptimiert.

    Also. Wenn man keine Ahnung hat...

    p.S.: im Standard wird das soweit ich weiss als "copy elision" bezeichnet.

    Hallo Nasenbär,

    da habe die copy elision tatsächlich übersehen. Danke für deinen netten Hinweis.



  • Schon mal versucht, ob das MSVC auch bei komplizierteren Funktionen optimiert, die nicht einfach ge-inlined werden können?



  • Nö, noch nicht versucht.
    Müsste ich mal ne DLL machen oder sowas, oder mit __declspec(noinline).

    Ich würde schätzen dass es trotzdem optimiert wird, da die Optimierung eigentlich nur vom Inhalt der Funktion abhängig ist, nicht davon wie sie aufgerufen wird.

    Das zurückgeben von Klassentypen ist sowieso meist so implementiert, dass der Funktion ein Zeiger auf einen Speicherbereich mitgegeben wird, in den sie das Objekt reinkonstruieren soll. Zumindest MSVC macht es so wenn ich mich richtig erinnere.

    Und damit hast du RVO und NRVO quasi "von selbst" - der Compiler muss einfach nur darauf verzichten das Objekt nochmal zu kopieren, wenn feststeht welches Objekt zurückgegeben wird.

    Und vor allem: der Aufrufer der Funktion muss nichts davon wissen. Der ruft einfach nur die Funktion auf, gibt nen Zeiger mit, und die Funktion konstruiert da dann *irgendwie* das Objekt rein. Ob sie das mit (N)RVO macht ist dem Aufrufer egal.

    Bei Fällen wo nicht klar ist, welches Objekt mal zu Returnwert wird, geht dann meist auch kein NRVO mehr. Beispiel:

    MyType Foo()
    {
        MyType mt1("foo");
        MyType mt2("bar");
        std::cout << &mt1 << &mt2;
        if (rand() % 2)
            return mt1;
        else
            return mt2;
    }
    

    Das sollte NRVO unmöglich machen, zumindest mit allen Compilern die "Class-Type-Returns" so implementieren wie beschrieben. Und natürlich wenn die Funktion nicht inline erweitert werden kann.



  • Das zurückgeben von Klassentypen ist sowieso meist so implementiert, dass der Funktion ein Zeiger auf einen Speicherbereich mitgegeben wird, in den sie das Objekt reinkonstruieren soll. Zumindest MSVC macht es so wenn ich mich richtig erinnere.

    Und damit hast du RVO und NRVO quasi "von selbst" - der Compiler muss einfach nur darauf verzichten das Objekt nochmal zu kopieren, wenn feststeht welches Objekt zurückgegeben wird.

    Vielleicht eine blöde Frage, aber wenn dem schon so ist, wozu brauchen wir dann noch "move semantics", wie sie in c++0x kommen sollen?



  • PhilippM schrieb:

    Vielleicht eine blöde Frage, aber wenn dem schon so ist, wozu brauchen wir dann noch "move semantics", wie sie in c++0x kommen sollen?

    Z.B. wenn man komplexe Objekte im Container hält, die können beim vergrößern einfach umgemoved werden anstatt alle zu kopieren und dann die Originale zu vernichten.



  • PhilippM schrieb:

    Vielleicht eine blöde Frage, aber wenn dem schon so ist, wozu brauchen wir dann noch "move semantics", wie sie in c++0x kommen sollen?

    Damit so ein Verhalten standardisiert werden kann und nicht von etlichen compilerspezifischen Faktoren abhängt.

    Aber RValue-Referenzen können einiges mehr als nur RVO/NRVO ersetzen, z.B. nichtkopierbare Objekte in STL-Containern speichern.



  • @PhilippM:
    Move-Semantik deckt wie schon geschrieben wurde viel mehr ab als RVO/NRVO überhaupt theoretisch abdecken kann.
    Mit Move-Semantik kann man z.B. Typen "movable" machen die ohne "noncopyable" sein müssen.
    Oder Member aus einem Objekt "raus-verschieben" (z.B. das Ergebnis einer Future).
    Oder generell die Resourcen von Rvalues "klauen", wenn man sie grad gut brauchen kann.

    Beispielsweise kann man einen Rvalue Overload für den operator + von std::string machen. Angenommen man nimmt lhs und rhs als Rvalue-Referenz, dann kann operator + nachgucken ob einer der beiden Strings ausreichend Platz für das Ergebnis hat, und dem dann einfach den Speicher klauen, was 1x new + 1x delete spart.

    Oder Rvalue Overloads für Funktionen wie boost::xxx_pointer_cast für shared_ptr... wenn der Operand eine Rvalue ist, dann kann man sich das (teure) "AddRef" auf den Shared-Count sparen, indem man dem Operanden einfach die "Referenz" Klaut.

    Oder an so Stellen:

    shared_ptr<Base> MyFactory()
    {
        shared_ptr<Derived> thing(new Derived());
        thing->PostConstructor();
        return thing;
    }
    

    Hier ist NRVO nicht möglich, da shared_ptr<Derived> und shared_ptr<Base> nicht der selbe Typ sind. Also muss der shared_ptr kopiert werden, was unnötig teuer ist. Natürlich kann man es umschreiben:

    shared_ptr<Base> MyFactory()
    {
        shared_ptr<Base> thing(new Derived());
        static_cast<Derived*>(thing)->PostConstructor(); // und wenn Derived virtual inheritance verwendet sind wir sowieso angeschissen
        return thing;
    }
    

    Beides hässlich.
    Mit Move-Semantik reicht es statt "return thing;" einfach "return move(thing);" zu schreiben. Was ich als 100x schöner empfinde.

    Kurz: viele viele bunte Smarties 🙂

    Und als netter Nebeneffekt wird auch Code schneller, wo die RVO/NRVO aus irgend einem Grund theoretisch möglich wäre, aber der Compiler nicht schlau genug ist.
    (Zumindest wenn der Returntyp "movable" ist)



  • PhilippM schrieb:

    Vielleicht eine blöde Frage, aber wenn dem schon so ist, wozu brauchen wir dann noch "move semantics", wie sie in c++0x kommen sollen?

    Copy elisions sind optional. Ein Compiler darf diese durchführen, muss aber nicht. Der neue C++ Standard wird Compiler dazu zwingen, dass, falls sie in einem Fall trotz Erlaubnis keine copy elision durchführen (aus was für Gründen auch immer), statt eines Copy-Ctors einen Move-Ctor aufzurufen (falls vorhanden). Ist im Moment (C++03) noch die Performanz von

    vector<int> quelle() {
      vector<int> result;
      ...
      return result;
    }
    

    abhängig von der Qualität des Compilers (NRVO ist keine Pflicht), wird in C++ garantiert, dass hier keine unnötige Kopie erzeugt wird. Im schlimmsten Fall nur eine Move-Construction; denn vector<int> wird einen Move-Constructor besitzen.

    Es gibt auch Fälle, in denen copy elisions erlaubt sind, welche aber von keinem Compiler aus technischen Gründen durchgeführt werden. Beispiel:

    string identity(string x) {return x;}
    

    Du wirst keinen Compiler finden, der hier eine copy/move elision durchführen kann. In C++0x wird hier automatisch das Rückgabe-Objekt von x "move-konstruiert".

    Dann haben wir auch noch Fälle, in denen nicht mal "copy elision" erlaubt ist und trotzdem können wir auch in diesen Fällen manchmal von einer Move-Konstruktion profitieren:

    void foo(vector<string> & v, string const& a, string const& b)
    {
      string tmp;
      tmp.reserve(a.size()+b.size());
      tmp += a;
      tmp += b;
      v.push_back(tmp);
    

    Nach dem push_back-Aufruf wird tmp nicht mehr benötigt. Wir können hier also eine unnötige Kopie durch ein

    v.push_back(std::move(tmp));
    

    vermeiden. Dabei ist std::move eigentlich nichts besonderes. Es ist nur ein versteckter Cast, der einen Rvalue-Ausdruck liefert, so dass push_back(string&&) statt push_back(string const&) aufgerufen wird. push_back(string&&) wird dann im Vektor ein neues String-Objekt per Move-Konstruktion erzeugen.

    Dazu kommen die Dinge, die hustbaer gesagt hat. Beispielsweise die Möglichkeit der "move-only" Typen (wie zB unique_ptr, Streams, unique_lock, unique_future, std::thread, ...). Bei diesen Typen gibt es keine sinnvolle Kopier-Semantik. Es ist aber trotzdem ggf praktisch, Objekte dieser Typen "umziehen" zu lassen (an eine neue Speicheradresse).



  • Vielen Dank für die vielen ausführlichen Antworten. Auf dieses Forum ist wie immer Verlass 🙂

    Philipp


Anmelden zum Antworten