2012-10-19 19 views
7

我正在使用ARM NEON彙編器優化4D(128位)矩陣向量乘法。如何使用ARM NEON優化環形4D矩陣向量乘法?

如果我加載矩陣,並將向量導入NEON寄存器並對其進行轉換,我不會得到很好的性能提升,因爲切換到NEON寄存器要花費20個週期。此外,我重新加載每個乘法的矩陣,儘管它沒有改變。

有足夠的寄存器空間在一次更多的向量上執行轉換。這是提高性能。

但是...

我不知道如何快,如果我做遍歷所有頂點這一操作將是(增加指針)彙編內。但我在Neon彙編程序的開頭,雖然不知道如何做到這一點。有人能幫我一下嗎?

我想達到的目標:

  1. 載荷矩陣和第一矢量
  2. 店循環計數 「計數」 和..
  3. - LOOP_START -
  4. 執行乘加(做轉化)
  5. 寫入Q0至Vout
  6. 增加指針VIN和VOUT由4(128位)
  7. 加載vIn到q5。
  8. - LOOP_END -

現有循環的C-版本:

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

繼只做一個轉型的NEON-版本代碼:

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-版本轉化:

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

性能:(變換> 90 000個頂點|安卓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

在簡單的C中提供循環,人們會更容易。 – auselen

+0

哦,是啊..猜你是對的! – oc1d

+0

也提供Matrix4Vector4Mul,實際上就是讓它們成爲一個循環,就像你用普通的c書寫它一樣。 – auselen

回答

0

手調整霓虹燈版本從所有的操作之間的依賴關係受到影響,而GCC能夠爲C版做出來的無序調度。您應該可以通過並行計算兩個或多個獨立線程來改進NEON版本:

NEON中的指針增量(後增量)使用感嘆號完成。然後這些寄存器應包含在輸出寄存器列表「= R」(VOUT)

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

另一個尋址模式允許後增量通過在另一臂寄存器定義的「步幅」。該選項僅在某些加載命令中可用(因爲有多種交錯選項以及加載到選定元素d1 [1](上半部分))。

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

計數器增量與序列發生

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

局部標籤的使用,在同一文件中的兩個「BNE循環」語句導致錯誤

每個人都應該能夠增加並行通過計算向量的融合乘法加法而不是單個元素來減少四倍。

在這種情況下,提前執行矩陣轉置(在調用例程或特殊尋址模式之前)是值得的。

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 

讀取和寫入到128個準塊(確保數據PTR對準太)
有一個與對齊一個malloc或只是手動ptr=((int)ptr + 15) & ~15調整。就像有一個後循環塊寫入結果一樣,人們可以編寫一個類似的預循環塊,跳過第一次將無意義寫入vOut(這也可以通過有條件寫入來克服)。不幸的是,只能有條件地寫入64位寄存器。

+0

不錯! :) Fused Multiply-Adds只支持VFPv4 ..我認爲cortex A15(和可選)。 – oc1d

+0

對不起,不記得了。我對A8寫了類似的東西有一個模糊的回憶。但無論如何,寄存器組(q12-q15)中還有4個寄存器用於在將中間結果與q4-q7相加之前相乘。此外,還有可能要進行定點實施並使用可用的乘法累加。 –

+1

而不是'「subs%2,%2,#1 \ n \ t」'它的''子%3,%3,#1 \ n \ t「',對吧? – oc1d

1

您是否嘗試玩編譯器標誌?

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

在這種情況下爲我做的很好(gcc 4.4.3,與Android NDK 8b一起發佈)。通過定義內部函數static和inline,以及將矩陣(m [X] [0] stuff)定義爲靜態全局變量,或者將Matrix4Vector4Mul合併到循環中並創建矩陣局部變量,而不是繼續傳遞函數 - gcc在那裏並不聰明。

當我這樣做時,我得到了下面的主循環。

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> 

有4個負載,4次乘法,12乘法和累加和4個店與您在Matrix4Vector4Mul正在做的事情相匹配。

如果您仍然不滿意編譯器生成的代碼,請通過編譯器'-S'來獲取程序集輸出,並將其作爲起點來進一步改進,而不是從頭開始。

您還應該檢查vertices的高速緩存行大小是否對齊(Cortex-A9爲32字節)才能獲得良好的數據流。

對於矢量化,有gcc選項,如-ftree-vectorizer-verbose=9來打印矢量化的信息。還可以在gcc文檔this one中查找,以瞭解如何指示gcc或需要修改的內容以實現矢量化的乘法運算。這可能聽起來有很多東西需要挖掘,但從長遠來看,這對你來說會比「手工矢量化」更有成效。

+0

謝謝! :)我將嘗試編譯器標誌並在此線程中發佈結果。但編譯器輸出似乎可以在單精度浮點上運行,而它也可以在NEON上運行。 Cortex A9一次執行64位(這將是8個週期),但是對於A15,執行單週期的128位。這將是** 4 **與** ** **轉換說明。有人說手寫ASM不值得花時間,因爲A9和A15比A8先進得多,但我無法找到一個證明。 – oc1d

+0

這是objdump,請使用arm-linux-androideabi-objdump -S 。 gcc -S創建一個可以稍後組裝的代碼,objdump輸出不能。這就是我上面提到的原因。 – auselen

+0

好吧..我的急躁;) – oc1d

0

這是一個幾乎整整一年老話題了現在,但我想給你的「正確」答案是很重要的,因爲東西是很腥這裏了,沒有人指出這一點至今:

  1. 如果可能的話,你應該避免使用q4-q7,因爲它們必須在使用前保存下來

  2. 糾正我,如果我錯了,但如果我的記憶沒有失敗,只有d0〜d3 d0〜d7)可以容納標量。我真的很想知道爲什麼gcc可以容忍d10和d11作爲標量操作數。由於這種方式在物理上是不可能的,所以我猜gcc再次在內聯程序集中做了一些瘋狂的事情。檢查你的內聯彙編代碼的反彙編。

真,你的內聯彙編代碼從兩個聯鎖遭受(負荷後2cycles和存儲前9個週期),但它是不可想象的我,NEON代碼的運行速度比C代碼慢。

從我這邊來看,gcc做了一些重大的寄存器來回傳輸錯誤消息。在這種情況下,它並不完全贊成。