【问题标题】:How can I optimize a looped 4D matrix-vector-multiplication with ARM NEON?如何使用 ARM NEON 优化循环 4D 矩阵向量乘法?
【发布时间】:2012-10-10 04:52:37
【问题描述】:

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

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

有足够的寄存器空间一次对更多向量执行转换。这提高了性能。

但是..

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

我想要达到的目标:

  1. 加载矩阵和第一个向量
  2. 存储循环计数“计数”和..
  3. -- LOOP_START --
  4. 执行乘加(进行转换)
  5. 将 q0 写入 vOut
  6. 将指针 vIn 和 vOut 增加 4(128 位)
  7. 将 vIn 加载到 q5。
  8. -- 循环结束--

循环的现有 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-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" 
    );

}

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 个顶点 | 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 )

【问题讨论】:

  • 用简单的 C 语言提供你的循环,人们会更容易。
  • 哦是的..猜你是对的!
  • 也提供 Matrix4Vector4Mul,实际上只是让它们成为一个循环,就像你用纯 c 编写的那样。
  • ...我现在提供了操作的所有代码。这是一个只有转换功能的循环。希望我没听错?

标签: android c android-ndk arm neon


【解决方案1】:

您是否尝试过使用编译器标志?

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

在这种情况下对我来说做得很好(gcc 4.4.3,随 Android NDK 8b 一起分发)。尝试通过定义内部函数静态和内联以及将矩阵(m[X][0] 东西)移动到静态全局变量来获得紧凑的源代码,或者只是将 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 字节)以获得良好的数据流。

对于矢量化,有像 -ftree-vectorizer-verbose=9 这样的 gcc 选项来打印矢量化的信息。还可以在 gcc 文档中搜索 this one 以了解如何指导 gcc 或需要修改哪些内容以使乘法向量化。这听起来可能需要深入研究,但从长远来看,它比“手动矢量化”更有成效。

【讨论】:

  • 谢谢! :) 我将尝试编译器标志并在此线程中发布结果。但编译器输出似乎适用于单精度浮点,而它也适用于带有 NEON 的四边形。 Cortex A9 一次执行 64 位(这将是 8 个周期),但对于 A15,单周期执行 128 位。这将是 416 的转换指令。有人说手写 ASM 不值得花时间,因为 A9 和 A15 比 A8 先进得多,但我找不到证明。
  • 那是objdump,使用arm-linux-androideabi-objdump -S 。 gcc -S 创建一个可以稍后组装的代码,objdump 输出不能。这就是我在上面提到的原因。
【解决方案2】:

手动调整的 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

另一种寻址模式允许按另一个臂寄存器中定义的“步幅”后递增。该选项仅在某些加载命令上可用(因为有多种交错选项以及加载到 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 loop”语句会导致错误

通过计算向量而不是单个元素的融合乘加,应该能够将并行度提高四倍。

在这种情况下,提前执行矩阵转置是值得的(在调用例程之前或使用特殊寻址模式)。

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) &amp; ~15

就像有一个后循环块写入结果一样,人们可以编写一个类似的前循环块,跳过第一次无意义的写入 vOut(这也可以通过条件写入来克服)。不幸的是,只能有条件地写入 64 位寄存器。

【讨论】:

  • 不错! :) Fused Multiply-Adds 仅在 VFPv4 中受支持。我认为 cortex A15(和可选)。
  • 抱歉,不记得了。我模糊地记得在 A8 中写过类似的东西。但无论如何,寄存器库 (q12-q15) 中仍有 4 个寄存器用于在添加到 q4-q7 之前将中间结果相乘。还有可能进行定点实现并使用可用的乘法累加。
  • 而不是"subs %2, %2, #1 \n\t" 它的"subs %3, %3, #1 \n\t",对吧?
【解决方案3】:

到目前为止,这已经是一个将近一整年的话题了,但我认为给你一个“正确”的答案很重要,因为这里有些东西很可疑,到目前为止还没有人指出这一点:

  1. 您应该尽可能避免使用 q4-q7,因为它们必须在使用前保存

  2. 如果我错了,请纠正我,但如果我的记忆没有让我失望,那么只有 d0~d3(或 d0~d7)可以保存标量。我真的很想知道为什么 gcc 容忍 d10 和 d11 作为标量操作数。由于这种方式在物理上是不可能的,我猜 gcc 又在用你的内联汇编做一些疯狂的事情。查看内联汇编代码的反汇编。

的确,您的内联汇编代码存在两个互锁(加载后 2 个周期和存储前 9 个周期),但我无法想象 NEON 代码的运行速度比 C 代码慢。

在我看来,gcc 确实做了一些繁重的寄存器来回传输,而不是吐出一条错误消息,这是一个非常强烈的猜测。在这种情况下,它并没有完全帮上忙。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2019-01-20
    • 2017-07-02
    • 2021-02-06
    • 1970-01-01
    • 1970-01-01
    • 2016-03-02
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多