2012-10-19 16 views
7

Pracuję nad optymalizacją mnożenia macierzy-wektorów 4D (128-bitowych) za pomocą ARM NEON Asembler.Jak zoptymalizować zapętlone mnożenie macierzy wektorowej 4D za pomocą ARM NEON?

Jeśli załaduję macierz i wektor do rejestrów NEON i zmienię to, nie otrzymam dużego zwiększenia wydajności, ponieważ przejście do rejestrów NEON kosztuje 20 cykli. Ponadto przeładowuję macierz dla każdego mnożenia, mimo że się nie zmieniła.

Jest wystarczająco dużo przestrzeni rejestrów, aby wykonać transformację na więcej wektorów w czasie. To zwiększa wydajność.

Ale ..

Zastanawiam się, jak szybko ta operacja będzie, jeśli zrobić pętlę nad wszystkimi wierzchołkami (zwiększenie wskaźników) w ramach asemblerze. Ale jestem na samym początku asemblera Neonów i choć nie wiem jak to zrobić. Czy ktoś może mi w tym pomóc?

Co chcę osiągnąć:

  1. macierz obciążenia i pierwszy wektor
  2. sklep licznik pętli "Count" i ..
  3. - LOOP_START -
  4. wykonać mnożenie-dodaje (zrobić transformacja)
  5. zapisu q0 do Vout
  6. wzrost wskaźniki Vin i Vout przez 4 (128 Bit)
  7. ŁADUJ vIn do q5.
  8. - LOOP_END -

istniejący C-Version pętli:

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; 
    //} 
} 

następujący kod dla NEON wersja robić tylko jedną transformację:

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 transformacji:

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; 
} 

Wydajność: (Przekształcenie> 90 000 werteksów | Android 4.0.4 SGS II)

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

Podaj swoją pętlę w prostym C, ludzie będą mogli łatwiej. – auselen

+0

o tak ... zgaduję, że masz rację! – oc1d

+0

Dostarcz także Matrix4Vector4Mul, w rzeczywistości utwórz je w jedną pętlę, tak jak napisałeś to w zwykłym c. – auselen

Odpowiedz

0

Ręka tuned wersja neon cierpi z zależnością pomiędzy wszystkich operacji, podczas gdy gcc jest w stanie zrobić, out-of-order harmonogram dla c-wersji. Powinieneś być w stanie ulepszyć wersję NEON, obliczając równolegle dwa lub więcej niezależnych wątków:

Przyrost wskaźnika (przyrost litery) w NEON odbywa się za pomocą wykrzyknika. Rejestry te następnie powinny być zawarte na liście wyjściowego rejestru „= r” (Vout)

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

Inny sposób rozwiązania umożliwia pocztowy przyrost przez „kroku” zdefiniowane w innym rejestrze ramienia. Opcja jest dostępna tylko w przypadku niektórych poleceń ładowania (ponieważ istnieje wiele opcji przeplatania, a także ładowanie do wybranych elementów, np. D1 [1] (część górna)).

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

Licznik przyrost dzieje się z sekwencji

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

Local etykieta jest stosowany, jako dwa „pętla bne” stwierdzenia w tym samym pliku powoduje błąd

jeden powinien być w stanie zwiększyć równoległość czterokrotnie, obliczając stopione pomnożenie, dodaje wektory zamiast pojedynczych elementów.

W tym przypadku warto wykonać wcześniej transpozycję macierzy (przed wywołaniem procedury lub ze specjalnym trybem adresowania).

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 

Odczyt i zapis do 128 bitów wyrównane bloków (upewnij się, że dane są wyrównane PTR zbyt)
istnieje malloc z align, lub po prostu ustawić ręcznie ptr=((int)ptr + 15) & ~15.

Tak jak istnieje blok pętli postów zapisujący wyniki, można napisać podobny blok pętli pre-loop, który pomija pierwszy zapis nonsensu do vOut (który można również pokonać przez zapis warunkowy). Można niestety zapisywać warunkowo tylko 64-bitowe rejestry.

+0

fajnie! :) Fused Multiply-Adds są obsługiwane tylko w VFPv4 .. myślę, że Cortex A15 (i opcjonalnie). – oc1d

+0

Niestety, nie pamiętam tego. Miałem niewyraźne wspomnienie pisania czegoś takiego w A8. Ale są jeszcze 4 rejestry pozostawione w banku rejestru (q12-q15), aby pomnożyć wynik pośredni przed dodaniem do q4-q7. Istnieje również możliwość przejścia do stałego punktu realizacji i wykorzystania dostępnego wielokrotnego akumulowania. –

