2012-10-19 11 views
7

Ich arbeite an der Optimierung einer 4D (128 Bit) Matrix-Vektor-Multiplikation mit ARM NEON Assembler.Wie kann ich eine geloopte 4D Matrix-Vektor-Multiplikation mit ARM NEON optimieren?

Wenn ich die Matrix und den Vektor in die NEON-Register lade und transformiere, bekomme ich keinen großen Leistungsschub, weil der Wechsel zu den NEON-Registern 20 Zyklen kostet. Außerdem lade ich die Matrix für jede Multiplikation neu, obwohl sie sich nicht geändert hat.

Es gibt genug Registerraum, um die Transformation auf mehr Vektoren pro Zeit durchzuführen. Dies erhöht die Leistung.

Aber ..

Ich frage mich, wie schnell dieser Vorgang wäre, wenn ich die Schleife über alle Eckpunkte tun (zunehmende Zeiger) im Assembler. Aber ich bin am Anfang von Neon Assembler und weiß nicht, wie ich das machen soll. Kann mir jemand dabei helfen?

Was ich erreichen möchte:

  1. Belastungsmatrix und erste Vektor
  2. Speicher Schleifenzählung "count" und ..
  3. - LOOP_START -
  4. führen mehrfach ergänzt (do die Transformation)
  5. Schreib Q0 bis VOUT
  6. Erhöhung Zeiger vIN und VOUT von 4 (128 Bit)
  7. LOAD vIN bis q5.
  8. - LOOP_END -

vorhandene C-Version von Schleife:

void TransformVertices(ESMatrix* m, GLfloat* vertices, GLfloat* normals, int count) 
{ 
    GLfloat* pVertex = vertices; 
    int i; 

    // iterate trough vertices only one at a time 
    for (i = 0; i < count ; i ++) 
    { 
     Matrix4Vector4Mul((float *)m, (float *)pVertex, (float *)pVertex); 
     pVertex += 4; 
    } 

    //LoadMatrix((const float*) m); 

    //// two at a time 
    //for (i = 0; i < count ; i += 2) 
    //{ 
    // Matrix4Vector4Mul2((float *)m, (float *)pVertex, (float *)(pVertex + 4)); 
    //  pVertex += 8; 
    //} 
} 

folgenden Code für NEON-Version auf nur eine Transformation zu tun:

void Matrix4Vector4Mul (const float* m, const float* vIn, float* vOut) 
{  
    asm volatile 
    (

    "vldmia %1, {q1-q4 }  \n\t" 
    "vldmia %2, {q5}   \n\t" 

    "vmul.f32 q0, q1, d10[0] \n\t"   
    "vmla.f32 q0, q2, d10[1] \n\t"  
    "vmla.f32 q0, q3, d11[0] \n\t"   
    "vmla.f32 q0, q4, d11[1] \n\t" 

    "vstmia %0, {q0}" 

    : // no output 
    : "r" (vOut), "r" (m), "r" (vIn)  
    : "memory", "q0", "q1", "q2", "q3", "q4", "q5" 
    ); 

} 

C-Version der Umwandlung:

void Matrix4Vector4Mul (const float* m, const float* vIn, float* vOut) 
{ 
    Vertex4D* v1 = (Vertex4D*)vIn; 
    Vertex4D vOut1; 
    Vertex4D* l0; 
    Vertex4D* l1; 
    Vertex4D* l2; 
    Vertex4D* l3; 

    // 4x4 Matrix with members m00 - m33 
    ESMatrix* m1 = (ESMatrix*)m; 

    l0 = (Vertex4D*)&m1->m00; 
    vOut1.x = l0->x * v1->x; 
    vOut1.y = l0->y * v1->x; 
    vOut1.z = l0->z * v1->x; 
    vOut1.w = l0->w * v1->x; 

    l1 = (Vertex4D*)&m1->m10; 
    vOut1.x += l1->x * v1->y; 
    vOut1.y += l1->y * v1->y; 
    vOut1.z += l1->z * v1->y; 
    vOut1.w += l1->w * v1->y; 

    l2 = (Vertex4D*)&m1->m20; 
    vOut1.x += l2->x * v1->z; 
    vOut1.y += l2->y * v1->z; 
    vOut1.z += l2->z * v1->z; 
    vOut1.w += l2->w * v1->z; 

    l3 = (Vertex4D*)&m1->m30; 
    vOut1.x += l3->x * v1->w; 
    vOut1.y += l3->y * v1->w; 
    vOut1.z += l3->z * v1->w; 
    vOut1.w += l3->w * v1->w; 

    *(vOut) = vOut1.x; 
    *(vOut + 1) = vOut1.y; 
    *(vOut + 2) = vOut1.z; 
    *(vOut + 3) = vOut1.w; 
} 

