如何使用ARM NEON优化循环4D矩阵向量乘法?

oc1*_*c1d 7 c android arm neon android-ndk

我正在使用ARM NEON Assembler优化4D(128位)矩阵向量乘法.

如果我将矩阵和矢量加载到NEON寄存器并对其进行转换,我将无法获得极大的性能提升,因为切换到NEON寄存器需要20个周期.此外,我为每次乘法重新加载矩阵,尽管它没有改变.

有足够的寄存器空间可以在更多的向量上执行转换.这是在提高性能.

但..

我想知道如果我在汇编程序中对所有顶点(增加指针)进行循环,这个操作会有多快.但我现在处于霓虹汇编程序的最开始,虽然不知道如何做到这一点.有人能帮我一把吗?

我想要实现的目标:

  1. 加载矩阵和第一个向量
  2. 存储循环计数"计数"和..
  3. - LOOP_START -
  4. 执行乘法 - 添加(执行转换)
  5. 将q0写入vOut
  6. 将指针vIn和vOut增加4(128位)
  7. LOAD 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;
    //}
}
Run Code Online (Sandbox Code Playgroud)

以下代码仅针对一次转换执行NEON-Version:

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" 
    );

}
Run Code Online (Sandbox Code Playgroud)

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;
}
Run Code Online (Sandbox Code Playgroud)

表现:(变换> 90 000顶点| 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 )
Run Code Online (Sandbox Code Playgroud)

Aki*_*nen 0

手工调整的 neon 版本受到所有操作之间的依赖性的影响,而 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
Run Code Online (Sandbox Code Playgroud)

另一种寻址模式允许按另一个臂寄存器中定义的“步幅”进行后增量。该选项仅在某些加载命令上可用(因为有多种交错选项以及加载到 d1[1](上部)的选定元素)。

vld1.16 d0, [%2], %3    ; // increment by register %3
Run Code Online (Sandbox Code Playgroud)

计数器递增按顺序发生

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

使用本地标签,因为同一文件中的两个“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 
Run Code Online (Sandbox Code Playgroud)

读取和写入 128 位对齐的块(确保数据 ptr 也对齐)
有一个带有对齐的 malloc,或者只需手动调整ptr=((int)ptr + 15) & ~15

正如有一个写入结果的后循环块一样,我们可以编写一个类似的预循环块,该块会跳过对 vOut 的第一次无意义写入(这也可以通过条件写入来克服)。遗憾的是,只能有条件地写入 64 位寄存器。