ARM NEON指令集总结

Posted feng..liu

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ARM NEON指令集总结相关的知识,希望对你有一定的参考价值。

ARM的NEON是类似于X86的SSE2的一种优化的指令集,主要就是为了实现SIMD全称Single Instruction Multiple Data,单指令多数据流,能够复制多个操作数,并把它们打包在大型寄存器的一组指令集。简单来说就是处理一些算法的时候,可以并行处理,大大提高了效率。

android手机上大部分都是ARM架构的,我们开启NEON后就可以使用这些指令集了,当然可以使用汇编,但是也为我们提供了封装好的方法,总结一下数据以及函数的解析规律。

NEON 的矢量数据类型
数据类型有下列的模式,首先是不是数组的

<type><size>x<number_of_lanes>_t
1
例如
int16x4_t 代表4个16bit的数据,也就是4个short
float32x4_t 代表4个32bit的float数据

然后是数组类型


<type><size>x<number_of_lanes>x<length_of_array>_t
1
2
类似于这样

struct int16x4x2_t{
int16x4_t val[2];
} <var_name>;
1
2
3
4
然后我发现还有一个poly的类型,好像多用在有限域的中,密码学等方面,这个暂时不管吧,基本算法也用不到。

函数指令解释
关于函数的指令解释,参考这篇文章:https://blog.csdn.net/xiongtiancheng/article/details/77103810
再参考这篇文字速查:https://blog.csdn.net/billbliss/article/details/78924636

方便观看我就直接转载过来了。

arm neon 指令分类:

正常指令(q)
正常指令可对上述任意向量类型执行运算,并生成大小相同且类型通常与操作数向量相同的结果向量。

长指令(l)
长指令对双字向量操作数执行运算,并生成四字向量结果。 所生成的元素通常是操作数元素宽度的两倍,并属于同一类型。

宽指令(w)
宽指令对一个双字向量操作数和一个四字向量操作数执行运算。 此类指令生成四字向量结果。 所生成的元素和第一个操作数的元素是第二个操作数元素宽度的两倍。

窄指令(n)
窄指令对四字向量操作数执行运算,并生成双字向量结果。 所生成的元素通常是操作数元素宽度的一半。

饱和指令(q)
通过在 V 和指令助记符之间使用 Q 前缀可以指定饱和指令。

上述的指令分类区别我感觉就是操作对象与返回对象的区别了。

数据类型 x 的饱和范围 (s 就是signed,有符号的意思,u就是unsigned,无符号的意思)
s8 –2^7 <= x < 2^7
s16 –2^15 <= x < 2^15
s32 –2^31 <= x < 2^31
s64 –2^63 <= x < 2^63
u8 0 <= x < 2^8
u16 0 <= x < 2^16
u32 0 <= x < 2^32
u64 0 <= x < 2^64

neon的寄存器:
有16个128位四字到寄存器Q0-Q15,32个64位双子寄存器D0-D31,两个寄存器是重叠的。

arm_neon.h 中的函数介绍
例如
int8x8_t vadd_s8 (int8x8_t __a, int8x8_t __b);
v是向量操作,可以认为就是neon函数,add 是相加,s8表示结果是s8类型(向量)
int8x16_t vaddq_s8 (int8x16_t __a, int8x16_t __b);
v是向量操作,可以认为就是neon函数,add 是相加,后面的q代表正常指令,s8表示结果是s8类型(向量)
int8x8_t vqadd_s8 (int8x8_t __a, int8x8_t __b);
v是向量操作,可以认为就是neon函数,后面的q代表饱和指令,add 是相加,s8表示结果是s8类型(向量)
int8x8_t vshr_n_s8 (int8x8_t __a, const int __b);
v是向量操作,可以认为就是neon函数,shr是右移位,n表示参数中有个基本数据类型,也就是不是向量或指针,s8表示结果是s8类型(向量)
int8_t vget_lane_s8 (int8x8_t __a, const int __b);
v是向量操作,可以认为就是neon函数,shr是右移位,lane表示操作向量中的某个元素,s8表示结果是s8类型(向量)
int8x8_t vget_high_s8 (int8x16_t __a); //ri = a(i+4);
v是向量操作,可以认为就是neon函数,get是取值,high表示取高64位,s8表示结果是s8类型(向量)
int8x8_t vget_low_s8 (int8x16_t __a); //ri = ai;
v是向量操作,可以认为就是neon函数,get是取值,low表示取低64为,s8表示结果是s8类型(向量)