Leistung: (Transform> 90 000 Vertices | II Android 4.0.4 SGS)

C-Version: 190 FPS 
NEON-Version: 162 FPS (.. slower -.-) 

--- LOAD Matrix only ONCE (seperate ASM) and then perform two V's at a time --- 

NEON-Version: 217 FPS (+ 33 % NEON | + 14 % C-Code) 
+0

Bieten Sie Ihre Schleife in einfachen C, würden die Menschen es einfacher. – auselen

+0

oh yeah .. denke du hast Recht! – oc1d

+0

Stellen Sie Matrix4Vector4Mul auch, in der Tat machen Sie sie nur eine Schleife, wie Sie es in Plain c schreiben würden. – auselen

Antwort

0

Die Hand abgestimmt Neonversion leidet an Abhängigkeit zwischen all Operationen, während gcc der Lage ist, out-of-order für die c-Version Planung zu tun. Sie sollten in der Lage sein, die NEON-Version zu verbessern, indem Sie parallel zwei oder mehr unabhängige Threads berechnen:

Pointer Inkrement (Post Inkrement) in NEON ist mit Ausrufezeichen getan. Diese Register sollte dann in dem Ausgaberegister Liste "= R" (VOUT)

vld1.32 {d0,d1}, [%2]! ; // next round %2=%2 + 16 
vst1.32 {d0}, [%3]! ; // next round %3=%3 + 8 

Eine andere Adressierungsmodus ermöglicht Post-Inkrement von einem "stride" definiert in einem anderen Arm Register aufgenommen. Die Option ist nur bei einigen Ladebefehlen verfügbar (da es eine Vielzahl von Interleaving-Optionen gibt sowie das Laden von ausgewählten Elementen von say d1 [1] (oberer Teil)).

vld1.16 d0, [%2], %3 ; // increment by register %3 

Die Zählerinkrement geschieht mit Sequenz

1: subs %3, %3, #1  ; // with "=r" (count) as fourth argument 
bne 1b     ; // create a local label 

Lokale Etikett verwendet wird, als zwei „bne loop“ Anweisungen in derselben Datei einen Fehler verursacht

Man sollte in der Lage sein, Parallelität zu erhöhen um einen Faktor von vier, indem fusionierte Multiplikationen für Vektoren anstelle von einzelnen Elementen berechnet werden.

In diesem Fall lohnt es sich, vorab eine Matrixtransponierung durchzuführen (entweder vor Aufruf der Routine oder mit Spezialadressierungsmodus).

asm(
    "vld1.32 {d0[0],d2[0],d4[0],d6[0]}, [%0]! \n\t" 
    "vld1.32 {d0[1],d2[1],d4[1],d6[1]}, [%0]! \n\t" 
    "vld1.32 {d1[0],d3[0],d5[0],d7[0]}, [%0]! \n\t" 
    "vld1.32 {d1[1],d3[1],d5[1],d7[1]}, [%0]! \n\t" 

    "vld1.32 {q8}, [%2:128]! \n\t" 
    "vld1.32 {q9}, [%2:128]! \n\t" 
    "vld1.32 {q10}, [%2:128]! \n\t" 
    "vld1.32 {q11}, [%2:128]! \n\t" 

    "subs %0, %0, %0 \n\t" // set zero flag 

    "1: \n\t" 
    "vst1.32 {q4}, [%1:128]! \n\t" 
    "vmul.f32 q4, q8, q0 \n\t" 
    "vst1.32 {q5}, [%1:128]! \n\t" 
    "vmul.f32 q5, q9, q0 \n\t" 
    "vst1.32 {q6}, [%1:128]! \n\t" 
    "vmul.f32 q6, q10, q0 \n\t" 
    "vst1.32 {q7}, [%1:128]! \n\t" 
    "vmul.f32 q7, q11, q0 \n\t" 

    "subne %1,%1, #64 \n\t" // revert writing pointer in 1st iteration 

    "vmla.f32 q4, q8, q1 \n\t" 
    "vmla.f32 q5, q9, q1 \n\t" 
    "vmla.f32 q6, q10, q1 \n\t" 
    "vmla.f32 q7, q11, q1 \n\t" 
    "subs %2, %2, #1 \n\t" 
    "vmla.f32 q4, q8, q2 \n\t" 
    "vmla.f32 q5, q9, q2 \n\t" 
    "vmla.f32 q6, q10, q2 \n\t" 
    "vmla.f32 q7, q11, q2 \n\t" 

    "vmla.f32 q4, q8, q3 \n\t" 
    "vld1.32 {q8}, [%2:128]! \n\t" // start loading vectors immediately 
    "vmla.f32 q5, q9, q3 \n\t" 
    "vld1.32 {q9}, [%2:128]! \n\t" // when all arithmetic is done 
    "vmla.f32 q6, q10, q3 \n\t" 
    "vld1.32 {q10}, [%2:128]! \n\t" 
    "vmla.f32 q7, q11, q3 \n\t" 
    "vld1.32 {q11}, [%2:128]! \n\t" 
    "jnz b1 \n\t" 
    "vst1.32 {q4,q5}, [%1:128]! \n\t" // write after first loop 
    "vst1.32 {q6,q7}, [%1:128]! \n\t" 
: "=r" (m), "=r" (vOut), "=r" (vIn), "=r" (N), 
: 
: "d0","d1","q0", ...); // marking q0 isn't enough for some gcc version 

