1.简介
读过上一篇文章“ARM NEON快速上手指南”之后,相信你已经对ARM NEON编程有了基本的认识。但在真正利用ARM NEON优化程序性能时,还有很多编程技巧和注意事项。本文将结合本人的一些开发经历,介绍NEON编程中的一些常见优化技巧,希望能对用户在NEON实际开发中有些借鉴意义。
2.NEON优化技术
在利用NEON优化程序时,有下述几项比较通用的优化技巧。
2.1 降低数据依赖性
在ARM v7-A NEON指令通常需要3~9个指令周期,NEON指令比ARM指令需要更多周期数。因此,为了减少指令延时,最好避免将当前指令的目的寄存器当作下条指令的源寄存器。如下例所示:
// C代码
float SumSquareError_C(const float* src_a, const float* src_b, int count)
{
float sse = 0u;
int i;
for (i = 0; i < count; ++i) {
float diff = src_a[i] - src_b[i];
sse += (float)(diff * diff);
}
return sse;
}
// NEON实现一
float SumSquareError_NEON1(const float* src_a, const float* src_b, int count)
{
float sse;
asm volatile (
"veor q8, q8, q8
"
"veor q9, q9, q9
"
"veor q10, q10, q10
"
"veor q11, q11, q11
"
"1:
"
"vld1.32 {q0, q1}, [%0]!
"
"vld1.32 {q2, q3}, [%0]!
"
"vld1.32 {q12, q13}, [%1]!
"
"vld1.32 {q14, q15}, [%1]!
"
"subs %2, %2, #16
"
// q0, q1, q2, q3 是vsub的目的地寄存器.
// 也是vmla的源寄存器。
"vsub.f32 q0, q0, q12
"
"vmla.f32 q8, q0, q0
"
"vsub.f32 q1, q1, q13
"
"vmla.f32 q9, q1, q1
"
"vsub.f32 q2, q2, q14
"
"vmla.f32 q10, q2, q2
"
"vsub.f32 q3, q3, q15
"
"vmla.f32 q11, q3, q3
"
"bgt 1b
"
"vadd.f32 q8, q8, q9
"
"vadd.f32 q10, q10, q11
"
"vadd.f32 q11, q8, q10
"
"vpadd.f32 d2, d22, d23
"
"vpadd.f32 d0, d2, d2
"
"vmov.32 %3, d0[0]
"
: "+r"(src_a),
"+r"(src_b),
"+r"(count),
"=r"(sse)
:
: "memory", "cc", "q0", "q1", "q2", "q3", "q8", "q9", "q10", "q11","q12", "q13","q14", "q15");
return sse;
}
// NEON实现二
float SumSquareError_NEON2(const float* src_a, const float* src_b, int count)
{
float sse;
asm volatile (
"veor q8, q8, q8
"
"veor q9, q9, q9
"
"veor q10, q10, q10
"
"veor q11, q11, q11
"
"1:
"
"vld1.32 {q0, q1}, [%0]!
"
"vld1.32 {q2, q3}, [%0]!
"
"vld1.32 {q12, q13}, [%1]!
"
"vld1.32 {q14, q15}, [%1]!
"
"subs %2, %2, #16
"
"vsub.f32 q0, q0, q12
"
"vsub.f32 q1, q1, q13
"
"vsub.f32 q2, q2, q14
"
"vsub.f32 q3, q3, q15
"
"vmla.f32 q8, q0, q0
"
"vmla.f32 q9, q1, q1
"
"vmla.f32 q10, q2, q2
"
"vmla.f32 q11, q3, q3
"
"bgt 1b
"
"vadd.f32 q8, q8, q9
"
"vadd.f32 q10, q10, q11
"
"vadd.f32 q11, q8, q10
"
"vpadd.f32 d2, d22, d23
"
"vpadd.f32 d0, d2, d2
"
"vmov.32 %3, d0[0]
"
: "+r"(src_a),
"+r"(src_b),
"+r"(count),
"=r"(sse)
:
: "memory", "cc", "q0", "q1", "q2", "q3", "q8", "q9", "q10", "q11", "q12", "q13","q14", "q15");
return sse;
}
在NEON实现一中,我们把目的寄存器立刻当作源寄存器;在NEON实现二中,我们重新排布了指令,并给予目的寄存器尽量多的延时。经过测试实现二比实现一快30%。由此可见,降低数据依赖性对于提高程序性能有重要意义。一个好消息是编译器能自动调整NEON intrinsics以降低数据依赖性。这个利用NEON intrinsics的一个很大优势。
2.2 减少跳转
NEON指令集没有跳转指令,当需要跳转时,我们需要借助ARM指令。在ARM处理器中,分支预测技术被广泛使用。但是一旦分支预测失败,惩罚还是比较高的。因此我们最好尽量减少跳转指令的使用。其实,在有些情况下,我们可以用逻辑运算来代替跳转,如下例所示:
// C实现
if( flag )
{
dst[x * 4] = a;
dst[x * 4 + 1] = a;
dst[x * 4 + 2] = a;
dst[x * 4 + 3] = a;
}
else
{
dst[x * 4] = b;
dst[x * 4 + 1] = b;
dst[x * 4 + 2] = b;
dst[x * 4 + 3] = b;
}
// NEON实现
//dst[x * 4] = (a&Eflag) | (b&~Eflag);
//dst[x * 4 + 1] = (a&Eflag) | (b&~Eflag);
//dst[x * 4 + 2] = (a&Eflag) | (b&~Eflag);
//dst[x * 4 + 3] = (a&Eflag) | (b&~Eflag);
VBSL qFlag, qA, qB
ARM NEON指令集提供了下列指令来帮助用户实现上述逻辑实现:
• VCEQ, VCGE, VCGT, VCLE, VCLT……
• VBIT, VBIF, VBSL……
减少跳转,不仅仅是在NEON中使用的技巧,是一个比较通用的问题。即使在C程序中,这个问题也是值得注意的。
2.3 其它技巧
在ARM NEON编程时,一种功能有时有多种实现方式,但是更少的指令不总是意味着更好的性能,要依据测试结果和profiling数据,具体问题具体分析。下面列出来我遇到的一些特殊情况。2.3.1 浮点累加指令通常情况下,我们会用VMLA/VMLS来代替VMUL + VADD/ VMUL + VSUB,这样使用较少的指令,完成更多的功能。但是与浮点VMUL相比,浮点VMLA/VMLS具有更长的指令延时,如果在指令延时中间不能插入其它计算的情况下,使用浮点VMUL + VADD/ VMUL + VSUB反而具有更好的性能。一个真实例子就是Ne10库函数的浮点FIR函数。代码片段如下所示:
实现1:在两条VMLA指令之间,仅有VEXT指令。而根据指令延时表,VMLA需要9个周期。
实现2:对于qAcc0,依然存在指令延时。但是VADD/VMUL只需要5个周期。下列代码中周期n粗略地表示了指令执行需要的周期数。与实现1相比,实现2节省了6个周期。性能测试也表明实现2具有更好的性能。
实现 1: VMLA
VEXT qTemp1,qInp,qTemp,#1
VMLA qAcc0,qInp,dCoeff_0[0]-- cycle 0
VEXT qTemp2,qInp,qTemp,#2
VMLA qAcc0,qTemp1,dCoeff_0[1] -- cycle 9
VEXT qTemp3,qInp,qTemp,#3
VMLA qAcc0,qTemp2,dCoeff_1[0] -- cycle 18
VMLA qAcc0,qTemp3,dCoeff_1[1] -- cycle 27
得到最终结果 qAcc0需要36个指令周期。
实现 2: VMUL+VADD
VEXT qTemp1,qInp,qTemp,#1
VMLA qAcc0,qInp,dCoeff_0[0] ]-- cycle 0
VMUL qAcc1,qTemp1,dCoeff_0[1]
VEXT qTemp2,qInp,qTemp,#2
VMUL qAcc2,qTemp2,dCoeff_1[0]
VADD qAcc0, qAcc0, qAcc1-- cycle 9
VEXT qTemp3,qInp,qTemp,#3
VMUL qAcc3,qTemp3,dCoeff_1[1]
VADD qAcc0, qAcc0, qAcc2-- cycle 14
VADD qAcc0, qAcc0, qAcc3-- cycle 19
得到最终结果 qAcc0需要24个指令周期。
与实现1相比,三条VADD指令需要6个发射指令周期。总共需要 30个指令周期。
modules/dsp/NE10_fir.neon.s:line 195
指令延时请参考下表:
Name | Format | Cycles | Result |
---|---|---|---|
VADD/VSUB/VMUL | Qd,Qn,Dm | 2 | 5 |
VMLA/VMLS | Qd,Qn,Dm | 2 | 9 |
表格来源于Cortex-A9 NEON Media Processing Engine Revision: r4p1 Technical Reference Manual: 3.4.8。
表格中:• Cycles:指令发射时间
• Result:指令执行时间
2.4 小结
总结起来,NEON的优化技巧主要有以下几点
• 尽量利用指令执行延时,合理安排指令顺序
• 少用跳转
• 注意cache命中率
审核编辑:郭婷
全部0条评论
快来发表一下你的评论吧 !