综上所述,可以总结函数的定义如下

v<noen函数前缀>q<饱和操作>ops<具体操作>tyep<指令类型 q,l,w,n>_flag<标识 n,lane,high or low>_dtype<返回值类型或参数类型>
1
arm_neon.h 支持的操作
add 加法
mul 乘法
sub 减法
mla 乘加
mls 乘减
ceq 比较,类似与 ==
cge 比较,类似与 >=
cle 比较,类似与 <=
cgt 比较,类似与 >
clt 比较,类似与 <
tst 做与运算后,判断是否等于0 ,ri = (ai & bi != 0) ? 1…1:0…0;
abd 两个向量相减后的绝对值,vabd -> ri = |ai - bi|;
max 求最大值,ri = ai >= bi ? ai : bi;
min 求最小值,ri = ai >= bi ? bi : ai;
shl 左移位, ri = ai << b;
shr 右移位, ri = ai >> b;
abs 求绝对值,ri = |ai|;
neg 取反,ri = -ai;
mvn 按位取反,ri = ~ai;
and 与运算,ri = ai & bi;
orr 或运算,ri = ai | bi;
eor 异或运算,ri = ai ^ bi;
cls 计算连续相同的位数
get 取值,从向量中取出一个值,所谓的向量可以认为是一个数组,给数组中的某个元素赋值
set 赋值,给向量中赋值
dup 构造一个向量,并赋上初始值,ri = a;
combine 合并操作,把两个向量合并
mov 改变数据类型,数据范围,比如把u8 变成u16,或者u16变成u8
zip 压缩操作
uzp 解压操作
ld1 加载数据,给定的buffer 指针中拷贝数据,注意是ld后面的是数字1,而不是字母l
st1 拷贝数据,将neon数据类型拷贝到指定buffer中

实例分析
这里分析一下从yuv422转yuv420P的用法。