Lesen und Schreiben zu 128 Bit ausgerichtet Blöcke (auch sicherstellen, dass die Daten ptr ausgerichtet)
gibt es eine malloc mit align oder passen nur manuell ptr=((int)ptr + 15) & ~15.

Genau wie es einen Post-Loop-Block gibt, der die Ergebnisse schreibt, kann man einen ähnlichen Vor-Schleifenblock schreiben, der das erste Schreiben von Unsinn nach vOut überspringt (das könnte auch durch bedingtes Schreiben überwunden werden). Man kann leider nur 64-Bit-Register bedingt schreiben.

+0

schön! :) Fused Multiply-Adds werden nur in VFPv4 unterstützt. Ich denke Cortex A15 (und optional). – oc1d

+0

Entschuldigung, konnte mich nicht erinnern. Ich hatte eine vage Erinnerung daran, so etwas in A8 zu schreiben. Aber es gibt trotzdem 4 Register in der Registerbank (q12-q15), um das Zwischenergebnis zu multiplizieren, bevor es zu q4-q7 addiert wird. Auch gibt es eine Möglichkeit, für die Implementierung von Festpunkten zu gehen und die verfügbare Multiplikation zu verwenden. –

+1

anstelle von '" subs% 2,% 2, # 1 \ n \ t "' seine '" subs% 3,% 3, # 1 \ n \ t "', oder? – oc1d

1

Haben Sie versucht, mit Compiler-Flags zu spielen?

-mcpu=cortex-a9 -mtune=cortex-a9 -mfloat-abi=softfp -mfpu=neon -O3 

macht in diesem Fall ziemlich Arbeit für mich (gcc 4.4.3, verteilt mit Android NDK 8b). Versuchen Sie, engen Quellcode durch Definition der internen Funktionen statisch und inline sowie bewegliche Matrix (m [X] [0] stuff) zu statischen globalen Variablen oder einfach zusammenführen Matrix4Vector4Mul in Schleife und machen Matrix lokale Variablen, anstatt sie in Funktion zu übergeben - gcc wird dort nicht schlau.

Wenn ich dies tue, komme ich unten für die Hauptschleife.

