std::mem_movable


  • Mod

    pumuckl schrieb:

    Ich bin mir relativ sicher, dass der Compiler die Folge x = 0; delete x; <end scope of x> problemlos erkennen und wegoptimieren kann. Jag doch mal ein simples Programm mit deiner String-Klasse durch den Compiler und schau, wie der Assembler dafür aussieht.

    Das würde nur bei link-time-Optimierung funktionieren, denn ohne Kenntnis der verwendeten Deallokationsfunktion, kann der Compiler nicht wissen, ob delete[] 0 keine beobachtbaren Nebeneffekte hat. Und das ist auch der Grund weshalb

    if(p!=nullptr)delete[]0;
    

    eben keineswegs sinnlos ist.



  • pumuckl schrieb:

    GorbGorb schrieb:

    Außerdem wird ein großes Stück Speicher mit mem_cpy schneller kopiert werden als mit Zuweisungen à 4 Byte.

    Wo steht das? Hast du Benchmarks? Der Compiler kann durchgängige 1:1 Zuweisungen nach Belieben zu einem memcpy optimieren und tut das auch.

    GorbGorb schrieb:

    Pro move ist das eine Zuweisung und eine delete[] (auf 0, aber kostenlos ist das trotzdem nicht), das ohne Not aufgerufen werden muss.

    Ich bin mir relativ sicher, dass der Compiler die Folge x = 0; delete x; <end scope of x> problemlos erkennen und wegoptimieren kann. Jag doch mal ein simples Programm mit deiner String-Klasse durch den Compiler und schau, wie der Assembler dafür aussieht.

    Ich hab mal ein benchmark gemacht:

    #include <iostream>
    #include <cstring>
    #include <ctime>
    #include <cstring>
    #include <vector>
    
    class my_string
    {
    public:
    	my_string()
    	{
    		mem_begin = 0;
    		mem_end = 0;
    		data_end = 0;
    	}
    	my_string( my_string &&str )
    	{
    		mem_begin = str.mem_begin;
    		mem_end = str.mem_end;
    		data_end = str.data_end;
    
    		str.mem_begin = 0;
    	}
    	~my_string()
    	{
    		delete[] mem_begin;
    	}
    private:
    	char *mem_begin;
    	char *mem_end;
    	char *data_end;
    };
    
    const unsigned int array_size = 10000;
    const unsigned int iterations = 100000;
    
    int main()
    {
    	my_string *a = new my_string[ array_size ];
    	my_string *b = static_cast< my_string* >( operator new( sizeof( my_string ) * array_size ) );
    
    	unsigned int t;
    
    	t = std::clock();
    	for( unsigned int i = 0 ; i < iterations ; ++i )
    	{
    		my_string *it_a = a;
    		my_string *end_a = a + array_size;
    		my_string *it_b = b;
    		while( it_a != end_a )
    		{
    			new ( it_b ) my_string( std::move( *it_a ) );
    
    			++it_a;
    			++it_b;
    		}
    	}
    	std::cout << "Move Constructor: " << std::clock() - t << std::endl;
    
    	//und zurück mit memcpy
    
    	t = std::clock();
    	for( unsigned int i = 0 ; i < iterations ; ++i )
    	{
    		std::memcpy( a , b , array_size * sizeof( my_string ) );
    	}
    	std::cout << "memcpy: " << std::clock() - t << std::endl;
    
    	std::cin.get();
    }
    

    Ausgabe bei mir (gcc 4.5):
    `

    Move Constructor: 6235

    memcpy: 1793

    `

    Es wurde also offensichtlich nichts optimiert. Interessant ist noch, dass sich memcpy und move constructor bei sehr großen Arraygrößen nicht mehr so viel nehmen, dann habe ich z.B. 850 und 632.
    Ein std::mem_movable brächte also offensichtlich bei den heutigen compilern (oder mindestens dem gcc) Vorteile.



  • Können std::memmove() und std::memcpy() (übrigens ohne Underscore) nicht genau dann sinvoll angewandt werden, wenn es sich um PODs handelt?

    Ich sehe hier nichts besonders Neues, im Grunde genommen handelt es sich bei std::move() 1 um die gleiche Thematik wie bei std::copy() . Eine kluge Implementierung kann für PODs durchaus Optimierungen anwenden.

    _____
    1: Überladung, die eine Iterator-Range verschiebt.



  • Nexus schrieb:

    Können std::memmove() und std::memcpy() (übrigens ohne Underscore) nicht genau dann sinvoll angewandt werden, wenn es sich um PODs handelt?

    std::string, std::vector, std::shared_ptr sind keine pods, können aber trotzdem per memcpy gemoved werden (sofern sie sich nicht intern selbst referenzieren, aber warum sollten sie).



  • Das bezweifle ich. Du kannst vielleicht was zusammenhacken, aber es bleibt undefiniertes Verhalten. Schliesslich ist der Implementierung freigestellt, dir mit "warum sollten sie"-Aktionen den Boden unter den Füssen wegzureissen.



  • Natürlich müssen sie nicht mem_movable sein, könnten es aber trotzdem sein. Deshalb meinte ich ja, wäre es sinnvoll, einen type trait zu haben, den man spezialisieren kann.

    So etwa:

    class string
    {
    //...
    };
    template<>
    class mem_movable< string >
        : public true_type
    {
    };
    //ein std::string wird vermutlich keine Zeiger auf sich selbst besitzen
    //falls er das doch tut, kann man die Spezialisierung ja weglassen
    


  • Zunächst mal solltest du schon die Optimierungen einschalten, um zu kucken, ob etwas optimiert wird. Dein Testfall erzeugt bei mir selbst mit -O1 nur noch

    Move Constructor: 3610000
    memcpy: 2130000
    

    Dass memcpy hier schneller ist, ist aber wenig verwunderlich, weil es stumpf weniger macht. Wie soll die Laufzeitumgebung deiner Meinung nach später auseinanderhalten, welche Objekte noch zerstört werden müssen? Stell dir vor, ich schreibe

    void move_memory_maybe(mem_movable<foo> *dest, mem_movable<foo> *src, std::size_t n) {
      if(rand() % 2 == 0) std::memcpy(dest, src, n * sizeof(*dest));
    }
    

    ...woher weiß der aufrufende Code später, wofür er Destruktoren aufrufen muss?



  • Es scheint wohl nicht ganz klar zu sein, was ich meinte...
    Also:
    Ich wünsche mir einen neuen std type trait, nämlich std::mem_movable.
    std::mem_movable gibt an, ob ein Typ statt per copy bzw move constructor per memcpy gemoved werden darf.
    Jemand der den std::vector baut dürfte z.B. bei allen Typen, für die std::mem_movable wahr ist, statt move constructor und anschließendem destructor auf den alten Objekten einfach memcpy verwenden und den alten Speicher ohne destructor freigeben.
    Offensichtlich ist das für POD Klassen automatisch erfüllt, bei anderen Typen würde std::mem_movable per default false ergeben.
    Es gibt aber auch noch viele andere Klassen, bei denen das so erfüllt ist, beispielsweise bei den meisten Versionen von std::vector und std::string.
    Heutige compiler können move & destructor nicht optimieren, wie das von mir gepostete benchmark zeigt.
    Wer solche Klassen entwickelt könnte jetzt also einfach den type trait std::mem_movable für seine Klasse spezialisieren, um trotzdem klar zu machen, dass seine Klasse auf die schnellstmögliche Weise per memcpy gemoved werden darf.
    Vor diesem Hintergrund verstehe ich nicht, wie

    seldon schrieb:

    void move_memory_maybe(mem_movable<foo> *dest, mem_movable<foo> *src, std::size_t n) {
      if(rand() % 2 == 0) std::memcpy(dest, src, n * sizeof(*dest));
    }
    

    Sinn ergeben soll. Man erstellt keine Instanzen von type traits, die sind doch nur fürs meta programming.
    Wenn man move_maybe mit dem normalen move schreiben würde, wüsste der aufrufende code doch auch nicht, was er jetzt zerstören soll:

    void move_maybe( T *dest , T *src )
    {
        if( rand() % 2 == 0 ) new ( dest ) T( std::move( *src ) );
    }
    //muss auf dest jetzt der destructor aufgerufen werden?
    

  • Mod

    GorbGorb schrieb:

    Es scheint wohl nicht ganz klar zu sein, was ich meinte...
    Also:
    Ich wünsche mir einen neuen std type trait, nämlich std::mem_movable.
    std::mem_movable gibt an, ob ein Typ statt per copy bzw move constructor per memcpy gemoved werden darf.
    Jemand der den std::vector baut dürfte z.B. bei allen Typen, für die std::mem_movable wahr ist, statt move constructor und anschließendem destructor auf den alten Objekten einfach memcpy verwenden und den alten Speicher ohne destructor freigeben.
    Offensichtlich ist das für POD Klassen automatisch erfüllt, bei anderen Typen würde std::mem_movable per default false ergeben.
    Es gibt aber auch noch viele andere Klassen, bei denen das so erfüllt ist, beispielsweise bei den meisten Versionen von std::vector und std::string.
    Heutige compiler können move & destructor nicht optimieren, wie das von mir gepostete benchmark zeigt.
    Wer solche Klassen entwickelt könnte jetzt also einfach den type trait std::mem_movable für seine Klasse spezialisieren, um trotzdem klar zu machen, dass seine Klasse auf die schnellstmögliche Weise per memcpy gemoved werden darf.
    Vor diesem Hintergrund verstehe ich nicht, wie

    seldon schrieb:

    void move_memory_maybe(mem_movable<foo> *dest, mem_movable<foo> *src, std::size_t n) {
      if(rand() % 2 == 0) std::memcpy(dest, src, n * sizeof(*dest));
    }
    

    Sinn ergeben soll. Man erstellt keine Instanzen von type traits, die sind doch nur fürs meta programming.
    Wenn man move_maybe mit dem normalen move schreiben würde, wüsste der aufrufende code doch auch nicht, was er jetzt zerstören soll:

    void move_maybe( T *dest , T *src )
    {
        if( rand() % 2 == 0 ) new ( dest ) T( std::move( *src ) );
    }
    //muss auf dest jetzt der destructor aufgerufen werden?
    

    Objekte, die nicht trivial kopiert werden können, per memcpy/memmove zu kopieren, führt zu undefiniertem Verhalten (genauer: das Kopieren selbst ist unproblematisch, aber an der Zieladresse entstehen keine Objekte des entsprechenden Typs, und dann darauf zugreifen zu wollen, als ob entsprechende Objekte da wären führt zu UB, i.d.R. über 3.10/10).
    Es wäre denkbar, ein Prädikat zu haben, dass aussagt, ob die Ausführung eines nicht-trivialen Destruktors unterbleiben darf, falls irgendwie eine flache Kopie erstellt wurde. Ohne eine entsprechend universell einsatzbare und schnelle Kopierroutine (und memcpy ist eben genau das nicht), ist es nicht besonders nützlich.



  • GorbGorb schrieb:

    Wenn man move_maybe mit dem normalen move schreiben würde, wüsste der aufrufende code doch auch nicht, was er jetzt zerstören soll:

    void move_maybe( T *dest , T *src )
    {
        if( rand() % 2 == 0 ) new ( dest ) T( std::move( *src ) );
    }
    //muss auf dest jetzt der destructor aufgerufen werden?
    

    Ich wollte eigentlich mehr auf src hinaus. Ein Move-Konstruktor muss ja nicht nur den Inhalt des Quellobjektes kopieren, sondern auch das alte invalidieren. Für std::vector in gccs libstdc++ sieht das etwa so aus (in einer privaten Basisklasse):

    _Vector_base(_Vector_base&& __x)
          : _M_impl(__x._M_get_Tp_allocator())
          {
            this->_M_impl._M_start = __x._M_impl._M_start;
            this->_M_impl._M_finish = __x._M_impl._M_finish;
            this->_M_impl._M_end_of_storage = __x._M_impl._M_end_of_storage;
            __x._M_impl._M_start = 0;
            __x._M_impl._M_finish = 0;
            __x._M_impl._M_end_of_storage = 0;
          }
    

    Der Destruktor wird dann für alle Objekte nach wie vor aufgerufen, macht aber für die geleerten (praktisch) nichts. Vor diesem Hintergrund ist es wenig verwunderlich, dass dein Benchmark einen Geschwindigkeitsvorteil für memcpy heraushaut, weil memcpy ja stumpf weniger macht. Ich sehe nicht, wie bei deinem Ansatz sichergestellt werden kann, dass die Laufzeitumgebung bewegte von unbewegten Objekten unterscheiden kann.



  • camper schrieb:

    Objekte, die nicht trivial kopiert werden können, per memcpy/memmove zu kopieren, führt zu undefiniertem Verhalten (genauer: das Kopieren selbst ist unproblematisch, aber an der Zieladresse entstehen keine Objekte des entsprechenden Typs, und dann darauf zugreifen zu wollen, als ob entsprechende Objekte da wären führt zu UB, i.d.R. über 3.10/10).

    Schade, dann bräuchte ein solches feature also Änderungen am c++ core (und wohl nicht unerhebliche).

    Es wäre denkbar, ein Prädikat zu haben, dass aussagt, ob die Ausführung eines nicht-trivialen Destruktors unterbleiben darf, falls irgendwie eine flache Kopie erstellt wurde. Ohne eine entsprechend universell einsatzbare und schnelle Kopierroutine (und memcpy ist eben genau das nicht), ist es nicht besonders nützlich.

    Mir ist das Ganze gekommen, als ich über einen move-Konstruktor für shared_ptr nachgedachte habe. Mit mem_movable könnte man sich hier ein if( ptr ) bzw. ein Zugriff auf ein std::atomic beim moven sparen (beides nicht gerade billig).
    Außerdem ist das halt etwas, das idiomatisches c++ langsamer als c macht, was ich irgendwie unbefriedigend finde.



  • GorbGorb schrieb:

    camper schrieb:

    Objekte, die nicht trivial kopiert werden können, per memcpy/memmove zu kopieren, führt zu undefiniertem Verhalten (genauer: das Kopieren selbst ist unproblematisch, aber an der Zieladresse entstehen keine Objekte des entsprechenden Typs, und dann darauf zugreifen zu wollen, als ob entsprechende Objekte da wären führt zu UB, i.d.R. über 3.10/10).

    Schade, dann bräuchte ein solches feature also Änderungen am c++ core (und wohl nicht unerhebliche).

    Ich gehe sogar soweit zu behaupten dass so ein Feature nicht konsistent in die Sprache eingeführt werden könnte.



  • pumuckl schrieb:

    Ich gehe sogar soweit zu behaupten dass so ein Feature nicht konsistent in die Sprache eingeführt werden könnte.

    c++ und Konsistenz... aber das ist ein anderes Thema.



  • GorbGorb schrieb:

    c++ und Konsistenz... aber das ist ein anderes Thema.

    Beispiele?



  • otze schrieb:

    GorbGorb schrieb:

    c++ und Konsistenz... aber das ist ein anderes Thema.

    Beispiele?

    Typenme z.B.

    (Von Effective C++ kopiert)

    template<typename T>
    class Derived : public Base<T>::Nested { //typename nicht zulässig
    public:
      explicit Derived(int x)
        : Base<T>::Nested(x) {} // typename nicht zulässig
    
      // ...
      void some_member_function() {
        //...
        typename Base<T>::Nested temp; // typename erforderlich
        //...
      }
    };
    

    Auch wenn einem die Regeln klar sind, warum man an den verschiedenen Stellen typename braucht bzw. es nicht zulässig ist, so ist es dennoch inkonsistent. Man hätte typename wenigstens dann, wenn es unnötig ist, trotzdem zulassen können.

    Man muss ja auch bei geerbten virtuellen Funktionen das virtual nicht mehr extra hinschreiben, aber es ist kein Fehler, wenn man es trotzdem tut.

    class Base {
      virtual foo();
      virtual bar();
    };
    
    class Derived {
      foo(){ // implizit virtual
        // ... 
      }
      virtual bar(){ // virtual zwar unnötig, aber dennoch kein Fehler
        // ...
      }
    };
    


  • Mist, Derived im zweiten Beispiel sollte natürlich von Base ableiten!



  • Zuerst mal wäre da die Erbsünde c zu nennen, Präprozessor, Ellipsen, etc.
    Dann kommen noch ein paar Sachen von c++ dazu:

    • don't pay what you don't use wird verletzt:
      - rtti
      - exceptions (war es nicht so dass die compiler dann nicht so gut optimieren
      können? wobei exceptions ein Programm auch schneller machen können, hier gibt es wohl keinen Königsweg)
    • die Standardbibliothek ist mit der Sprache verwurstelt: typeid, dynamic_cast und sizeof
    • private Member sieht man auch in einer abgeleiteten Klasse:
    class A
    {
    };
    class Base
    {
        class A
        {
        };
    };
    class Derived
        : public Base
    {
        A a;
    };
    
    • template Syntax, bin ja grade erst wieder auf die Schnauze geflogen, weil sowas möglich ist:
    template< template< class > class Template >
    class A
    {
    };
    template< class Type >
    class B
    {
    };
    
    template< class Type >
    class C
    {
    	A< C > a;  //C als class template
    	B< C > b;  //C als class
    };
    


  • bmario_ schrieb:

    Auch wenn einem die Regeln klar sind, warum man an den verschiedenen Stellen typename braucht bzw. es nicht zulässig ist, so ist es dennoch inkonsistent.

    Unintuitiv, aber nicht inkonsistent. Inkonsistenz würde bedeuten, dass der Standard sich selbst widerspricht.

    GorbGorb schrieb:

    Zuerst mal wäre da die Erbsünde c zu nennen, Präprozessor, Ellipsen, etc.

    Was ist da inkonsistent?

    don't pay what you don't use wird verletzt:
    - rtti
    - exceptions (war es nicht so dass die compiler dann nicht so gut optimieren
    können? wobei exceptions ein Programm auch schneller machen können, hier gibt es wohl keinen Königsweg)

    Wo wird da was verletzt? Für Klassen ohne virtuelle Funktionen gibts auch kein RTTI. Wenn man keine Exceptions benutzt, verlangt der Standard auch keinen Overhead dafür. (Was die Compiler draus machen ist was anderes)

    [*] die Standardbibliothek ist mit der Sprache verwurstelt: typeid, dynamic_cast und sizeof

    Mal abgesehen davon dass weder dynamic_cast noch sizeof irgendwas mit der Standardbibliothek zu tun haben: Was macht den Standard in sich inkonsistent, wenn zwei seiner Features (typeid-Operator und type_info Klasse) voneinander abhängig sind? Beide sind Teil des Standards, genauso wie Klassen und Memberfunktionen - letztere würden ohne erstere auch keinen Sinn machen.

    [*] private Member sieht man auch in einer abgeleiteten Klasse:

    class A
    {
    };
    class Base
    {
        class A
        {
        };
    };
    class Derived
        : public Base
    {
        A a;
    };
    

    Da bist du einem Irrtum aufgesessen. Derived sieht nur ::A, nicht Base::A.

    [*] template Syntax, bin ja grade erst wieder auf die Schnauze geflogen, weil sowas möglich ist:

    template< template< class > class Template >
    class A
    {
    };
    template< class Type >
    class B
    {
    };
    
    template< class Type >
    class C
    {
    	A< C > a;  //C als class template
    	B< C > b;  //C als class
    };
    

    Und? Wo ist die Inkonsistenz? Dass du mit den Regeln auf dem Kriegsfuß stehst und Probleme damit hast, bedeutet doch nicht, dass der Standard inkonsistent ist.



  • Zu GorbGorbs Beitrag wollte ich auch gerade was schreiben, pumuckl hat aber schon alles gesagt 🙂

    Wie sieht es aber mit dem typename aus, warum genau ist das bei Vererbungs- und Initialisierungsliste verboten? Ich würde hier übrigens auch von Inkonsistenz sprechen, dazu muss kein Widerspruch vorhanden sein.



  • GorbGorb schrieb:

    Zuerst mal wäre da die Erbsünde c zu nennen, Präprozessor, Ellipsen, etc.

    Mit beidem kann man tolle Sachen machen. Den Präprozessor, um Schreibarbeit zu sparen, Ellipsen für TMP.


Anmelden zum Antworten