void yuy2_to_yuv420p(const uint8_t * const srcSlice[], const int srcStride[],
int imageWidth, int imageHeight, uint8_t * const dst[],
const int dstStride[]) {
//ffmpeg两个AVFrame的的数据转换
//yuy2的数据
const uint8_t *yuy2Line = (const uint8_t *) srcSlice[0];
//每行数据个数
const int yuy2Pitch = srcStride[0];

//目标数据的y指针
uint8_t *yLine = (uint8_t *) dst[0];
//目标数据每行数据量,下面同理
const int yPitch = dstStride[0];
uint8_t *uLine = (uint8_t *) dst[1];
const int uPitch = dstStride[1];
uint8_t *vLine = (uint8_t *) dst[2];
const int vPitch = dstStride[2];
const unsigned int linePairs = imageHeight / 2;

const unsigned int groups = imageWidth / 32;
const unsigned int tails = imageWidth & 31;
const unsigned int tailPairs = tails / 2;
for (unsigned int row = 0; row < linePairs; row++) {
const uint8_t *yuy2Ptr = (const uint8_t *) yuy2Line;
uint8_t *yPtr = yLine;
uint8_t *uPtr = uLine;
uint8_t *vPtr = vLine;

//每次读取32个像素填充到数据到dst
for (unsigned int i = 0; i < groups; i++) {
//yvyu yuyv
//取16*4个字节 也就是16个yuyv数据,32个像素
//分为4个通道存储,将yuyv分离
//val[0] y0 y2 y4 y6 y8 y10 y12 y14 y16 y18 y20 y22 y24 y26 y28 y30
//val[2] y1 y3 y5 y7 y9 y11 y13 y15 y17 y19 y21 y23 y25 y27 y29 y31
//val[1] u0....
//val[3] v0...
const uint8x16x4_t srcYUYV = vld4q_u8(yuy2Ptr);

uint8x16x2_t srcY;

//16个y
srcY.val[0] = srcYUYV.val[0];
//另外16个y
srcY.val[1] = srcYUYV.val[2];
//回推到yPtr,所有的y,两个向量交替回推
vst2q_u8(yPtr, srcY);
//回推到uPtr,所有的u
vst1q_u8(uPtr, srcYUYV.val[1]);
//回推到uPtr,所有的v
vst1q_u8(vPtr, srcYUYV.val[3]);
yuy2Ptr += 64;
yPtr += 32;
uPtr += 16;
vPtr += 16;
}
//上面的玄循环结束之后
//变成类似 yPtr yyyy ... 0 2
// uPtr uu .. 1
// vPtr vv .. 3

//对32求余后剩余的数据也填充进去,一次两个像素
for (unsigned int i = 0; i < tailPairs; i++) {
yPtr[0] = yuy2Ptr[0];
yPtr[1] = yuy2Ptr[2];
*(uPtr++) = yuy2Ptr[1];
*(vPtr++) = yuy2Ptr[3];
yuy2Ptr += 4;
yPtr += 2;
}
//奇数宽高的处理,奇数yuv我也不太清楚是怎么排列的,基本是没见过奇数
//看这里的处理是再保留一个y和一个uv,舍去一个y
if (tails & 1) {
yPtr[0] = yuy2Ptr[0];
uPtr[0] = yuy2Ptr[1];
vPtr[0] = yuy2Ptr[3];
}

//接着处理第二行,一个循环处理两行数据
yuy2Line += yuy2Pitch;
yLine += yPitch;
uLine += uPitch;
vLine += vPitch;

yuy2Ptr = (const uint8_t *) yuy2Line;
yPtr = yLine;

//处理第二行数据,奇数行保留yuv,偶数行只保留Y。。这里我指的是像素的行数,从1开始
//下面的处理和上面类似
for (unsigned int i = 0; i < groups; i++) {
const uint8x16x4_t srcYUYV = vld4q_u8(yuy2Ptr);
uint8x16x2_t srcY;
srcY.val[0] = srcYUYV.val[0];
srcY.val[1] = srcYUYV.val[2];
vst2q_u8(yPtr, srcY);
yuy2Ptr += 64;
yPtr += 32;
}
for (unsigned int i = 0; i < tailPairs; i++) {
yPtr[0] = yuy2Ptr[0];
yPtr[1] = yuy2Ptr[2];
yuy2Ptr += 4;
yPtr += 2;
}
if (tails & 1)
yPtr[0] = yuy2Ptr[0];

yuy2Line += yuy2Pitch;
yLine += yPitch;
}

//高度奇数,再处理最后一行
if (imageHeight & 1) {
const uint8_t *yuy2Ptr = (const uint8_t *) yuy2Line;
uint8_t *yPtr = yLine;
uint8_t *uPtr = uLine;
uint8_t *vPtr = vLine;
for (unsigned int i = 0; i < groups; i++) {
const uint8x16x4_t srcYUYV = vld4q_u8(yuy2Ptr);
uint8x16x2_t srcY;
srcY.val[0] = srcYUYV.val[0];
srcY.val[1] = srcYUYV.val[2];
vst2q_u8(yPtr, srcY);
vst1q_u8(uPtr, srcYUYV.val[1]);
vst1q_u8(vPtr, srcYUYV.val[3]);
yuy2Ptr += 64;
yPtr += 32;
uPtr += 16;
vPtr += 16;
}
for (unsigned int i = 0; i < tailPairs; i++) {
yPtr[0] = yuy2Ptr[0];
yPtr[1] = yuy2Ptr[2];
*(uPtr++) = yuy2Ptr[1];
*(vPtr++) = yuy2Ptr[3];
yuy2Ptr += 4;
yPtr += 2;
}
if (tails & 1) {
yPtr[0] = yuy2Ptr[0];
uPtr[0] = yuy2Ptr[1];
vPtr[0] = yuy2Ptr[3];
}
}
}
————————————————
版权声明:本文为CSDN博主「JabamiLight」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/a568478312/article/details/80937797

以上是关于ARM NEON指令集总结的主要内容,如果未能解决你的问题,请参考以下文章

利用ARM NEON intrinsic优化常用数学运算

RISC-V指令集架构特点及其总结

优化系列汇编优化技术:ARM架构64位(AARCH64)汇编优化及demo

ARM体系结构总结

如何在运行时检测 NEON 和 Helium 指令集的可用性

Thumb指令集与ARM指令集的差别