ARM NEON指令集优化理论与实践
一.简介
NEON就是一种基于SIMD思想的ARM技术,相比于ARMv6或之前的架构,NEON结合了64-bit和128-bit的SIMD指令集,提供128-bit宽的向量运算(vector operations)。NEON技术从ARMv7开始被采用,目前可以在ARM Cortex-A和Cortex-R系列处理器中采用。NEON在Cortex-A7、Cortex-A12、Cortex-A15处理器中被设置为默认选项,但是在其余的ARMv7 Cortex-A系列中是可选项。NEON与VFP共享了同样的寄存器,但它具有自己独立的执行流水线。
二. NEON寄存器
三. NEON指令集
所有的支持NEON指令都有一个助记符V,下面以32位指令为例,说明指令的一般格式:
V{<mod>}<op>{<shape>}{<cond>}{.<dt>}{<dest>}, src1, src2
- <mod>
- Q: The instruction uses saturating arithmetic, so that the result is saturated within the range of the specified data type, such as VQABS, VQSHL etc.
- H: The instruction will halve the result. It does this by shifting right by one place (effectively a divide by two with truncation), such as VHADD, VHSUB.
- D: The instruction doubles the result, such as VQDMULL, VQDMLAL, VQDMLSL and VQ{R}DMULH.
- R: The instruction will perform rounding on the result, equivalent to adding 0.5 to the result before truncating, such as VRHADD, VRSHR.
- <op> - the operation (for example, ADD, SUB, MUL).
- <shape> - Shape,即前文中的Long (L), Wide (W), Narrow (N).
- <cond> - Condition, used with IT instruction.
- <.dt> - Data type, such as s8, u8, f32 etc.
- <dest> - Destination.
- <src1> - Source operand 1.
- <src2> - Source operand 2.
注: {} 表示可选的参数。
比如:
VADD.I16 D0, D1, D2 @ 16位加法
VMLAL.S16 Q2, D8, D9 @ 有符号16位乘加
四.NEON支持的指令总结
- 运算:和、差、积、商
- 共享的 NEON 和 VFP 指令:涉及加载、多寄存器间的传送、存储
五. NEON 优化技术
在利用NEON优化程序时,有下述几项比较通用的优化技巧。
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. 减少跳转
NEON指令集没有跳转指令,当需要跳转时,我们需要借助ARM指令。在ARM处理器中,分支预测技术被广泛使用。但是一旦分支预测失败,惩罚还是比较高的。因此我们最好尽量减少跳转指令的使用。其实,在有些情况下,我们可以用逻辑运算来代替跳转,如下例所示:
ARM NEON指令集提供了下列指令来帮助用户实现上述逻辑实现:
/***************************************************************/
// 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
/***************************************************************/
• VCEQ, VCGE, VCGT, VCLE, VCLT……
• VBIT, VBIF, VBSL……
减少跳转,不仅仅是在NEON中使用的技巧,是一个比较通用的问题。即使在C程序中,这个问题也是值得注意的。
3. 其它技巧
在ARM NEON编程时,一种功能有时有多种实现方式,但是更少的指令不总是意味着更好的性能,要依据测试结果和profiling数据,具体问题具体分析。下面列出来我遇到的一些特殊情况。
4. 浮点累加指令
通常情况下,我们会用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个周期。