我正在使用ARM NEON Assembler优化4D(128位)矩阵向量乘法。
如果我将矩阵和矢量加载到NEON寄存器并对其进行转换,我将无法获得很好的性能提升,因为切换到NEON寄存器需要20个周期。此外,我为每次乘法重新加载矩阵,尽管它没有改变。
有足够的寄存器空间可以在更多的向量上执行转换。这是在提高绩效。
但是..
我想知道如果我在汇编程序中对所有顶点(增加指针)进行循环,这个操作会有多快。但我在霓虹汇编程序的最开始,虽然不知道如何做到这一点。有人能帮我一把吗?
我想要实现的目标:
循环的现有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-version of transformation:
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 )
答案 0 :(得分: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个字节)以获得良好的数据流。
对于矢量化,有像-ftree-vectorizer-verbose=9
这样的gcc选项来打印矢量化的信息。还可以在gcc文档this one中搜索,看看如何指导gcc或者你需要修改什么来使你的乘法矢量化。这可能听起来很多,但从长远来看,它比“手工矢量化”更有成效。
答案 1 :(得分: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 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) & ~15
。
就像有一个后循环块写入结果一样,可以编写一个类似的预循环块,跳过第一次写废话到vOut(也可以通过条件写入来克服)。遗憾的是,只能有条件地写入64位寄存器。
答案 2 :(得分:0)
这是一个差不多一年的话题,但我认为重要的是给你“正确的”答案,因为这里的东西非常可疑,到目前为止还没有人指出这一点:
如果可能,您应该避免使用q4-q7,因为它们必须在使用前保存
如果我错了,请纠正我,但如果我的记忆没有让我失望,只有d0~d3(或d0~d7)可以存放标量。我真的很想知道为什么gcc容忍d10和d11作为标量操作数。因为它在物理上是不可能的,所以我猜gcc会再次对你的内联汇编做一些疯狂的事情。查看内联汇编代码的反汇编。
是的,你的内联汇编代码有两个互锁(加载后2个循环和存储前9个循环),但对于我来说,NEON代码运行速度比C代码慢,这是不可想象的。
从我这边来看,这是一个非常强烈的猜测,gcc做了一些沉重的寄存器来回传输而不是吐出错误信息。在这种情况下,它并不完全有利。