GCC SSE Inline assembler funktioniert nicht



  • Hallo, ich habe ein problem mit dem GCC Inline Assembler und hoffe, daß Ihr mir helfen könnt. Bin gerade dabei mir einige Vektor-Klassen zu schreiben, welche über inline assembler die volle Geschwindigkeit der SSE-Instruktionen nutzen sollen. Leider gibt es bereits bei der ersten Klase und ersten Methode Probleme...gcc compiliert zwar einwandtfrei, jedoch ist das Resultat fehlerhaft.

    Definition:

    class Vec2
    {
    protected:
        float v[2] __attribute__((aligned(16)));
    
    public:
        float& x;
        float& y;
    
        Vec2();
        Vec2(const Vec2& vec);
        Vec2(float X, float Y);
    
        ...
    
        Vec2& operator+=(float s);
    };
    

    Implementation:

    inline Vec2& Vec2::operator+=(float s)
    {
        float  t[2] = {s, s};
    
        asm
        (
            "movlps xmm0, [%1]\n"
            "movlps xmm1, [%2]\n"
            "addps  xmm0, xmm1\n"
            "movlps [%0], xmm0\n"
            : "=r" (v)
            : "r" (v), "r" (t)
        );
    
        return *this;
    }
    

    Test-code:

    Vec2 c(1.0f, 2.0f);
        c += 3.0f;
        printf("C: [ %f %f ]\n", c.x, c.y);
    

    Resultat:

    C: [ 0.000000 0.000000 ]

    Aus der SSE-Doku geht hervor, das "movlps" KEIN 16 Byte Alignment benötigt, daher dürfte es mit dem Array "t" auch keine Probleme geben. Endianess is Little-Endian, also auch von den Adressen her kein Fehler. Auch das Ändern des "Clobbings" in "=rm" bzw. "rm" oder "g" bringt kein korrektes Resultat. Auch bei Compilierung OHNE Code-Optimierung läuft das Programm nicht korrekt.
    Hoffe, daß mir hier jemand weiter helfen kann.

    Gruss
    Marc


  • Mod

    Ich glaube, da solltest du lieber im Assembler-Forum fragen.

    Der GCC erstellt übrigens auch selber Code für SSE, wenn man ihm denn erlaubt Code zu erstellen der hinterher nicht auf einem 386er laufen würde:
    http://gcc.gnu.org/onlinedocs/gcc/i386-and-x86_002d64-Options.html

    Wenn du nicht gerade ganz pfiffige SSE-Algorithmen hast, würde ich mein Geld bei dieser Art der Optimierung eindeutig auf den Compiler setzen. Wahrscheinlich würde der Buchmacher solch eine Wette noch nicht einmal annehmen, weil niemand auf den Menschen setzen würde.



  • Wär schön, wenn es so wäre, aber für die Funktion:

    Vec2& Vec2::operator+=(float s)
    {
        x += s;
        y += s;
    
        return *this;
    }
    

    ...geneneriert mir GCC:

    004013B0	push   %ebp
    004013B1	mov    %esp,%ebp
    004013B3	mov    0x8(%ebp),%eax
    004013B6	movss  0xc(%ebp),%xmm0
    004013BB	mov    0x8(%eax),%edx
    004013BE	movss  (%edx),%xmm1
    004013C2	addss  %xmm0,%xmm1
    004013C6	movss  %xmm1,(%edx)
    004013CA	mov    0xc(%eax),%edx
    004013CD	addss  (%edx),%xmm0
    004013D1	movss  %xmm0,(%edx)
    004013D5	pop    %ebp
    004013D6	ret
    

    ...und das sind 2x "addss"...ich würde das ganze allerdings gerne über die SIMD-Funktionen von GCC lösen, also "addps". Den Code generiert mir GCC 4.5 übrigens bei "-msse -msse2 -mfpmath=sse"...dachte ehrlich, daß GCC etwas besser optimieren würde.

    Trotzdem danke für die schnelle Antwort.



  • Nimm SSE-Intrinsics, Inlineassembly ist meist nur für graue Haare gut.

    #include <xmmintrin.h>
    class Vec2
    {
      protected:
        float v[2];
    
      public:
        Vec2() {v[0]=0;v[1]=0;}
        Vec2(float X, float Y) {v[0]=X;v[1]=Y;}
    
        Vec2& operator+=(float s)
        {
          __m128 ps=_mm_set1_ps(s);
          __m128 pv=_mm_loadl_pi(_mm_setzero_ps(),(__m64*)v);
          pv=_mm_add_ps(ps,pv);
          _mm_storel_pi((__m64*)v,pv);
          return *this;
       }
    };
    

    Test:

    const int vc=1000000;
      Vec2* vecs=new Vec2[vc+1];
      if ((int)vecs%16!=0)vecs++;
      assert((int)vecs%16==0);
      for (int i=0;i<vc;i++)vecs[i]=Vec2(i,i+2);
    
      uint start=mclock();
      for (int x=0;x<1000;x++)for (int i=0;i<vc;i++)vecs[i]+=3.5;
      uint end=mclock();
    
      cout << end-start << " ms" << endl;
    

    Ergebnis: 2516 ms.

    Generierter Code für die innere Schleife:

    .L13:
    	leal	(%ebx,%eax), %edx
    	movaps	%xmm1, %xmm0
    	movlps	(%edx), %xmm0
    	addps	%xmm2, %xmm0
    	movlps	%xmm0, (%edx)
    	addl	$8, %eax
    	cmpl	$8000000, %eax
    	jne	.L13
    

    Nun ein Test mit:

    Vec2& operator+=(float s)
        {
          v[0] += s;
          v[1] += s;
          return *this;
       }
    

    Ergebnis: 2260 ms.

    Generierter Code:

    .L15:
    	movaps	%xmm1, %xmm0
    	movlps	(%eax), %xmm0
    	movhps	8(%eax), %xmm0
    	addps	%xmm2, %xmm0
    	movaps	%xmm0, (%eax)
    	addl	$16, %eax
    	cmpl	%ebx, %eax
    	jne	.L15
    

    Man beachte: zwei Vec2 werden pro Durchgang bearbeitet. Statt der ersten drei Instruktionen hätte es leider auch ein movaps getan, aber gcc kann sich nicht sicher sein, dass die Adresse 16 byte aligned ist.
    Edit: obwohl... beim zweiten movaps schämt er sich dann doch nicht... naja.

    Wenn man da noch manuell nachhilft, werden es 2085 ms.

    Wenn man vecs auf dem Stack anlegt, geht folgendes:
    Vec2 __attribute__((aligned(16))) vecs[vc];

    und daraufhin wird auch perfekter Code produziert:

    .L7:
    	movaps	(%eax), %xmm0
    	addps	%xmm1, %xmm0
    	movaps	%xmm0, (%eax)
    	addl	$16, %eax
    	cmpl	%ebx, %eax
    	jne	.L7
    

    aber ob und wie man dem gcc verklickern kann, dass ein Zeiger auf aligned memory zeigt, weiß ich nicht.



  • Erstens: Vielen vielen Dank für diese überaus detaillierte Antwort. Werde mich jetzt mit den XMM-Intrinsics anfreunden, auch, wenn diese laut GNU schon deprecated sind (Man soll die neuen vector-attribute verwenden, mit denen jedoch der GCC 3.5 als auch der GCC 4.5 nur Fehler machen (movaps mit unaligned memory etc.) und es kommt bei etwas grösseren Projekten fast immer zum Laufzeitfehler).

    Zweitens: Dem compiler sagen, daß ein Zeiger 16 Byte aligned ist, ist eigentlich sehr einfach...machst es schon selbst..beispiel:

    float v[4] __attribute__((aligned(16)));
    

    Damit wird fest gelegt, daß das Array 16 Byte aligned angelegt wird und jeder Zeiger auf das Array ebenfalls auf 16 Byte aligned memory zeigt.

    Gruss und nochmal vielen Dank



  • Damit wird fest gelegt, daß das Array 16 Byte aligned angelegt wird und jeder Zeiger auf das Array ebenfalls auf 16 Byte aligned memory zeigt.

    Ja, aber das gilt nur, wenn man nicht mit new arbeitet.
    Deswegen ist ein Weg nötig, selbst "Zeiger auf 16 byte-aligned Vec2" deklarieren zu können.



  • Ja, auf diese Problem bin ich nun auch gestossen, jedoch liess es sich sehr elegant über das Überladen der operatoren "new" und "new[]", bzw. "delete" und "delete[]" lösen...zur Reservierung des Speichers muss man dann lediglich einen Puffer mit genügend Platz für die Vektoren + 16 Byte anlegen und die eigentliche Anfangs-Adresse auf die nächst-beste "aligned"-Adresse legen.

    MFG
    Marc



  • Dieser Thread wurde von Moderator/in pumuckl aus dem Forum C++ in das Forum Compiler- und IDE-Forum verschoben.

    Im Zweifelsfall bitte auch folgende Hinweise beachten:
    C/C++ Forum :: FAQ - Sonstiges :: Wohin mit meiner Frage?

    Dieses Posting wurde automatisch erzeugt.



  • Als Antwort auf die ursprüngliche Frage schlage ich ein Zitat aus
    http://tldp.org/HOWTO/Assembly-HOWTO/gas.html vor:

    The order of operands is source(s) first, and destination last...

    Zu deutsch: die Operanden-Reihenfolge ist Quelle zuerst, Ziel am Ende

    Schon der Anfang des ASM-codes erscheint damit etwas "fragwürdig":

    asm
        (
            "movlps xmm0, [%1]\n"
            "movlps xmm1, [%2]\n"
    


  • Zu deutsch: die Operanden-Reihenfolge ist Quelle zuerst, Ziel am Ende

    Das ist korrekt für AT&T-Syntax, aber wie Du vielleicht übersehen hast, benutze ich die Intel-Syntax, welche man bei dem GCC-Compiler einstellen kann, da ich mit der Intel-Syntax praktisch "gross geworden" bin und mich mit der AT&T-Syntax überhaupt nicht anfreunden kann.


Anmelden zum Antworten