mit Assembler optimieren
-
Das mag sein, aber meine mit Assembler programmierte Version (die mit dem Stack arbeitet, pushed und poped) war schneller als die in C++ geschriebene rekursive Methode. Von daher erhoffe ich mir Rekursionen aus meinen Programmen (eben beim Durchgehen eines Binärbaumes) mit Assembler entfernen zu können und so einen Geschwindigkeitsvorteil zu erhalten.
-
Andreas_L schrieb:
Das mag sein, aber meine mit Assembler programmierte Version (die mit dem Stack arbeitet, pushed und poped) war schneller als die in C++ geschriebene rekursive Methode. Von daher erhoffe ich mir Rekursionen aus meinen Programmen (eben beim Durchgehen eines Binärbaumes) mit Assembler entfernen zu können und so einen Geschwindigkeitsvorteil zu erhalten.
Hast du dir mal den vom Compiler generierten Code im Disassembler angesehen?
-
@Andreas_L: Da machst Du irgendwas falsch. Versuch doch mal, Dein C/C++ Programm mit verschiedenen Optimierungsstufen zu kompilieren (wenn es denn danach noch funktioniert)
-
Hallo, also ich bin nochmal die Compiler-Einstellungen durchgegangen und hab explizit alles auf Geschwindigkeitsoptimierung und nicht Codegrößenoptimierung gesetzt.
Mit "QueryPerformanceCounter((LARGE_INTEGER*)&StartTimeNorm);" frage ich Start und Endzeit ab und berechne dann die Differenz.
Wenn ich die Summe von 1-5000 berechnen will, dann liefert diese Methode für die for-Schleife (also die Beste Möglichkeit) einen Wert von 11 (ich kenne die Einheit jetzt nicht- aber trotzdem ist ja ein quantitativer Vergleich möglich).
Meine Assembler-Methode (die mit dem Stack arbeitet) liefert einen Wert von 38 und die rekursive Variante 215. Auch wenn ich das mehrmals teste bleiben die Werte in dieser Größenordnung. D.h. für mich ist die Assembler-Methode schon ne recht gute Alternative, oder nicht?
Was mich bei der rekursiven Variante aber etwas gewundert hat ist, dass die Zeit nicht linear mit der Größenordnung ansteigt - für die Summe bis 2500 braucht sie nur einen Wert von 75 - weiß aber ehrlich gesagt nicht, warum das so ist.
Hast du dir mal den vom Compiler generierten Code im Disassembler angesehen?
Also da ich gerade erst mit Assembler anfange kenne ich mich noch nicht so gut damit aus - wisst ihr vielleicht ob Visual Studio 2008 die Möglichkeit bietet den erzeugten Code in Assembler anzuzeigen?
Vielen Dank und viele Grüße
Andreas
-
War das nicht Alt+8 ?
Aber nur im Debugger, also vorher Strg+F10 um hinzugehen.
-
cool, vielen Dank das funktioniert!!!
Für die for-Schleife ergab sich folgender Code (bei GRENZE = 5000):
int Summe = 0; 0040104B xor ebx,ebx 0040104D xor edx,edx for(int i = 1; i <= GRENZE; i++) Summe+=i; 0040104F mov eax,1 00401054 xor ecx,ecx 00401056 add ebx,eax 00401058 lea ecx,[ecx+eax+1] 0040105C lea edx,[edx+eax+2] 00401060 lea edi,[edi+eax+3] 00401064 add eax,4 00401067 cmp eax,1388h 0040106C jle TestFunc+46h (401056h) 0040106E add ecx,edx 00401070 add ecx,edi 00401072 add ebx,ecx
Um ehrlich zu sein verstehe ich den Code aber noch nicht komplett - mein größtes Problem sind diese Zeilen:
00401058 lea ecx,[ecx+eax+1] 0040105C lea edx,[edx+eax+2] 00401060 lea edi,[edi+eax+3]
Was macht er hier?
Was mich allerdings gewundert hat ist, dass er meine Assemblerbefehle ja gar nicht 1:1 umsetzt:
Pushen: //push ecx 004010AD push ecx // dec ecx 004010AE dec ecx // test ecx, 0FFFFFFFFh 004010AF test ecx,0FFFFFFFFh // jnz Pushen 004010B5 jne Pushen (4010ADh)
ich hatte jnz Pushen geschrieben und er hat jne Pushen daraus gemacht - gibts dafür einen Grund?
Viele Grüße
Andreas
-
Andreas_L schrieb:
ich hatte jnz Pushen geschrieben und er hat jne Pushen daraus gemacht - gibts dafür einen Grund?
jnz und jne sind zwei Bezeichner für den selben opcode
-
Andreas_L schrieb:
Um ehrlich zu sein verstehe ich den Code aber noch nicht komplett - mein größtes Problem sind diese Zeilen:
00401058 lea ecx,[ecx+eax+1] 0040105C lea edx,[edx+eax+2] 00401060 lea edi,[edi+eax+3]
Was macht er hier?
Da rechnet er ecx <- ecx + eax + 1. Die eckigen Klammern sind darum, weil lea eigentlich für Operationen auf Adressen gedacht ist.
Der Compiler hat da die Schleife ausgerollt, und rechnet immer 4 Werte in einem Durchlauf aus, wobei in eax immer 0, 4, 8, 12, ... addiert werden, in ecx immer 1, 5, 9, 13, etc., in edx 2, 6, 10, ... und in edi 3, 7, 11, ...
Ganz zum Schluss werden die Teilsummen dann zusammengerechnet.
Das hat zwei Vorteile (die je nach CPU unterschiedlich starke Effekte bringen)
* einmal wird die Schleife 4 mal so selten durchlaufen, d.h. es werden 4x weniger Vergleiche (cmp) und 4x weniger Sprünge (jle) ausgeführt, die ja eigentlich nichts mit der Berechnung zu tun haben.
* außerdem gibt es zwischen den Registern weniger dichte Abhängigkeiten, sodass die Pipeline besser gefüllt werden kann. Die Addition auf ecx, edx, und edi erfolgt eine gute Weile (relativ gesehen ;)) nachdem eax das letzte mal geändert wurde. Wenn du hintereinander auf das selbe Register zugreifen würdest, dann müsstest du u.U. auf das Ergebnis der vorherigen Operation warten. (Gerade dieser Effekt ist stark CPU-spezifisch)
-
cool, vielen Dank für die Erklärung. Hab die Summe mal bis 4999 laufen lassen - da konnte er diese vierer Zerlegung nicht machen und hat auch etwas länger gebraucht^^
Kann ich also lea für beliebige, längere arithmetische Rechnungen verwenden? Ist das dann auch schneller als die Berechnungen einzeln zu machen?
Viele Grüße
Andreas
-
Andreas_L schrieb:
Kann ich also lea für beliebige, längere arithmetische Rechnungen verwenden? Ist das dann auch schneller als die Berechnungen einzeln zu machen?
Das hängt von der CPU ab, hier gibt es mehr Info: http://www.agner.org/optimize/
-
Das ist wie gesagt eigentlich der Grund warum Compilercode fast immer schneller sein sollte, der Compiler weiß ganz genau was die CPU am schnellsten/effektivsten ausführen kann und spart Zeit bei Aktionen die man als Highlevel-Coder gar nicht wahrgenommen hätte.
-
Andreas_L schrieb:
Kann ich also lea für beliebige, längere arithmetische Rechnungen verwenden
Nein, beliebige gehen nicht. LEA steht für Load Effective Address, und die Rechnungen, die du da durchführen kannst, müssen eine Berechnung der Effektiven Adresse sein. Diese haben die Form
Base + Index * Scale + Displacement Base und Index sind Register. Scale ist 1, 2, 4 oder 8. Displacement ist eine Konstante.