Tausch (Assembler, Kopien auf Stack, Zeiger, Referenzen)
-
Ich habe hier verschiedene Tauschaktionen für zwei globale Variablen a und b dargestellt, für Einsteiger vielleicht interessant:
#include <iostream> #include <conio.h> using namespace std; //globale Variablen int a = 1; int b = 2; void ausgabe() { cout << &a << ": " << a << "\t " << &b << ": " << b << endl << endl; } void erfolgloser_swap(int x, int y) { cout << "Kopien auf dem Stack: " << endl; cout << &x << ": " << x << "\t " << &y << ": " << y << endl; int temp = x; x = y; y = temp; cout << &x << ": " << x << "\t " << &y << ": " << y << endl; cout << "swap nicht erfolgreich." << endl; } void zeiger_swap(int* x, int* y) { int temp = *x; *x = *y; *y = temp; } void referenzen_swap(int& x, int& y) { int temp = x; x = y; y = temp; } int main() { ausgabe(); cout << "Tausch mittels Register eax und ebx: " << endl; //swap (a,b) __asm("mov _b, %eax"); //AT&T Syntax bei Dev-C++ __asm("mov _a, %ebx"); __asm("mov %eax, _a"); __asm("mov %ebx, _b"); ausgabe(); cout << "Erfolgloser Tausch, da nur lokale Kopien getauscht werden." << endl; erfolgloser_swap(a,b); ausgabe(); cout << "Tausch mittels Zeiger: " << endl; zeiger_swap(&a, &b); ausgabe(); cout << "Tausch mittels Referenzen: " << endl; referenzen_swap(a,b); ausgabe(); getch(); }
-
Inwiefern ist die Assembler-Geschichte für Einsteiger interessant? Wollen die Assembler lernen -- dann sollten sie sich mal den XCHG-Befehl angucken. Ansonsten bringt das ganze nix, Assembler ist keine Wunderwaffe mit dem man alles erschlagen kann, insbesondere wenn man den Registerallokator durcheinanderbringt. Eine inline-swap-Funktion mit Referenzen wär das gescheiteste (gibts sogar schon: std::swap)
-
Anfänger sollten möglichst noch nicht so viel mit Zeigern rumhantieren. Deshalb ist swap mit Zeigern blödinnig für Anfänger.
Wenn du nur Kopie vs. Referenz zeigst kannst du es in dein Anfänger-Tutorium packen.
-
So langsam fange ich an zu zweifeln, ob C++ überhaupt für Einsteiger geeignet ist. Ich halte es da mit Richard Feynman (großartiger Physiker und Lehrer). Der hat zu Anfang alles(!) an die Tafel geklatscht und dann step by step die Feinheiten analysiert. Eure Scheu vor Assembler und C teile ich in keinster Weise. Ansonsten kann man doch gleich Java oder C# anbieten. Wer sich auf C/C++ einlässt, muss hart im Nehmen sein. Dafür gibt's dann die volle Bandbreite zur Belohnung. Kastriertes Standard-C++ ist nur für Weicheier.
Ich werde sogar die anderen Alternativen (siehe Bashar's Hinweise) noch dazu packen.
-
Du meinst die einzig sinnvolle: std::swap? Das ist ja einfach unglaublich.
Wenn man ein swap braucht verwendet man std::swap und wenn's schnell sein muss und keine Rücksicht auf Portabilität gelegt wird, vielleicht auch Assembler, aber alles andere ist Humbug. Also: verwende Dinge, die auch Sinn machen und nicht irgendwas, was sowieso niemand braucht. Und dein Assemblerbeispiel ist hundsmiserabel (siehe Bashars Post).
-
also ich finde das tauschen mit zeigern und referenzen fuer einsteiger
interessant und wichtig. das hilft beim verstaendnis; anstatt nur irgendeine
funktion stupide aufzurufen und nicht zu wissen was dahinter steckt.
-
Die Weicheier werden aber immer schneller damit fertig sein ihren Code auf ein anderes System zu Portieren wie die Freestyle Programmierer ;o)
Es ist im übrigen keine Scheu davor. Vielmehr ist es eine wirtschaftliche Überlegung: Knall ich da nun Assemblersource rein (wobei ich dann vllt. erst mal das Assembler lernen muss) oder überlasse ich es dem Compiler z.B. std::swap umzusetzen und nehme dabei vllt. ein klein wenig Laufzeitverlust hin ?
Betrachtet man dazu das man eventuell mal auf eine andere Plattform portieren will wäre eine Möglichkeit noch einen Profiler zu befragen ob es sinn macht den Quellcode einer starken Abhängigkeit auszusetzen.
-
Knuddlbaer schrieb:
Es ist im übrigen keine Scheu davor. Vielmehr ist es eine wirtschaftliche Überlegung: Knall ich da nun Assemblersource rein (wobei ich dann vllt. erst mal das Assembler lernen muss) oder überlasse ich es dem Compiler z.B. std::swap umzusetzen und nehme dabei vllt. ein klein wenig Laufzeitverlust hin ?
wenn std::swap() fuer builtins nicht spezialisiert ist - wuerde ich nur diese spezialisierungen ergaenzen (und da xchg verwenden) - so kann man immer std::swap() verwenden und sich sicher sein, dass es sau schnell ist.
man beachte nur die laufzeit von
string s1("hallo du schoene grosse tolle coole einmalige welt"); string s2("!"); //teuer: string t(s1); s1=s2; s2=t; //billig: swap(s1,s2); //denn swap macht einfach s1.swap(s2); //und das kostet vielleicht 3 xchg
Templates sind geil.
-
Es geht doch offenbar nicht darum zu zeigen, auf welche weise man Variablen übergeben kann, da das Assemblerbeispiel dann nicht passt. Also scheint es darum zu gehen, wie man Variablen tauscht. Aber wieso werden dann Möglichkeiten gezeigt, die sich nur in der Übergabe unterschieden?
An deiner Didaktik musst du noch etwas feilen.
-
Schaut euch zu diesem Thema mal Lektion 33 - 37 bei Volkard an. Der findet sogar die swap-Funktion mit den Referenzen als Parameter nicht gut, da sich a und b verändern, nur weil man sie einer Funktion übergeben hat. Ich meine, man sollte alles zeigen, dabei für die einzelnen Methoden die Themen Portabilität und Performance diskutieren. Assembler ist bezüglich Portabilität absolut problematisch (AT&T-, Intel-Syntax).
-
Ich habe keine Scheu vor Assembler, und habe auch nichts bezüglich Portabilität gesagt. Ich sage, dass das Beispiel mit Assembler keinen didaktischen Wert hat: Ein Einsteiger wär gut beraten, da einfach drüberzulesen, und dann kannstes auch gleich weglassen. Mit Hardcore-C++ hat das nichts zu tun, schon allein weil das Beispiel so mies ist. Das schlimme ist, dass jemand, der in diesem Gebiet eher unbeleckt ist, glauben könnte, die Assembler-Lösung sei die schnellste und daher vorzuziehen.
-
EDIT: Einiges davon hatte Bashar schon erwähnt...
Das Problem ist, dass sich Anfänger auf diese Weise von vorne herein an umständliche Sachen gewöhnen könnten. Vor 2 Jahren hätte ich Erhards Code gelesen und mir gedacht: "Geil, Assembler. Sieht ja voll kompliziert aus. Dann ist es bestimmt auch schneller als das popelige swap!". Überzeugt davon das Richtige zu tun hätte ich es in vieele vieeele Programme eingebaut und würde mich heute totärgern weil ich solch einen Mist in meinen alten Sourcen rumgammeln habe aber nicht die Zeit finde es auszubessern. Ich denke man sollte in normalen Anwendungen komplett auf Assembler verzichten. Sicher, ich hatte auch schonmal einen dieser Fälle in denen ein wenig inline-asm Wunder wirkte war mir aber auch im klaren darüber was ich machte und versuchte diese Stelle so isoliert wie möglich zu halten damit ich bei der Portierung mit ein paar ifdefs (also quasi mit blauen Flecken) davonkommen könnte. Wenn man so versessen auf "Hardcoreprogrammierung" ist sollte man bei seinem TASM bleiben und C++ den "Weicheiern" überlassen.
-
die darstellung von
call by value
und
call by referenceist ein typisches lehrbeispiel, soll übrigens nicht "std::swap" ersetzen ;), sondern muss zwischen dem erlernen von zeigern und referenzen und klassen angesiedelt werden, um zu zeigen, daß es billiger ist, mit referenzen objekte zu übergeben, als per wert.
und natürlich auch: was passiert da. (mal wieder im speicher rumkriechen, ein wenig den überblick kriegen, zeiger festigen...)
assembler gehört da nicht rein. ! ausrufungszeichen.
entweder ich unterrichte c++ oder ich unterrichte assembler.um assemblercode einzubinden, hätte ich den anspruch, daß die leute wissen was sie tun, wäre dann was für "c++ für leute mit assemblervorkenntnissen".
aber an der stelle vollkommen unbrauchbar.
-
o.k., überzeugt. Assembler bleibt weg. XCHG würde die Sache auch nicht verbessern, da man hierbei ebenfalls über die Register (die man eigentlich vorher pushen und nachher poppen müsste) gehen muss. Die STL-Methode std::swap gehört selbstverständlich dazu und ist die Methode der Wahl.
Wäre das so akzeptabel?
#include <iostream> #include <conio.h> using std::cout; using std::endl; //globale Variablen int a = 1; int b = 2; void ausgabe() { cout << &a << ": " << a << "\t " << &b << ": " << b << endl << endl; } void erfolgloser_swap(int x, int y) { cout << endl << "Kopien auf dem Stack: " << endl; cout << &x << ": " << x << "\t " << &y << ": " << y << endl; int temp = x; x = y; y = temp; cout << &x << ": " << x << "\t " << &y << ": " << y << endl; cout << endl; } void zeiger_swap(int* x, int* y) { int temp = *x; *x = *y; *y = temp; } void referenzen_swap(int& x, int& y) { int temp = x; x = y; y = temp; } int main() { cout << "Ausgangssituation: " << endl; ausgabe(); cout << "Tausch mittels std::swap(x,y): " << endl; std::swap(a,b); // Methode der Wahl aus der STL ausgabe(); cout << "Tausch mittels Referenzen: " << endl; referenzen_swap(a,b); ausgabe(); cout << "Tausch mittels Zeiger: " << endl; zeiger_swap(&a, &b); ausgabe(); cout << "Erfolgloser Tausch, da nur lokale Kopien getauscht werden:" << endl; erfolgloser_swap(a,b); ausgabe(); getch(); }
-
jetzt stoeren nur noch die funktion ausgabe() und die globalen variablen.
-
Mir hat das mit den globalen Variablen gut gefallen, da man dann den Unterschied bezüglich Speicheradresse leichter erkennt (global/lokal).
Aber das geht auch anders, man kann ja beides zeigen:#include <iostream> #include <conio.h> using std::cout; using std::endl; void ausgabe(int x, int y) { cout << &x << ": " << x << "\t " << &y << ": " << y << endl << endl; } void erfolgloser_swap(int x, int y) { cout << endl << "Kopien auf dem Stack: " << endl; ausgabe(x,y); int temp = x; x = y; y = temp; ausgabe(x,y); cout << endl; } void zeiger_swap(int* x, int* y) { int temp = *x; *x = *y; *y = temp; } void referenzen_swap(int& x, int& y) { int temp = x; x = y; y = temp; } void referenzen_swap_Variante(int& x, int& y) { x^=y; y^=x; x^=y; } int main() { //lokale Variablen int a = 1; int b = 2; cout << "Ausgangssituation: " << endl; ausgabe(a,b); cout << "Tausch mittels std::swap(x,y): " << endl; std::swap(a,b); // Methode der Wahl aus der STL ausgabe(a,b); cout << "Tausch mittels Referenzen: " << endl; referenzen_swap(a,b); ausgabe(a,b); cout << "Tausch mittels Referenzen (Variante): " << endl; referenzen_swap_Variante(a,b); ausgabe(a,b); cout << "Tausch mittels Zeiger: " << endl; zeiger_swap(&a, &b); ausgabe(a,b); cout << "Erfolgloser Tausch, da nur lokale Kopien getauscht werden:" << endl; erfolgloser_swap(a,b); ausgabe(a,b); getch(); }
referenzen_swap_Variante(...) ist ein Vorschlag von HumeSikkins (s.u.).
-
Hallo,
warum zeigt man Anfängern eigentlich nur den Dreieckstausch? Eine so schöne Situation würde ich nicht verstreichen lassen und gleich boolesche Logik (oder heißt das dann Algebra?) motivieren. Schließlich muss das sowieso *jeder* Programmierer können.
-
HumeSikkins schrieb:
Eine so schöne Situation würde ich nicht verstreichen lassen und gleich boolesche Logik (oder heißt das dann Algebra?) motivieren. Schließlich muss das sowieso *jeder* Programmierer können.
aeh... was meinst du?
-
void swap(int& a, int& b) { a^=b; b^=a; a^=b; }
-
Danke!
Sehr gute Idee. Ich habe es gleich oben in den Code editiert. Da kann es gleich jeder ausprobieren.
Jetzt fehlt eigentlich nur noch ein Geschwindigkeitsvergleich aller Methoden (da nehmen wir Assembler testweise wieder mit ins Boot!), z.B. jeweils x Mio. Tauschoperationen. Frage: was sollte bei Massenanwendung der schnellste Algo sein? Setzt ihr auf std::swap(x,y) ?Verloren! std::swap(x,y) ist hinter meiner "ach so miesen" Assemblervariante. Mit xchg war es deutlich langsamer, mov ist besser. Zur Zeitmessung bin ich wegen der Assembler-Variante auf globale Variablen umgestiegen. HumeSikkins boolsche-Tauschvariante ist leider langsamer als der Dreieckstausch. Sieht man von Assembler ab, so ist std::swap(...) vergleichbar schnell wie die eigene Zeiger- oder Referenz-Variante.
Ergebnis: (60000000 Aufrufe)
1. Assembler (die obige mov-Variante) 2.6 sec
2. std::swap(...) 2.9 sec
3. ref und zeiger (fast gleich) 2.9 sec
4. ref (boolsche Variante) // Schönheit siegt nicht immer. 3.2 sec(gerundete Mittelwerte aus Messungen)