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.
    

Anmelden zum Antworten