std::mem_movable
-
Hallo,
wäre es nicht sinnvoll, einen einen type trait
mem_movable<T>einzuführen? Er wäre dann erfüllt, wenn eine Instanz eine Klasse per mem_cpy gemoved werden kann, wenn danach kein Destruktor auf den alten Speicher aufgerufen wird. Er müsste vermutlich für nicht-pods explizit spezialisiert werden.
Vorteil wäre der, dass nicht unnötig Destruktoren aufgerufen werden, wenn sie nicht nötig sind. So könnte beispielsweise ein std::vector viel performanter wachsen, als das bisher der Fall ist.
Im Grunde sind wohl fast alle Klassen mem_movable, es sei denn sie besitzen intern pointer auf eigene member bzw. move/copy Konstruktoren sind explizit deleted.
Ein shared_ptr muss beispielsweise im Destruktor auf jeden fall irgendeinif(...)ausführen, was man sich damit sparen könnte.
Was haltet ihr davon?
-
Seit C++11 ist das überflüssig. Move-Semantik kann im Prinzip ebenso performant wie memmove/memcpy implementiert werden und ist sicherer, hat mehr Anwendungsfälle und funktioniert vollautomatisch ohne zusätzlichen Trait.
-
ipsec schrieb:
Seit C++11 ist das überflüssig. Move-Semantik kann im Prinzip ebenso performant wie memmove/memcpy implementiert werden und ist sicherer, hat mehr Anwendungsfälle und funktioniert vollautomatisch ohne zusätzlichen Trait.
Zum einen ist mem_cpy meistens performanter, als einzeln Variablen zu verschieben, und zum andern hab ich doch beschrieben, dass man manchmal unnötigen code im Destruktor ausführt. Ein einfacher std::string könnte z.B. so aussehen:
class string { public: string( string &&s ) { mem_begin = s.mem_begin; mem_end = s.mem_end; data_end = s.data_end; s.mem_begin = 0; //schon mal eine Zuweisung mehr als nötig } ~string() { delete[] mem_begin; //das ist eigentlich unnötig, wenn nur gemoved werden soll } private: char *mem_begin; char *mem_end; char *data_end; //von mem_begin bis data_end reicht der eigentlich gespeicherte String };Pro move ist das eine Zuweisung und eine delete[] (auf 0, aber kostenlos ist das trotzdem nicht), das ohne Not aufgerufen werden muss. Außerdem wird ein großes Stück Speicher mit mem_cpy schneller kopiert werden als mit Zuweisungen à 4 Byte.
Im Grunde hindert einen ja nichts daran, sowas einfach selbst zu definieren. Aber ohne dass die std container das auch nutzen, bringt es leider eher wenig.
-
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.
-
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()undstd::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 beistd::copy(). Eine kluge Implementierung kann für PODs durchaus Optimierungen anwenden._____
1: Überladung, die eine Iterator-Range verschiebt.
-
Nexus schrieb:
Können
std::memmove()undstd::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: 2130000Dass 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, wieseldon 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?
-
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, wieseldon 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
virtualnicht 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!