Beschleunigte Funktion für Quadratwurzel



  • Die Speeds - Wie ist der Compiler da eingestellt ? Für Debug oder "Maximize Speed"...



  • Da ich letztens selbst diverse sqrt-Funktionen getestet hab, hab ich FastSQRT einfach mal dagegen verglichen. Und was soll ich sagen, so fast is die nicht, ganz im Gegenteil, es war die langsamste.

    Testsystem:
    AMD Athlon XP Barton 2,6 GHz
    MSC 7.1 (Maximize Speed /O2, debug disabled, SSE instruction set)

    die Testkandidaten:

    // fpu
    inline float sqrt1(float x)
    {
    	asm
    	{
    		fld	x
    		fsqrt
    		fstp	x
    	}
    	return x;
    }
    
    // sse
    inline float sqrt2(float x)
    {
    	asm
    	{
    		movss	xmm0, x
    		sqrtss	xmm0, xmm0
    		movss	x, xmm0
    	}
    	return x;
    }
    
    // sse inverse approximation (12-bit precision)
    inline float sqrt3(float x)
    {
    	asm
    	{
    		movss	xmm0, x
    		rsqrtss	xmm1, xmm0
    		mulss	xmm0, xmm1
    		movss	x, xmm0
    	}
    	return x;
    }
    
    // sse inverse approximation (23-bit precision)
    inline float sqrt4(float x)
    {
    	const float const_0_5 = 0.5;
    	const float const_3 = 3;
    	__asm
    	{
    		movss	xmm1, x
    		movss	xmm2, const_3
    		rsqrtss	xmm0, xmm1
    		mulss	xmm1, xmm0
    		mulss	xmm1, xmm0
    		subss	xmm2, xmm1
    		mulss	xmm0, const_0_5
    		mulss	xmm0, xmm2
    		mulss	xmm0, x
    		movss	x, xmm0
    	}
    	return x;
    }
    
    // 3dnow inverse approximation (15-bit precision)
    inline float sqrt5(float x)
    {
    	asm
    	{
    		movd	mm0, x
    		pfrsqrt	mm1, mm0
    		punpckldq	mm0, mm0
    		pfmul	mm0, mm1
    		movd	x, mm0
    		femms
    	}
    	return x;
    }
    
    // 3dnow inverse approximation (24-bit precision)
    inline float sqrt6(float x)
    {
    	asm
    	{
    		movd	mm0, x
    		pfrsqrt	mm1, mm0
    		movq	mm2, mm1
    		pfmul	mm1, mm1
    		punpckldq	mm0, mm0
    		pfrsqit1	mm1, mm0
    		pfrcpit2	mm1, mm2
    		pfmul	mm0, mm1
    		movd	x, mm0
    		femms
    	}
    	return x;
    }
    

    das Testprogramm:

    float f0 = 1234.5678f;
    	float f1 = 0;
    	chronometer cm;
    	for (unsigned int i2 = 0; i2 != 4; i2++)
    	{
    		cm.start();
    		for (unsigned int i = 0; i != 100000000; i++)
    		{
    			f1 += sqrt1(f0);	// sqrt2, sqrt3, etc.
    		}
    		cm.stop();
    		std::cout << "[" << i2 << "] " << "time: " << cm.elapsed().milliseconds() << std::endl;
    		cm.reset();
    	}
    

    die Testergebnisse:

    sqrt1: ~1308 (~1,093e-08 Fehler)
    sqrt2: ~838 (~1,093e-08 Fehler)
    sqrt3: ~686 (~3,356e-05 Fehler)
    sqrt4: ~1052 (~1,093e-08 Fehler)
    sqrt5: ~612 (~1,093e-08 Fehler)
    sqrt6: ~914 (~1,093e-08 Fehler)
    FastSQRT: ~1447 (~1,093e-08 Fehler)

    Natürlich ist das ein rein synthetischer Test. Bei diversen anderen Testalgorithmen kommen zum Teil völlig andere Ergebnisse raus (also andere Zeiten, das Verhältnis der Ergebnisse zwischen den einzelnen Funktionen bleibt aber ungefähr gleich). So waren zB bei einfachem Ausrechnen (ohne f1 aufzuaddieren) die SSE Funktionen fast doppelt so schnell.
    Wie auch immer, ich denk der Test zeigt wo's lang geht:
    1. ich seh keinen wirklichen Verwendungszweck von FastSQRT
    2. enorme Performancegewinne lassen sich hier nicht erzielen und wie Optimizer schon gesagt hat, ist es besser, Algorithmen zu verwenden wo man möglichst nicht die Wurzel ziehen braucht (wobei das imo nicht immer möglich ist)



  • Es geht eben nichts über optimalen ASS-Code. 🙂
    Hast Du auch sqrt(...) unter diesen Bedingungen getestet?



  • Nein, hab ich nicht. Da sqrt() letztendlich auch nur eine Implementierung der CPU Möglichkeiten ist, würde ich keine schlechteren Ergebnisse als bei der FPU Version erwarten. Ausserdem muss man davon ausgehen, dass gute Compiler je nach verwendeten Instruction Set Erweiterungen sqrt() unterschiedlich in den Code "einbauen".



  • nimm mal die hier raus:

    if( r == 0.0f ) return 0.0f; 
        if( r <  0.0f ) r = -r     ;
    

    dann wird das Ergebnis für FastSquad ein bisschen besser aussehen. ansonsten bekomme ich ähnliche Ergebnisse.



  • // Null und negative Zahlen abfangen
        //if( r == 0.0f ) return 0.0f;
        if( r <  0.0f ) r = -r     ;
    

    Die negativen Zahlen sollte man schon abfangen, sonst ergibt es: -1.#IND



  • ja, schon klar. aber dann in jeder funktion.


  • Mod

    Erhard Henkes schrieb:

    // Null und negative Zahlen abfangen
        //if( r == 0.0f ) return 0.0f;
        if( r <  0.0f ) r = -r     ;
    

    Die negativen Zahlen sollte man schon abfangen, sonst ergibt es: -1.#IND

    dafür gibt es eine funktion, die ist schneller als ein conditinal jump.

    rapso->greets();



  • Ich denke rapso meint hier fabs (oder auch fabsf für single precision Werte) aber da die Funktion viel mit der Integerunit arbeitet, würde ich hier mit einem weiteren Integer das Signbit wegmaskieren. Sonst lädt man den Wert in die FPU, generiert dort den absoluten Wert um in kurz danach wieder zurück in ein GPR zu verschieben.

    cya
    liquid



  • Ich würde die ifs komplett weglassen.

    sqrt(-1) ist nunmal nicht 1. Warum dann so tun? Wie wär's mit einem

    assert(r>=0.0f);
    

    ?

    MfG Jester



  • Afaik werden Asserts im Non-Debug Modus deaktiviert.

    cya
    liquid



  • Das ist ja auch der Sinn der Sache. Ich halte es auch für unsinnig, kommentarlos ein falsches Ergebnis zurückzuliefern.

    Kannst ja ein std::complex werfen. 🤡



  • Ich wollte darauf hinaus, dass negative Fälle dann beim Release nicht mehr abgefangen werde. Keine Ahnung ob das so beabsichtigt ist.

    cya
    liquid



  • Ja, denke ich mal. Die Wahrscheinlichkeit ist nach gründlichem Testen wohl sehr groß, dass nur korrekte Parameter übergeben werden.
    Und sonst isses halt undefiniertes Verhalten. Gibt's ja in C++ eh genug. 🤡 Eine Wurzelfunktion ist IMHO zu kritisch, um sich solche Tests zu erlauben. sqrt() aus <cmath> macht es ja auch nicht.

    EDIT: Mit "falsches Ergebnis" oben meinte ich nicht direkt ein falsches Ergebnis wie -1#IND zurückliefern, sondern ein Ergebnis, wo man nachher nicht mehr erkennen kann, dass es falsch ist, wie 1.0 bei sqrt(-1.0). Das halte ich für Unsinn.



  • http://assemblyrequired.crashworks.org/2009/10/16/timing-square-root/ als aktuelle Ergänzung dieses stets interessanten Themas.



  • TGGC schrieb:

    Ich benutze die Funktionen aus <cmath>, da ich noch kein Problem damit hatte.

    👍
    Die meisten Compiler kennen diese Funktionen sowieso als Intrinsics, würde mich wundern, wenn man den Compiler da irgendwie übertreffen könnte...



  • dot schrieb:

    TGGC schrieb:

    Ich benutze die Funktionen aus <cmath>, da ich noch kein Problem damit hatte.

    👍
    Die meisten Compiler kennen diese Funktionen sowieso als Intrinsics, würde mich wundern, wenn man den Compiler da irgendwie übertreffen könnte...

    cmath garantiert lediglich die korrektheit, nicht geschwindigkeit. die meisten spiele hatten deswegen ihre eigenen wurzel funktionen.
    auf konsolen ist das auch weiterhin ueblic, da die emulation der korrekten wurzel weitaus teurer ist, als eine approximierte version (bei der man z.b. das vorzeichen immer als positiv annimmt usw.)


Anmelden zum Antworten