Frage zu RValue-Referenzen und Optimierung



  • Hallo,
    ich habe folgendes Codebeispiel:

    #include <vector>
    #include <string>
    
    struct Data {
    	Data(std::string string, int integer) : str(string), i(integer){};
    	std::string str;
    	int i;
    };
    
    class Second {
    public:
    	Second(Data&& data_) : data(data_){};
    private:
    	Data data;
    };
    
    class First {
    public:
    	First(Data&& data) : second(std::move(data)){};
    private:
    	Second second;
    };
    
    int main() {
    	First test(Data("test",42));
    	return 0;
    }
    

    Nun stellen sich mir da zwei Fragen.

    1. Sind hier RValue-Referenzen sinnvoll? Reichen hier nicht auch normale Referenzen zur Optimierung? Und wo ist dann das wirkliche Einstatzgebiet von RValue-Referenzen?

    2. Wenn ich bei meinem Compiler (MSVC 10) die Optmierung ausschalte ist die Disassembly wesentlich kleiner, als wenn ich das O2-Flag drin habe. Das ganze sieht auf den ersten Blick dann erst mal schneller aus. Wieso ist das so?
    Hier mal die Disassembly:

    ; Ohne Optimierung
    002616E0  push        2Ah  
    002616E2  sub         esp,20h  
    002616E5  mov         ecx,esp  
    002616E7  mov         dword ptr [ebp-110h],esp  
    002616ED  push        offset string "test" (268830h)  
    002616F2  call        std::basic_string<char,std::char_traits<char>,std::allocator<char> >::basic_string<char,std::char_traits<char>,std::allocator<char> > (2612DAh)  
    002616F7  mov         dword ptr [ebp-144h],eax  
    002616FD  lea         ecx,[ebp-13Ch]  
    00261703  call        Data::Data (261019h)  
    00261708  mov         dword ptr [ebp-148h],eax  
    0026170E  mov         eax,dword ptr [ebp-148h]  
    00261714  mov         dword ptr [ebp-14Ch],eax  
    0026171A  mov         dword ptr [ebp-4],0  
    00261721  mov         ecx,dword ptr [ebp-14Ch]  
    00261727  push        ecx  
    00261728  lea         ecx,[ebp-38h]  
    0026172B  call        Parent::Parent (261177h)  
    00261730  mov         byte ptr [ebp-4],2  
    00261734  lea         ecx,[ebp-13Ch]  
    0026173A  call        Data::~Data (261145h)
    
    ; Mit O2 optimiert
    0001107A  xor         ebx,ebx  
    0001107C  lea         edi,[ebx+4]  
    0001107F  lea         esi,[esp+14h]  
    00011083  mov         dword ptr [esp+28h],0Fh  
    0001108B  mov         dword ptr [esp+24h],ebx  
    0001108F  mov         byte ptr [esp+14h],bl  
    00011093  call        std::basic_string<char,std::char_traits<char>,std::allocator<char> >::assign (11280h)  
    00011098  mov         eax,esi  
    0001109A  mov         dword ptr [esp+88h],ebx  
    000110A1  push        eax  
    000110A2  or          eax,0FFFFFFFFh  
    000110A5  lea         ecx,[esp+3Ch]  
    000110A9  mov         dword ptr [esp+50h],0Fh  
    000110B1  mov         dword ptr [esp+4Ch],ebx  
    000110B5  mov         byte ptr [esp+3Ch],bl  
    000110B9  call        std::basic_string<char,std::char_traits<char>,std::allocator<char> >::assign (111A0h)  
    000110BE  mov         edi,dword ptr [__imp_operator delete (120BCh)]  
    000110C4  mov         esi,10h  
    000110C9  mov         dword ptr [esp+54h],2Ah  
    000110D1  cmp         dword ptr [esp+28h],esi  
    000110D5  jb          wmain+0A1h (110E1h)  
    000110D7  mov         ecx,dword ptr [esp+14h]  
    000110DB  push        ecx  
    000110DC  call        edi  
    000110DE  add         esp,4  
    000110E1  xor         eax,eax  
    000110E3  mov         ecx,0Fh  
    000110E8  mov         dword ptr [esp+28h],ecx  
    000110EC  mov         dword ptr [esp+24h],eax  
    000110F0  mov         byte ptr [esp+14h],al  
    000110F4  mov         dword ptr [esp+88h],1  
    000110FF  lea         edx,[esp+38h]  
    00011103  mov         dword ptr [esp+6Ch],ecx  
    00011107  mov         dword ptr [esp+68h],eax  
    0001110B  mov         byte ptr [esp+58h],al  
    0001110F  push        edx  
    00011110  or          eax,0FFFFFFFFh  
    00011113  xor         ebx,ebx  
    00011115  lea         ecx,[esp+5Ch]  
    00011119  call        std::basic_string<char,std::char_traits<char>,std::allocator<char> >::assign (111A0h)  
    0001111E  mov         eax,dword ptr [esp+54h]  
    00011122  mov         dword ptr [esp+74h],eax  
    00011126  cmp         dword ptr [esp+4Ch],esi  
    0001112A  jb          wmain+0F6h (11136h)  
    0001112C  mov         ecx,dword ptr [esp+38h]  
    00011130  push        ecx  
    00011131  call        edi  
    00011133  add         esp,4
    


  • Bubiboy schrieb:

    1. Sind hier RValue-Referenzen sinnvoll? Reichen hier nicht auch normale Referenzen zur Optimierung? Und wo ist dann das wirkliche Einstatzgebiet von RValue-Referenzen?

    Data zwingt dich immer zu kopieren, da hilft garnichts. Wenn dein Code sagt: A muss immer kopiert werden, dann kannst das nicht umgehen und musst A immer kopieren.

    rvalue Referenzen ermoeglichen dir zu sagen: A muss nicht kopiert werden.

    2. Wenn ich bei meinem Compiler (MSVC 10) die Optmierung ausschalte ist die Disassembly wesentlich kleiner, als wenn ich das O2-Flag drin habe. Das ganze sieht auf den ersten Blick dann erst mal schneller aus. Wieso ist das so?

    Weil du mit O2 inlining aktiviert hast. Dadurch hast du idR laengeren aber schnelleren Code.



  • Shade Of Mine schrieb:

    Data zwingt dich immer zu kopieren, da hilft garnichts.

    Wieso das? Wenn ich das Ganze richtig verstanden habe, kann der Compiler im Fall von Data einen impliziten Move-Konstruktor und Move-Zuweisungsoperator generieren. Jetzt mal unabhängig vom Rest des Codes.



  • Ich glaube da fehlen zumindest zwei std::move.
    1: Second(Data&& data_) : data(std::move(data_)){};
    2: Data(std::string string, int integer) : str(std::move(string)), i(integer){};

    Bei 1 deswegen, weil das Ding nen Namen hat, und was nen Namen hat wird nimmer als Rvalue verwendet (auch wenn es eine Rvalue Referenz ist).

    Bei 2 deswegen, weil der Compiler ja nicht wissen kann, dass du den übergebenen Parameter "kaputtmachen" willst (bzw. auch ganz allgemein deswegen, weil das Teil nen Namen hat).

    Das Move "in" den Parameter sollte noch gehen, das Move vom Parameter ins Member aber nicht (also nicht ohne std::move).

    Wobei ich dazusagen muss dass ich mir Rvalue Referenzen auch nur am Rande angesehen habe, und noch nirgends wirklich verwendet (wir verwenden noch MSVC 2005 in der Firma)!

    Eine Bestätigung/Korrektur eines "Wissenden" wäre also fein 🙂



  • @hustbaer: korrekt. Alles was einen Namen hat ist ein LValue, und in beiden Fällen kann gemoved werden.



  • Sind hier RValue-Referenzen sinnvoll? Geht so. Jedenfalls nicht so, wie sie aktuell angewendet wurden. Man kann's auch ganz ohne machen. Warum auch nicht?

    #include <vector>
    #include <string>
    #include <utility>
    
    struct Data {
        std::string str;
        int i;
    };
    
    class Second {
    public:
        Second(Data d) : data(std::move(d)){}
    private:
        Data data;
    };
    
    class First {
    public:
        First(Data d) : second(std::move(d)){}
    private:
        Second second;
    };
    
    int main() {
        First test(Data{"test",42});
        return 0;
    }
    

    (ungetestet)
    Wenn ich hier keinen Fehler gemacht habe, wird hier nichts unnötig kopiert, nur ein paar mal "gemovet".

    Rvalue-Referenzen bzgl Move-Semantik sind dazu da, den Move-Constructor schreiben zu können. Sonst sind das stinknormale Referenzen. Es gibt nur zwei Sonderheiten (und eine dritte, dich ich verschweige, weil ich Dich nicht durcheinander bringen will): Du kannst eine Rvalue-Referenz nur mit etwas Initialisieren, was sich entweder auf ein temporäres Objekt oder auf ein anderes Objekt beziehen wird, dessen Zustand verändert werden darf, ohne dass das jemanden stören würde. Mit std::move kann man so etwas zB ausdrücken, sollte man aber auch nur machen, wenn es nicht sowieso schon überflüssig wäre. Der zweite Unterschied liegt bei einer Rvalue-Referenz als Rückgabe-Typ einer Funktion. Das sollte man zwar bei eigenen Funktionen vermeiden (!!!), aber du solltest wissen, dass ein solcher Funktionsaufruf ein Rvalue-Ausdruck und kein Lvalue-Ausdruck darstellt. std::move und std::forward nutzen dies aus.

    Nachtrag: Data wird hier jeweils "per Wert" an die Konstruktoren übergeben. Je nachdem, was Du für eine Quelle hast (ist das Argument ein Lvalue oder Rvalue?) wird hier entweder der Kopier- oder der Move-Konstruktor ausgewählt. Der Parameter 'd' ist ein Lvalue-Ausdruck, der aber später nicht mehr benötigt wird. Daher wird der nächste Konstruktor mit std::move(d) als Argument aufgerufen, was eine unnötige Kopie verhindert; denn Data bekommt automatisch einen Move-Construktor, der wich für Rvalue-Quellen verantwortlich fühlt.


Anmelden zum Antworten