a4: ed567a03 vldr s15, [r6, #-12] 
    a8: ee276aa0 vmul.f32 s12, s15, s1 
    ac: ee676aa8 vmul.f32 s13, s15, s17 
    b0: ed564a04 vldr s9, [r6, #-16] 
    b4: ee277a88 vmul.f32 s14, s15, s16 
    b8: ed165a02 vldr s10, [r6, #-8] 
    bc: ee677a80 vmul.f32 s15, s15, s0 
    c0: ed565a01 vldr s11, [r6, #-4] 
    c4: e2833001 add r3, r3, #1 
    c8: ee046a89 vmla.f32 s12, s9, s18 
    cc: e1530004 cmp r3, r4 
    d0: ee446aaa vmla.f32 s13, s9, s21 
    d4: ee047a8a vmla.f32 s14, s9, s20 
    d8: ee447aa9 vmla.f32 s15, s9, s19 
    dc: ee056a22 vmla.f32 s12, s10, s5 
    e0: ee456a01 vmla.f32 s13, s10, s2 
    e4: ee057a21 vmla.f32 s14, s10, s3 
    e8: ee457a02 vmla.f32 s15, s10, s4 
    ec: ee056a8b vmla.f32 s12, s11, s22 
    f0: ee456a83 vmla.f32 s13, s11, s6 
    f4: ee057aa3 vmla.f32 s14, s11, s7 
    f8: ee457a84 vmla.f32 s15, s11, s8 
    fc: ed066a01 vstr s12, [r6, #-4] 
100: ed466a04 vstr s13, [r6, #-16] 
104: ed067a03 vstr s14, [r6, #-12] 
108: ed467a02 vstr s15, [r6, #-8] 
10c: e2866010 add r6, r6, #16 
110: 1affffe3 bne a4 <TransformVertices+0xa4> 

mit 4 Lasten, 4 vervielfacht, 12 multiplizieren und sammelt und 4 speichert, die mit übereinstimmt, was Sie in Matrix4Vector4Mul tun.

Wenn Sie mit Compiler-generiertem Code immer noch nicht zufrieden sind, übergeben Sie den Compiler '-S', um die Assembly-Ausgabe zu erhalten, und verwenden Sie diese als Ausgangspunkt, um weiter zu verbessern, anstatt von Grund auf neu zu starten.

Sie sollten auch überprüfen, dass vertices Cache-Zeile Größe ausgerichtet ist (32 Bytes für Cortex-A9), um einen schönen Datenfluss zu erhalten.

Für die Vektorisierung gibt es GCC-Optionen wie -ftree-vectorizer-verbose=9, um Informationen zu drucken, was vektorisiert wurde. Suchen Sie auch in der GCC-Dokumentation this one, um zu sehen, wie Sie gcc steuern können oder was Sie ändern müssen, um Ihre Multiplikationen zu vektorisieren. Das klingt vielleicht nach Eintauchen, aber auf lange Sicht wäre es fruchtbarer für Sie als "Hand Vectorizing".

+0

Danke! :) Ich werde die compilerflags ausprobieren und das Ergebnis in diesem Thread posten. Aber der Compileroutput scheint auf Gleitkommazahlen einfacher Genauigkeit zu funktionieren, während er auch auf Quads mit NEON funktionieren könnte. Cortex A9 führt 64 Bit gleichzeitig aus (und das sind 8 Zyklen), aber mit A15 gibt es 128 Bit Ausführung mit einem Zyklus. Das sind ** 4 ** vs. ** 16 ** Anweisungen für die Transformation. Man sagt, dass handgeschriebene ASM die Zeit nicht wert ist, da A9 und A15 viel fortgeschrittener als A8 sind, aber ich konnte keinen Beweis dafür finden. – oc1d

+0

Das ist Objdump, verwenden Sie Arm-Linux-Androideabi-Objdump -S . gcc -S erstellt einen Code, der später zusammengebaut werden kann, objdump Ausgabe kann nicht. Deshalb habe ich oben erwähnt. – auselen

+0

naja .. meine Ungeduld;) – oc1d

0

Es ist ein fast ein Jahr altes Thema jetzt, aber ich denke, es ist wichtig, dass Sie die „richtige“ Antwort zu geben, da hier etwas sehr faul ist, und niemand hat dies bisher darauf hingewiesen:

  1. Sie sollten q4-q7 möglichst vermeiden, da sie vor der Verwendung aufbewahrt werden müssen

  2. Korrigieren Sie mich, wenn ich falsch liege, aber wenn mein Gedächtnis mir nicht versagt, nur d0 ~ d3 (oder d0 ~ d7) kann Skalare enthalten. Ich frage mich wirklich, warum gcc toleriert d10 und d11 als skalare Operanden. Da es physikalisch unmöglich ist, denke ich, gcc macht wieder etwas verrücktes mit deiner Inline-Montage. Überprüfen Sie die Demontage Ihres Inline-Assembly-Codes.

Es stimmt, Ihr Inline-Assembler-Code leidet an zwei Verriegelungen (2cycles nach Last und 9 Zyklen vor der Lagerung), aber es ist unvorstellbar für mich, dass der NEON-Code läuft langsamer als der C-Code.

Es ist eine sehr starke Vermutung von meiner Seite, dass gcc einige schwere Register überträgt und stattdessen eine Fehlermeldung ausgibt. Und es tut in diesem Fall nicht gerade einen Gefallen.