+1

zamiast "subs subs% 2,% 2, # 1 \ n \ t" 'jego' "subs% 3,% 3, # 1 \ n \ t" ', prawda? – oc1d

1

Czy próbowałeś grać z flagami kompilatora?

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

wykonuje dla mnie w tej sprawie bardzo dobrą robotę (gcc 4.4.3, rozpowszechniany z Androidem NDK 8b). Staraj się mieć ścisły kod źródłowy poprzez zdefiniowanie wewnętrznych funkcji statycznych i inline oraz ruchomych macierzy (m [X] [0] rzeczy) do statycznych zmiennych globalnych lub po prostu scalenie Matrix4Vector4Mul w pętlę i tworzenie lokalnych zmiennych macierzy zamiast przekazywania go w funkcji - gcc nie jest tam sprytny.

Kiedy to zrobię, dostaję się poniżej głównej pętli.

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> 

Mając 4 ładunki, 4, 12 mnożenia mnożenie i gromadzi oraz 4 sklepy który pasuje z tym, co robisz w Matrix4Vector4Mul.

Jeśli nadal nie jesteś usatysfakcjonowany wygenerowanym przez kompilator kodem, prześlij kompilator '-S', aby uzyskać dane wyjściowe zespołu i użyj go jako punktu wyjścia do dalszej poprawy, zamiast zaczynać od zera.

Należy również sprawdzić, czy vertices jest wyrównany do rozmiaru linii pamięci podręcznej (32 bajty dla Cortex-A9), aby uzyskać ładny przepływ danych.

Do wektoryzacji dostępne są opcje gcc, takie jak -ftree-vectorizer-verbose=9, aby wydrukować informacje o wektorze. Wyszukaj również w dokumentacji gcc this one, aby zobaczyć, jak możesz kierować gcc lub co musisz zmodyfikować, aby uzyskać wektory mnożenia. To może zabrzmieć bardzo głęboko, ale na dłuższą metę będzie bardziej owocne niż "wektoryzacja rąk".

+0

Dzięki! :) Wypróbuję kompilatorflags i post w tym wątku. Wydaje się jednak, że wydajność kompilatora działa na zmiennoprzecinkach o pojedynczej precyzji, podczas gdy może działać również na quadach z NEON. Cortex A9 wykonuje 64 bity na raz (i to będzie 8 cykli), ale z A15 jest 128 bitowe wykonanie z pojedynczym cyklem. Będzie to instrukcja ** 4 ** vs. ** 16 ** dla transformacji. Mówi się, że odręczny ASM nie jest wart czasu, ponieważ A9 i A15 są znacznie bardziej zaawansowane niż A8, ale nie mogłem znaleźć na to dowodu. – oc1d

+0

To jest objdump, użyj arm-linux-androideabi-objdump -S . gcc -S tworzy kod, który może być złożony później, wyjście objdump nie może. Dlatego wspomniałem w ten sposób powyżej. – auselen

+0

cóż .. moja niecierpliwość;) – oc1d

0

Jest to niemal jeden pełny rok stary temat teraz, ale myślę, że to ważne, aby dać Ci „prawidłową” odpowiedź ponieważ coś jest bardzo podejrzany tutaj, a nikt nie wskazywał na to do tej pory:

  1. Należy unikać używania q4-q7, o ile jest to możliwe, ponieważ należy je zachować przed użyciem.

  2. Popraw mnie, jeśli się mylę, ale jeśli moja pamięć mnie nie zawodzi, to tylko d0 ~ d3 (lub d0 ~ d7) może pomieścić skalary. Naprawdę zastanawiam się, dlaczego gcc toleruje d10 i d11 jako operatory skalarne. Ponieważ jest to fizycznie niemożliwe, myślę, że gcc znowu robi coś szalonego ze swoim wbudowanym zestawem. Sprawdź demontaż swojego wbudowanego kodu zespołu.

To prawda, że ​​Twój kod inline montaż cierpi z dwoma blokadami (2cycles po obciążeniu i 9 cykli przed sklepie), ale to nie do pomyślenia dla mnie, że kod NEON działa wolniej niż kod C.

To naprawdę mocne przypuszczenie z mojej strony, że gcc robi jakiś ciężki rejestr przesyłając tam iz powrotem zamiast wypluwania komunikatu o błędzie. I w tym przypadku nie robi to za bardzo przysługi.

Powiązane problemy