Anfängerprobleme mit inline ASM und FPU



  • Hallo,
    ich wollte mir in ASM mal eine Berechnungschleife schreiben. Leider habe ich aber feststellen müssen, dass ab einem gewissen Zeitpunkt, ca. 7-8 Durchgänge, die Werte, welche ich mit fld lade nicht mehr stimmen. ????
    Um das Ganze zu debuggen habe ich mir eine Variable (t1) angelegt, in welche das oberste Stackelement geschrieben wird. Somit sind die Zeilen "fst t1" nur zum debuggen gedacht, da ich nicht weiß wie ich mir im VS 2003 Debugger den FPU Stack anzeigen lassen kann (ST(0) z.B. kennt er nicht).

    Der ganze ASM Bereich ist noch einmal in eine for-Schleife eingebettet, welche den Wert von c.im ändert. Es kann also auch sein, dass c.im bei dem nächsten Durchgang, um 0,0001 dekremmentiert wurde. Da die Berechnung durch die Bedingung fcom abgebrochen wurde. Startwert ist c.re = -2.0 und c.im = 1.25.

    Im Prinzip tritt der Fehler schon in den ersten Zeilen nach einigen Durchläufen auf.
    Hier:
    fld ST(0)
    Also ich nehme das oberste Element und kopiere es, sodass dieses Element nun zweimal hintereinander auf dem Satck liegt.

    Kann es sein das der FPU Stack irgendwie überläuft? Aber eigentlich liegen doch keine zusätzlichen Elemente mehr drinnen, da ich den Stack doch auch "poppe" ;-). Um das zu testen hatte ich mir auch ein Array angelegt, welches ich so gefüllt habe:

    fst [arr+0]
    fincstp
    fst [arr+4]
    ...
    fdecstp
    ...

    Die Werte lagen auch korrekt in dem Satck und es waren keine "Leichen" der vorhergehenden Durchgänge enthalten.

    ///////////////////////////////////

    Hier der komplette Code:

    int miters = 200;
    float t1;
    float diverg = 3;
    z.im = -1.0f; z.re = 0.5f;
    __asm
    {
        pushad
        xor ecx, ecx
        xor eax, eax
        mov ebx, [miters]
        fld [z.im]
        fld [z.re]
        // FPU Stack:
        // z.re, z.im
    
        calcLoop:
            // z.re = z.re*z.re - z.im*z.im + c.re;
                                // FPU Stack:
            fst t1				// Hier ist der Brekpoint im Debugger.  t1 ist hier noch OK, auch nach mehreren Iterationen 
            fld ST(0)           // z.re, z.re, z.im
            fst t1				// Nach einigen Durchläufen steht in t1 -1,#IND oder so ähnlich.
            fmul ST(0), ST(0)   // z.re^2, z.re, z.im
            fld ST(2)           // z.im, z.re^2, z.re, z.im
            fmul ST(0), ST(0)   // z.im^2, z.re^2, z.re, z.im
            fsub                // z.re^2-z.im^2, z.re, z.im
            fadd [c.re]         // z.re^2-z.im^2+c.re, z.re, z.im
            fst t1
    
            // z.im = z.re*z.im + z.re*z.im + c.im;
            fld ST(2)           // z.im, z.re^2-z.im^2+c.re, z.re, z.im
            fmul ST(0), ST(2)   // z.im*z.re, z.re^2-z.im^2+c.re, z.re, z.im
            fadd ST(0), ST(0)   // z.im*z.re+z.im*z.re, z.re^2-z.im^2+c.re, z.re, z.im
            fadd [c.im]         // z.im*z.re+z.im*z.re+c.im, z.re^2-z.im^2+c.re, z.re, z.im
            fst t1
    
            // obersten beiden Elemente kopieren und letzten beiden Stackelemente löschen
            fst ST(3)           // z.im*z.re+z.im*z.re+c.im, z.re^2-z.im^2+c.re, z.re, z.im*z.re+z.im*z.re+c.im
            fxch                // z.re^2-z.im^2+c.re, z.im*z.re+z.im*z.re+c.im, z.re, z.im*z.re+z.im*z.re+c.im
            fst ST(2)           // z.re^2-z.im^2+c.re, z.im*z.re+z.im*z.re+c.im, z.re^2-z.im^2+c.re, z.im*z.re+z.im*z.re+c.im
    
            // (z.re*z.re + z.im*z.im)^0.5
            fmul ST(0), ST(0)   // (z.re^2-z.im^2+c.re)^2, z.im*z.re+z.im*z.re+c.im, z.re^2-z.im^2+c.re, z.im*z.re+z.im*z.re+c.im
            fxch                // z.im*z.re+z.im*z.re+c.im, (z.re^2-z.im^2+c.re)^2, z.re^2-z.im^2+c.re, z.im*z.re+z.im*z.re+c.im
            fmul ST(0), ST(0)   // (z.im*z.re+z.im*z.re+c.im)^2, (z.re^2-z.im^2+c.re)^2, z.re^2-z.im^2+c.re, z.im*z.re+z.im*z.re+c.im
            fadd                // (z.im*z.re+z.im*z.re+c.im)^2+(z.re^2-z.im^2+c.re)^2, z.re^2-z.im^2+c.re, z.im*z.re+z.im*z.re+c.im
            fsqrt               // ((z.im*z.re+z.im*z.re+c.im)^2+(z.re^2-z.im^2+c.re)^2)^0.5, z.re^2-z.im^2+c.re, z.im*z.re+z.im*z.re+c.im
    
            fld [diverg]        // diverg, ((z.im*z.re+z.im*z.re+c.im)^2+(z.re^2-z.im^2+c.re)^2)^0.5, z.re^2-z.im^2+c.re, z.im*z.re+z.im*z.re+c.im
            fcomp               // diverg fcom magn mit pop: ((z.im*z.re+z.im*z.re+c.im)^2+(z.re^2-z.im^2+c.re)^2)^0.5, z.re^2-z.im^2+c.re, z.im*z.re+z.im*z.re+c.im
            fstsw ax            // schreibe fpu flags in ax
            sahf                // schreibe ax in cpu flags
            inc ecx             // ecx++
        jc theEnd               // wenn CF=1 -> ST(1) < ST(0), dann stoppe
            fst t1
            fxch                // z.re^2-z.im^2+c.re, ((z.im*z.re+z.im*z.re+c.im)^2+(z.re^2-z.im^2+c.re)^2)^0.5, z.im*z.re+z.im*z.re+c.im
            fst t1
            fstp ST(1)          // z.re^2-z.im^2+c.re, z.im*z.re+z.im*z.re+c.im == z.re, z.im
            fst t1
            cmp ecx, ebx
        jc calcLoop             // wenn ecx < ebx, go on
    
        // Werte aus Registern in Speicher schreiben
        theEnd:                 //
            mov [iter], ecx
            fstp [magn]         // z.re^2-z.im^2+c.re, z.im*z.re+z.im*z.re+c.im == z.re, z.im
    
            popad
    }
    

    ////////////////////////////////////////

    Danke das Du dir die Zeit genommen hast den Mist durchzulesen. 🙂
    Ich bin echt verzweifelt, weil ich keine Fehlerursache ausmachen kann.
    Schon mal vielen dank im voraus für die Hilfe.



  • Ich hab mir jetzt nicht alles angeschaut, aber der VS Debugger sollte eigentlich im Fenster mit den Registerinhalten den FPU Stack anzeigen (muss man glaub ich extra anhakeln im Kontextmenü).


  • Mod

    ich hab das jetzt nicht debugged, nur ein paar kommentare angefügt und die debugger hilfen entfernt (siehe ringdings hinweis; du kannst dir damit auch xmm und mmx register anzeigen lassen, je nach extension im entsprechenden format), vielleicht hilft es:

    int miters = 200;
    float t1;
    float diverg = 3;
    z.im = -1.0f; z.re = 0.5f;
    __asm
    {
        pushad                  // eigentlich brauchst du nur zwei register
        xor ecx, ecx            // eax, ecx, edx dürfen frei verändert werden 
        xor eax, eax            // der compiler geht immer davon aus, dass sie sich verändern
        mov ebx, [miters]       // also hier besser z.b. edx verwenden, dann wird das pusha/popa überflüssig
        fld [z.im]
        fld [z.re]
        // FPU Stack:
        // z.re, z.im
    
        calcLoop:
            // z.re = z.re*z.re - z.im*z.im + c.re;
            fld ST(0)
            fmul ST(0), ST(0)
            fld ST(2)
            fmul ST(0), ST(0)
            fsub                // soll sicher fsubp sein, eigentlich dürfte der compiler das so gar nicht übersetzen
                                // ich persönlich mag die parameterlose form nicht besonders, da die semantik wechselt von instruktion zu instruktion
                                // also besser fsubp st(1),st(0)                             ,  stack jetzt z.re^2-z.im^2, z.re, z.im
            fadd [c.re]
    
            // z.im = z.re*z.im + z.re*z.im + c.im;
            fld ST(2)
            fmul ST(0), ST(2)
            fadd ST(0), ST(0)
            fadd [c.im]         // keine einwände hier
    
            // obersten beiden Elemente kopieren und letzten beiden Stackelemente löschen
            fst ST(3)           // z.im*z.re+z.im*z.re+c.im, z.re^2-z.im^2+c.re, z.re, z.im*z.re+z.im*z.re+c.im
            fxch                // z.re^2-z.im^2+c.re, z.im*z.re+z.im*z.re+c.im, z.re, z.im*z.re+z.im*z.re+c.im
            fst ST(2)           // z.re^2-z.im^2+c.re, z.im*z.re+z.im*z.re+c.im, z.re^2-z.im^2+c.re, z.im*z.re+z.im*z.re+c.im
    
            // (z.re*z.re + z.im*z.im)^0.5
            fmul ST(0), ST(0)   // (z.re^2-z.im^2+c.re)^2, z.im*z.re+z.im*z.re+c.im, z.re^2-z.im^2+c.re, z.im*z.re+z.im*z.re+c.im
            fxch                // z.im*z.re+z.im*z.re+c.im, (z.re^2-z.im^2+c.re)^2, z.re^2-z.im^2+c.re, z.im*z.re+z.im*z.re+c.im
            fmul ST(0), ST(0)   // (z.im*z.re+z.im*z.re+c.im)^2, (z.re^2-z.im^2+c.re)^2, z.re^2-z.im^2+c.re, z.im*z.re+z.im*z.re+c.im
            fadd                // dito, das p postfix fehlt, also faddp bzw. faddp st(1), st(0)
            fsqrt               // ((z.im*z.re+z.im*z.re+c.im)^2+(z.re^2-z.im^2+c.re)^2)^0.5, z.re^2-z.im^2+c.re, z.im*z.re+z.im*z.re+c.im
                                // die wurzel wird nur zum vergleich benötigt, also warum nicht die summe der quadrate mit dem quadrat des divergenzradius vergleichen?
            fld [diverg]        // kann man direkt auch mit fcom diverg haben, wenn diverg nicht gerade im extended format ist
            fcomp               // der vergleich ist dann nat. andersherum
                                // noch besser ist, diesen wert noch vor der schleife zu laden oder zu berechnen
                                // ist ja i.d.R. 2 bzw 4 wenn man quadriert
            fstsw ax            // schreibe fpu flags in ax - fnstsw ist günstiger, wenn alles richtig und mit gültigen operanden, kann sowieso keine exception entstehen
            sahf                // schreibe ax in cpu flags
            inc ecx             // ecx++
        jc theEnd               // wenn CF=1 -> ST(1) < ST(0), dann stoppe - wird zu jnc wenn du den direkten vergleich benutzt
            fxch                // z.re^2-z.im^2+c.re, ((z.im*z.re+z.im*z.re+c.im)^2+(z.re^2-z.im^2+c.re)^2)^0.5, z.im*z.re+z.im*z.re+c.im
            fstp ST(1)          // z.re^2-z.im^2+c.re, z.im*z.re+z.im*z.re+c.im == z.re, z.im
                                // das ist umständlich, besser ist fstp st(0)
            cmp ecx, ebx        // eleganter ist ebx(bzw. das register, das mit miters initialisiert wurde) direkt zum zählen zu verwenden: dec ebx
                                // und dann direkter sprung mit jnz; zum schluss den inhalt nur von miters abziehen
        jc calcLoop             // wenn ecx < ebx, go on
    
        // Werte aus Registern in Speicher schreiben
        theEnd:                 //
            mov [iter], ecx
            fstp [magn]         // z.re^2-z.im^2+c.re, z.im*z.re+z.im*z.re+c.im == z.re, z.im
                                // hier fehlen noch zwei fstp st(0) - den stack immer leer verlassen
                                // (bzw. mit returnwert in st(0) bei naked funktionen)
            popad
    }
    

    ich hoffe ich habe alles gefunden, auf jeden fall war dein problem tatsächlich stacküberlauf, wenn ich mit meiner vermutung bzgl. das parametelosen fadd/fsub richtig liege



  • 👍 👍 👍 👍 👍 👍 👍 👍 👍
    Ich danke euch beiden erst mal unheimlich! Ich hab z.Z. keine Zeit ;), aber ich werd mir das Ganze heut Abend mal in Ruhe anschauen.

    Also THX noch mal !!



  • Noch ne kurze Frage.
    Ich habe mir gerade den Debugger mit einem anderen Proggi angeschaut, da ich auf Arbeit bin und mein Projekt hier nicht komplett mit habe. Ich kann zwar unter dem Menü: Debugger -> Fenster -> Register auswählen und bekomme die Register angezeigt, aber im Kontextmenü kann ich nicht den FPU Stack auswählen, sondern nur die CPU Flags. ???? Oder kann es sein, dass ich die nur angezeigt bekomme, wenn ich diese explizit nutze. Wie gesagt, ich habs jetzt nicht auf die Schnelle hier mit meinem Projekt probieren können.


  • Mod

    es gibt noch 1 oder 2 punkte, um das ganze zu optimieren: es fällt ja z.b. auf, dass die quadrate von real- und imaginärteil 2mal berechnet werden. einaml beim divergenztest und zum zweiten mal dann für den nächsten iterationsschritt. da kann man sicher noch sparen. eigentlich solltest du die FPU immer angezeigt bekommen. andere registersätze sind nat. nur verfügbar, wenn die maschine, auf der du debuggst, diese auch hat.



  • camper schrieb:

    es gibt noch 1 oder 2 punkte, um das ganze zu optimieren: es fällt ja z.b. auf, dass die quadrate von real- und imaginärteil 2mal berechnet werden. einaml beim divergenztest und zum zweiten mal dann für den nächsten iterationsschritt. da kann man sicher noch sparen. eigentlich solltest du die FPU immer angezeigt bekommen. andere registersätze sind nat. nur verfügbar, wenn die maschine, auf der du debuggst, diese auch hat.

    Also, danke noch mal für eure Hilfe. Zu hause habe ich dann auch die ganze "Pracht" 😉 der Register anschalten können. Ich hatte es hier auch nur mit einem C# Projekt getestet.
    Mit dem Quadrat hast Du natürlich auch Recht! Aber man möge mir dies verzeihen 😉 ,ich bin nur ein einfacher ASM Anfänger.

    Ich habe den Code nach deinen Vorschlägen noch mal umgebaut und der sieht jetzt so aus:

    __asm
    			{
    				xor eax, eax
    				mov ecx, [miters]
    				mov [iter], ecx		// FPU Stack:
    
    				fld [z.im]			// z.im
                    fld [z.re]			// z.re, z.im
                    fld ST(0)           // z.re, z.re, z.im
                    fmul ST(0), ST(0)   // z.re^2, z.re, z.im
                    fld ST(2)           // z.im, z.re^2, z.re, z.im
                    fmul ST(0), ST(0)   // z.im^2, z.re^2, z.re, z.im
    
    				calcLoop:
    					// z.re = z.re*z.re - z.im*z.im + c.re;
    										// FPU Stack:                   
                        fsubp ST(1), ST(0)	// z.re^2-z.im^2, z.re, z.im
                        fadd [c.re]			// z.re^2-z.im^2+c.re, z.re, z.im
    
                		// z.im = z.re*z.im + z.re*z.im + c.im;
                        fld ST(2)           // z.im, z.re^2-z.im^2+c.re, z.re, z.im
                        fmul ST(0), ST(2)	// z.im*z.re, z.re^2-z.im^2+c.re, z.re, z.im
                        fadd ST(0), ST(0)	// z.im*z.re+z.im*z.re, z.re^2-z.im^2+c.re, z.re, z.im
                        fadd [c.im]			// z.im*z.re+z.im*z.re+c.im, z.re^2-z.im^2+c.re, z.re, z.im
    
    					// obersten beiden Elemente kopieren und letzten beiden Stackelemente löschen
    					fst ST(3)			// z.im*z.re+z.im*z.re+c.im, z.re^2-z.im^2+c.re, z.re, z.im*z.re+z.im*z.re+c.im
    					fxch				// z.re^2-z.im^2+c.re, z.im*z.re+z.im*z.re+c.im, z.re, z.im*z.re+z.im*z.re+c.im
    					fst ST(2)			// z.re^2-z.im^2+c.re, z.im*z.re+z.im*z.re+c.im, z.re^2-z.im^2+c.re, z.im*z.re+z.im*z.re+c.im
    
                        // (z.re*z.re + z.im*z.im)
                        fmul ST(0), ST(0)	// (z.re^2-z.im^2+c.re)^2, z.im*z.re+z.im*z.re+c.im, z.re^2-z.im^2+c.re, z.im*z.re+z.im*z.re+c.im
                        fxch				// z.im*z.re+z.im*z.re+c.im, (z.re^2-z.im^2+c.re)^2, z.re^2-z.im^2+c.re, z.im*z.re+z.im*z.re+c.im
    					fmul ST(0), ST(0)	// (z.im*z.re+z.im*z.re+c.im)^2, (z.re^2-z.im^2+c.re)^2, z.re^2-z.im^2+c.re, z.im*z.re+z.im*z.re+c.im
                        fst ST(1)			// (z.re^2-z.im^2+c.re)^2, (z.im*z.re+z.im*z.re+c.im)^2, (z.re^2-z.im^2+c.re)^2, z.re^2-z.im^2+c.re, z.im*z.re+z.im*z.re+c.im
                        fst ST(1)			// (z.im*z.re+z.im*z.re+c.im)^2, (z.re^2-z.im^2+c.re)^2, (z.im*z.re+z.im*z.re+c.im)^2, (z.re^2-z.im^2+c.re)^2, z.re^2-z.im^2+c.re, z.im*z.re+z.im*z.re+c.im
                        faddp ST(1), ST(0)	// (z.im*z.re+z.im*z.re+c.im)^2+(z.re^2-z.im^2+c.re)^2, (z.im*z.re+z.im*z.re+c.im)^2, (z.re^2-z.im^2+c.re)^2, z.re^2-z.im^2+c.re, z.im*z.re+z.im*z.re+c.im
    
                        fcom [divergSq]		// magnitudeSq fcom divergSq
                        fnstsw ax			// schreibe fpu flags in ax
                        sahf				// schreibe ax in cpu flags
                    jnc theEnd				// wenn CF=0 -> ST(0) < divergSq, dann stoppe
    					fstp ST(0)			// (z.im*z.re+z.im*z.re+c.im)^2, (z.re^2-z.im^2+c.re)^2, z.re^2-z.im^2+c.re, z.im*z.re+z.im*z.re+c.im
    					dec ecx				// ecx--
    				jnz calcLoop			// wenn ecx > 0, go on
    
    				// Werte aus Registern in Speicher schreiben und Stack aufräumen
                    theEnd:
                    	sub [iter], ecx
                    	fstp [magn]			// (z.im*z.re+z.im*z.re+c.im)^2, (z.re^2-z.im^2+c.re)^2, z.re^2-z.im^2+c.re, z.im*z.re+z.im*z.re+c.im
                    	fstp ST(0)			// (z.re^2-z.im^2+c.re)^2, z.re^2-z.im^2+c.re, z.im*z.re+z.im*z.re+c.im
    					fstp ST(0)			// z.re^2-z.im^2+c.re, z.im*z.re+z.im*z.re+c.im
    					fstp ST(0)			// z.im*z.re+z.im*z.re+c.im
    					fstp ST(0)			// clear!
    			}
    

    Also, vielen vielen Dank noch mal! Das hätte ich so alleine nicht hinbekommen.



  • Noch ein klitzekleine Frage. 😉

    Und zwar ist die ASM Routine gleich schnell wie das C-Derivat. 😞 Naja, wenigstens ist sie nicht langsamer. :p
    Der "Flaschenhals" des Ganzen scheint die Win32 API SetPixel Funktion zu sein, welche sich an die ASM Routine anschließt. Diese Funktion verbraucht über 3/4 der Gesamtlaufzeit. Deshalb würde ich gerne diese Funktion durch eine Eigene ersetzen. Die Win32 Funktion sieht ja so aus: SetPixel(Handle auf DC, x, y, Farbe). Leider weiß ich nicht genau wie der Speicher dort aufgebaut ist. Das Ganze wird ja nicht direkt in den GraKa Speicher geschrieben, sondern in ein MemDC. Aber das müßte ja eigentl. auch egal sein. Jedenfalls ist ein Handle auf ein DC ja nur ein Zeiger auf den Speicher, wo die Farbinfos liegen, richtig? Ich würde halt gerne mit "rep stosX" den Speicher über ASM füllen. Aber ich habe keine Ahnung in welcher Form dieser aufgebaut. Also, die Pixel von links nach rechts , dann die nächste Zeile..., oder von oben nach unten, dann die nächste Spalte ... Welche Farbtiefe, also wie breit der Speicherbereich für ein Pixel ist. Word(2^32 Farben), Byte?? ÄIch erzeuge mir den memDC über CreateCompatibleDC(...), somit ist der memDC genauso aufgebaut wie der GraKa Speicher oder?
    Und ob sich es überhaupt lohnt dies mit "rep stos" zu machen?

    Wenn die Frage hier deplatziert ist, teilt mir das bitte mit und ich werde es mal bei den Win32 API Leuten 😃 posten.

    Danke im voraus!


  • Mod

    nach CreateCompatibleBitmap musst du glaub ich noch den bitmapinfoheader ausfüllen, dann kommt CreateDIBSection und SelectObject; bitmaps sind dann zeile für zeile organisiert, wobei jede zeile auf 4 byte ausgerichtet ist; siese info ist aber mit vorsicht zu geniessen, besser erst noch ins MSDN schauen

    auf jeden fall ist die idee, zunächst ein bitmap zu füllen, eine sehr gute. und wenn du mutig bist, machst du alles (einschliesslich der winapi aufrufe) per inline assembler 😉

    im (z.re*z.re + z.im*z.im) teil ist dir glaub ich ein fehler unterlaufen, die beiden fst st(1) verstehe ich nicht

    man kann die schleife noch etwas weiter verbessern, divergSq und c sind ja invariant (zumindest per pixel, evtl. auch im gesamten bild, je nachdem, welche menge zu zeichnest), also ist es sinnvoll, diese bereits vor der schleife zu laden, das spart speicherzugriffe und umwandlungskosten (die fpu wandelt ja alles ins 80bit extended format um). dann kann man fcom auch wieder durch fcomp ersetzen, das spart dann ein fstp ein, ausserhalb der schleife muss man die addition dann eben nochmal ausführen, das ist ja vertretbar. evtl. könnte man es auch so machen, dass man re^2 direkt mit der summe überschreibt (also noch ein fxch einfügen), das fcom belässt, aber das fstp entfernt, und dann im nächsten durchlauf, den imaginärteil zweimal abzieht - das spart einmal fld/fstp, allerdings kommt eine zusätzliche subtraktion ins spiel, das muss man also abwägen (bzw. ausmessen - das ist immer das beste )
    3. generell lässt sich das ganze beschleunigen, wenn du bereit bist, genauigkeit zu opfern. also per _controlfp - inwieweit das machbar ist, hängt nat. von den voraussetzungen ab. eine reduzierung auf double (53bits) sollte aber meistens vertretbar sein. (diese massnmahme kann man im übrigen auch zusammen mit ganz normalen math operation benutzen - dort hilft es auch). 24bits für floats ist optimal, wenn nur geschwindigkeit gefragt ist.
    4. da du ja jetzt die quadrate nicht mehr doppelt berechnest, kannst du noch mehr sparen - die nicht quadrierten real/imaginärwerte brauchst du ja nur noch, um das produkt zu berechen, da kannst aber genausogut gleich nur das produkt speichern. (letzlich führst du die hälfte der berechnung der nächsten iteration aus, bevor die abbruchbedingung getestet wurde; diese form der optimierung kommt bei dieser art von problemen oft vor, denn man kann dann häufig einige instruktionen, die nur daten hin und her schieben, einsparen).



  • Mit DIB section usw. geht's natürlich, aber am einfachsten ist es wohl mit SetDIBitsToDevice. Da rechnest du z.B. eine ganze Zeile ins RAM und schreibst die dann mit einem einzigen Call auf den Bildschirm.



  • Danke für die Korrektur des Fehlers und die Hinweise. Echt stark!

    Hier der überarbeitete Code:

    __asm
    			{
    				xor eax, eax
    				mov ecx, [miters]
    				mov [iter], ecx		// FPU Stack:
    
    				fld [z.im]			// z.im
    				fmul [z.re]			// z.im*z.re
    				fld [z.re]			// z.re, z.im*z.re
    				fmul ST(0), ST(0)	// z.re^2, z.im*z.re
    				fld [z.im]			// z.im, z.re^2, z.im*z.re
    				fmul ST(0), ST(0)	// z.im^2, z.re^2, z.im*z.re
    
    				calcLoop:
    					// z.re = z.re*z.re - z.im*z.im + c.re;
    										// FPU Stack:
                        fsubp ST(1), ST(0)	// z.re^2-z.im^2, z.im*z.re
                        fadd [c.re]			// z.re^2-z.im^2+c.re, z.im*z.re
    
                		// z.im = z.re*z.im + z.re*z.im + c.im;
                        fxch ST(1)			// z.im*z.re, z.re^2-z.im^2+c.re
                        fadd ST(0), ST(0)	// z.im*z.re+z.im*z.re, z.re^2-z.im^2+c.re
                        fadd [c.im]			// z.im*z.re+z.im*z.re+c.im, z.re^2-z.im^2+c.re                
    
    					fld ST(1)			// z.re^2-z.im^2+c.re, z.im*z.re+z.im*z.re+c.im, z.re^2-z.im^2+c.re
    					fld ST(1)			// z.im*z.re+z.im*z.re+c.im, z.re^2-z.im^2+c.re, z.im*z.re+z.im*z.re+c.im, z.re^2-z.im^2+c.re
    					fmul ST(3), ST(0)	// z.im*z.re+z.im*z.re+c.im, z.re^2-z.im^2+c.re, z.im*z.re+z.im*z.re+c.im, z.re^2-z.im^2+c.re * z.im*z.re+z.im*z.re+c.im
    
                        // (z.re*z.re + z.im*z.im)
                        fmulp ST(2), ST(0)	// z.re^2-z.im^2+c.re, (z.im*z.re+z.im*z.re+c.im)^2, z.re^2-z.im^2+c.re * z.im*z.re+z.im*z.re+c.im
                        fmul ST(0), ST(0)	// (z.re^2-z.im^2+c.re)^2, (z.im*z.re+z.im*z.re+c.im)^2, z.re^2-z.im^2+c.re * z.im*z.re+z.im*z.re+c.im
                        fxch				// (z.im*z.re+z.im*z.re+c.im)^2, (z.re^2-z.im^2+c.re)^2, z.re^2-z.im^2+c.re * z.im*z.re+z.im*z.re+c.im
                        fst ST(1)			// (z.re^2-z.im^2+c.re)^2, (z.im*z.re+z.im*z.re+c.im)^2, (z.re^2-z.im^2+c.re)^2, z.re^2-z.im^2+c.re * z.im*z.re+z.im*z.re+c.im
                        fst ST(1)			// (z.im*z.re+z.im*z.re+c.im)^2, (z.re^2-z.im^2+c.re)^2, (z.im*z.re+z.im*z.re+c.im)^2, (z.re^2-z.im^2+c.re)^2, z.re^2-z.im^2+c.re * z.im*z.re+z.im*z.re+c.im
                      	faddp ST(1), ST(0)	// (z.re^2-z.im^2+c.re)^2+(z.im*z.re+z.im*z.re+c.im)^2, (z.im*z.re+z.im*z.re+c.im)^2, (z.re^2-z.im^2+c.re)^2, z.re^2-z.im^2+c.re * z.im*z.re+z.im*z.re+c.im
    
                        fcom [divergSq]		// magnitudeSq fcom divergSq
                        fnstsw ax			// schreibe fpu flags in ax
                        sahf				// schreibe ax in cpu flags
                    jnc theEnd				// wenn CF=0 -> ST(0) < divergSq, dann stoppe
    					fstp ST(0)			// (z.im*z.re+z.im*z.re+c.im)^2, (z.re^2-z.im^2+c.re)^2, z.re^2-z.im^2+c.re * z.im*z.re+z.im*z.re+c.im
    					dec ecx				// ecx--
    				jnz calcLoop			// wenn ecx > 0, go on
    
    				// Werte aus Registern in Speicher schreiben und Stack aufräumen
                    theEnd:
                    	sub [iter], ecx
                    	fstp [magn]			// (z.im*z.re+z.im*z.re+c.im)^2, (z.re^2-z.im^2+c.re)^2, z.re^2-z.im^2+c.re * z.im*z.re+z.im*z.re+c.im
                    	fstp ST(0)			// (z.re^2-z.im^2+c.re)^2, z.re^2-z.im^2+c.re * z.im*z.re+z.im*z.re+c.im
    					fstp ST(0)			// z.re^2-z.im^2+c.re * z.im*z.re+z.im*z.re+c.im
    					fstp ST(0)			// clear!
    			}
    

    Besser so? 🙂

    Noch ne Frage zu dem _controlfp. Ich habe mal in die MSDN geschaut. Wenn ich diesen Aufruf hier mache: "_control87( _PC_24, MCW_PC )", dann setze ich die Genauigkeit auf 24 Bit herunter, richtig? Gilt dies solange bis ich es wieder auf einen anderen Wert setze / das Proggi beende?

    Ich habe in dem Referenzteil in meinem ASM Buch von Oliver Müller gelesen, dass es angeblich genauso lange dauert, Operationen wie fadd z.B direkt mit Speicherinhalten zu machen oder mit dem Stack. Mir scheint das aber auch nicht so recht einzuleuchten. Ich werde die 3 Varis deshalb: diverg, c.im, c.re mit in den Stack schmeißen.

    Also meinst Du, dass ich das Handle der Bitmap zum zeichnen benutzen soll und nicht das Handle des memDc? Müsste doch eigentlich egal sein oder? Denn schließlich wähle ich ja die Bmp vorher in den memDC ein und somit zeigt der ja auf den gleichen Bereich oder versteh ich da etwas falsch?

    Also, danke noch mal.



  • Wenn du bei jedem Pixel einen GDI-Aufruf machst, ist es ziemlich egal, wie schnell deine Berechnung ist. Daher musst du mit einer Bitmap arbeiten.

    Die FPU-Genauigkeit bleibt bis zum nächsten _controlfp oder bis zum Ende des Programms so eingestellt.

    Es mag sein, dass es auf manchen CPUs keinen Geschwindigkeitsnachteil bringt, aus dem Speicher zu laden. Darauf verlassen würde ich mich aber nicht.


  • Mod

    Besser so? 🙂

    das problem mit den fehlerhaften fst st(1) hast du noch nicht beseitigt. ich glaube dort müsste ein fld stehen, allerdings ist es schwierig, deinen kommentaren zu folgen (den stack zustand zu dokumentieren ist sehr gut) - ich schlage vor die ausdrücke zu vereinfachen: z. wird weggelassen, also nur re bzw. im; bei quadraten entfällt das ^ und dann folgende ausdrücke, sobald sie entstehen:
    nre = re2-im2+c.re (für neues oder next re)
    nim_2 = re*im
    nim = 2*re*im+c.im
    so wäre der zustand des stacks viel leichter zu überschauen.

    Noch ne Frage zu dem _controlfp. Ich habe mal in die MSDN geschaut. Wenn ich diesen Aufruf hier mache: "_control87( _PC_24, MCW_PC )", dann setze ich die Genauigkeit auf 24 Bit herunter, richtig? Gilt dies solange bis ich es wieder auf einen anderen Wert setze / das Proggi beende?

    prinzipiell bleibt das controlwort bestehen, solange es nicht überschrieben wird oder ein finit erfolgt. theoretisch sollte eine funktion, die das control word ändert, das auch dokumentieren, mit den normalen math routinen gibt es afaik jedenfalls keine probleme, es ist also eine permanente änderung.

    Ich habe in dem Referenzteil in meinem ASM Buch von Oliver Müller gelesen, dass es angeblich genauso lange dauert, Operationen wie fadd z.B direkt mit Speicherinhalten zu machen oder mit dem Stack. Mir scheint das aber auch nicht so recht einzuleuchten. Ich werde die 3 Varis deshalb: diverg, c.im, c.re mit in den Stack schmeißen.

    die aussage ist insofern richtig, als die zusätzliche latenz durch speicherzugriffe häufig durch die pipeline verdeckt wird, in schleifen, insbesondere wenn sie kurz sind, muss das aber nicht unbedingt gelten. jedenfalls mal ein zitat:
    IA-32 Intel Architecture Optimization Reference Manual:
    Floating-point, MMX technology, Streaming SIMD Extensions and Streaming SIMD Extension 2 instructions with load operations require 6 more clocks in latency than the register-only version of the instructions, but throughput remains the same.
    das bezieht sich auf den P4; bei athlons sind es 2 takte (siehe Software Optimization Guide for AMD Athlon 64 and AMD Opteron Processors) - beides übrigens sehr empfelenswerte lektüre. bei komplexen operationen, die gerne den ganzen stack benutzen würden, kann es tatsächlich eine gute idee sein, einzelne konstanten direkt aus dem speicher zu holen, wenn man dadurch die registerlast reduzieren kann.

    Also meinst Du, dass ich das Handle der Bitmap zum zeichnen benutzen soll und nicht das Handle des memDc? Müsste doch eigentlich egal sein oder? Denn schließlich wähle ich ja die Bmp vorher in den memDC ein und somit zeigt der ja auf den gleichen Bereich oder versteh ich da etwas falsch?

    Da fragst du am besten einen von den freundlichen winapi experten 🙂

    wenn sich diverSq in einem register befindet, kannst du auch fcomi bzw. fcomip einsetzen (setzt mindestens P2 bzw. PPro voraus), damit entfällt dann das fnstsw und sahf - es ist allerdings sinnvoll, ein paar instruktionen for dem jnc einzuschieben, um fehlerhafte sprungvorhersagen zu minimieren - dec ecx z.b. ist hier ungefährlich, da dec das carry flag nicht verändert. auch zwischen den beiden sprungbefehlen sollten sich weitere befehle befinden, da der throughput von sprüngen nicht sehr hoch ist.



  • Ich muss mich immer wieder bei euch bedanken, wie schnell und kompetent ihr mir hier helft! Super! Ich hab echt ne ganze Menge gelernt. Big THX !!!

    Ich hatte heute in der Mittagspause schnell ein bisschen an dem Code bebastelt, da hatte ich den Fehler mit dem fst wohl übersehen. Ist natürlich unsinnig, ich will ja die Daten kopieren und keine überschreiben.

    Ansonsten sieht der Code jetzt wie folgt aus:

    Aber die Laufzeit ist auf die Millisekunde genau gleich geblieben. 😞 Deswegen werde ich mich wohl morgen um die SetPixel Funktion kümmern. Aber für heute ist erst mal Schluss. 🙂 Schließlich ist ja Fr. 😃

    Also, schönen Abend noch.

    camper schrieb:

    ...beides übrigens sehr empfelenswerte lektüre...

    Das von Intel hab ich mir schon als CD geordert. 🙂 Das von AMD muss ich mir auch noch besorgen, wenn ich mal die Muse habe mich auf der Homepage umzuschauen. 😮

    wenn sich diverSq in einem register befindet, kannst du auch fcomi bzw. fcomip einsetzen (setzt mindestens P2 bzw. PPro voraus), damit entfällt dann das fnstsw und sahf - es ist allerdings sinnvoll, ein paar instruktionen for dem jnc einzuschieben, um fehlerhafte sprungvorhersagen zu minimieren - dec ecx z.b. ist hier ungefährlich, da dec das carry flag nicht verändert. auch zwischen den beiden sprungbefehlen sollten sich weitere befehle befinden, da der throughput von sprüngen nicht sehr hoch ist.

    Das versteh ich nicht ganz. Was sollte ich denn für Befehle dort dazwischen "rein packen"? 😕

    Ich muss mich immer wieder bei euch bedanken, wie schnell und kompetent ihr mir hier helft! Super! Ich hab echt ne ganze Menge gelernt. Big THX !!!

    Ich hatte heute in der Mittagspause schnell ein bisschen an dem Code bebastelt, da hatte ich den Fehler mit dem fst wohl übersehen. Ist natürlich unsinnig, ich will ja die Daten kopieren und keine überschreiben.

    Ansonsten sieht der Code jetzt wie folgt aus:

    __asm
    {
        xor eax, eax
        mov ecx, [miters]
        mov [iter], ecx     // FPU Stack:
    
        fld [c.im]          // c.im
        fld [c.re]          // c.re, c.im
        fld [divergSq]      // div, c.re, c.im
        fld [z.im]          // z.im, div, c.re, c.im
        fmul [z.re]         // z.im*z.re, div, c.re, c.im
        fld [z.re]          // z.re, z.im*z.re, div, c.re, c.im
        fmul ST(0), ST(0)   // z.re2, z.im*z.re, div, c.re, c.im
        fld [z.im]          // z.im, z.re2, z.im*z.re, div, c.re, c.im
        fmul ST(0), ST(0)   // z.im2, z.re2, z.im*z.re, div, c.re, c.im
    
        calcLoop:
            // z.re = z.re*z.re - z.im*z.im + c.re;
                                // FPU Stack:
            fsubp ST(1), ST(0)  // z.re2-z.im2, z.im*z.re, div, c.re, c.im
            fadd ST(0), ST(3)   // z.re2-z.im2+c.re = nre, z.im*z.re, div, c.re, c.im
    
            // z.im = z.re*z.im + z.re*z.im + c.im;
            fxch ST(1)          // z.im*z.re, nre, div, c.re, c.im
            fadd ST(0), ST(0)   // z.im*z.re+z.im*z.re, nre, div, c.re, c.im
            fadd ST(0), ST(4)   // z.im*z.re+z.im*z.re+c.im = nim, nre, div, c.re, c.im   
    
            // nre * nim
            fld ST(1)           // nre, nim, nre, div, c.re, c.im
            fld ST(1)           // nim, nre, nim, nre, div, c.re, c.im
            fmul ST(3), ST(0)   // nim, nre, nim, nre * nim, div, c.re, c.im
    
            // nz.re*nz.re , nz.im*nz.im
            fmulp ST(2), ST(0)  // nre, nim2, nre * nim, div, c.re, c.im
            fmul ST(0), ST(0)   // nre2, nim2, nre * nim, div, c.re, c.im
    
            // (nz.re2 + nz.im2)
            fxch ST(1)          // nim2, nre2, nre * nim, div, c.re, c.im
            fld ST(1)           // nre2, nim2, nre2, nre * nim, div, c.re, c.im
            fld ST(1)           // nim2, nre2, nim2, nre2, nre * nim, div, c.re, c.im
            faddp ST(1), ST(0)  // nre2+nim2, nim2, nre2, nre * nim, div, c.re, c.im
    
            fcom ST(4)          // magnitudeSq fcom divergSq
            fnstsw ax           // schreibe fpu flags in ax
            sahf                // schreibe ax in cpu flags
            dec ecx             // ecx--
        jnc theEnd              // wenn CF=0 -> ST(0) < divergSq, dann stoppe
            fstp ST(0)          // nim2, nre2, nre * nim, div, c.re, c.im
        jnz calcLoop            // wenn ecx > 0, go on
    
        // Werte aus Registern in Speicher schreiben und Stack aufräumen
        theEnd:
            sub [iter], ecx
            fstp [magn]         // nim2, nre2, nre * nim, div, c.re, c.im
            fstp ST(0)          // nre2, nre * nim, div, c.re, c.im
            fstp ST(0)          // nre * nim, div, c.re, c.im
            fstp ST(0)          // div, c.re, c.im
            fstp ST(0)          // nc.re, c.im
            fstp ST(0)          // c.im
            fstp ST(0)          // clear!
    }
    

    Aber die Laufzeit ist auf die Millisekunde genau gleich geblieben. 😞 Deswegen werde ich mich wohl morgen um die SetPixel Funktion kümmern. Aber für heute ist erst mal Schluss. 🙂 Schließlich ist ja Fr. 😃

    Also, schönen Abend noch.



  • Du weißt aber schon, dass man die PDFs auch saugen kann?



  • Ringding schrieb:

    Du weißt aber schon, dass man die PDFs auch saugen kann?

    Jupp. 🙂 Aber wieso soll ich mit meiner schmalbandigen Verbindung 😉 die PDFs saugen, wenn Intel so freundlich ist und sie einem zuschickt. 👍



  • Ich hatte ganz vergessen euch mitzuteilen, dass es dann alles ordenlich geklappt hatte und das Programm zieml. gut funktioniert. Durch das direkte kopieren in den Speicherbereich der DibSection arbeitet der Algorithmus jetzt 9-10 mal schneller. Wen es interessiert, der kann sich das Ganze ("Fractal Set Viewer") von meiner Homepage runterladen.

    Vielen Dank noch mal!


  • Mod

    sehr schön. natürlich ist noch lange nicht das ende der fahnenstange, was die optimierung betrifft, erreicht 🙂
    ich hätte da ein paar ideen, was man versuchen könnte:

    1. sse statt fpu - da gibt es prinzipiell zwei möglichkeiten,
    - man arbeitet mit ungepackten daten. hier könnte man evtl. eine bessere auslastung der ausführungseinheiten des prozessors erreichen, die register sind ja nicht als stack angelegt, so dass man nicht ständig mit stackumorganisieren beschäftigt ist
    - mit gepackten daten: also 4 floats mit einer instruktion. nachteil ist, dass man sich hier erst einmal gedanken machen muss, wie man mit fällen zurecht kommt, bei denen nur einige aber nicht alle der punkte divergieren. hier müsste man irgendwie die punkte auf die register mappen. am besten aber erst mal eine studie in c/c++ schreiben, um das konzept zu prüfen; auf jeden fall gäbe es eine menge overhead, ein geschwindigkeitsfaktor 2 könnte aber drin sein
    2. integer statt fpu. gerade beim mandelbrot set bietet sich unter umständen festkommaarithmetik an, da durch die addition des komplexen C der iterationswert ohnehin ständig normalisiert ist. es ergeben sich aber möglichweise etwas andere rundungsfehler.
    3. kombiniere integer mit fpu oder sse - integer und fpu/sse-befehle können ja weitestgehend parallel und unabhängig ausgeführt werden. hier wäre also auch noch potential vorhanden.


Log in to reply