最近在学习NEON对数据运算的加速,用在神经网络加速的场景下。从基本的库开始了解如何做到这一点。
PS:学习后记:看了公司好多汇编加速代码后,结合自己以前工作,总体感觉如果完全使用汇编代码,代码的可读性确实太差了。我觉得在考虑寄存器、时序、流水线、cache等条件下的C+neon内建函数,会是一个开发效率和执行效率的权衡结果。经过几天的努力,基本把坑填完了。
NEON 向量数据类型是根据以下模式命名的:
(type)x(lanes)_t
int16x4_t 是一个包含四条向量线的向量,每条向量线包含一个有符号 16 位整数。
int8x8_t int8x16_t
int16x4_t int16x8_t
int32x2_t int32x4_t
int64x1_t int64x2_t
uint8x8_t uint8x16_t
uint16x4_t uint16x8_t
uint32x2_t uint32x4_t
uint64x1_t uint64x2_t
float16x4_t float16x8_t
float32x2_t float32x4_t
poly8x8_t poly8x16_t
poly16x4_t poly16x8_t
(1)、正常指令:生成大小相同且类型通常与操作数向量相同的结果向量;
(2)、长指令:对双字向量操作数执行运算,生成四字向量的结果。所生成的元素一般是操作数元素宽度的两倍,
并属于同一类型;
(3)、宽指令:一个双字向量操作数和一个四字向量操作数执行运算,生成四字向量结果。所生成的元素和第一个
操作数的元素是第二个操作数元素宽度的两倍;
(4)、窄指令:四字向量操作数执行运算,并生成双字向量结果,所生成的元素一般是操作数元素宽度的一半;
(5)、饱和指令:当超过数据类型指定的范围则自动限制在该范围内。
(6)、舍入:直接计算四舍五入后结果
(7)、半:运算结果/2。
其中(舍入,半)还没搞清楚,先挖坑以后填。
以下内在函数对向量进行加法运算。结果中的每条向量线都是对每个操作数向量中的相应向量线执行加法运算的结果。执行的运算如下:
向量加法: vadd -> Vr[i]:=Va[i]+Vb[i]
Vr、Va、Vb 具有相等的向量线大小。
向量长型加法: vadd -> Vr[i]:=Va[i]+Vb[i]
Va、Vb 具有相等的向量线大小,结果为向量线宽度变成两倍的 128 位向量。
向量宽型加法: vadd -> Vr[i]:=Va[i]+Vb[i]
向量半加: vhadd -> Vr[i]:=(Va[i]+Vb[i])>>1
向量舍入半加: vrhadd -> Vr[i]:=(Va[i]+Vb[i]+1)>>1
向量饱和加法: vqadd -> Vr[i]:=sat(Va[i]+Vb[i])
高位半部分向量加法: vaddhn -> Vr[i]:=Va[i]+Vb[i]
高位半部分向量舍入加法 vraddhn
向量减法:vsub -> Vr[i]:=Va[i]-Vb[i]
Vr、Va、Vb 具有相等的向量线大小。
向量长型减法:vsub -> Vr[i]:=Va[i]-Vb[i]
Va、Vb 具有相等的向量线大小,结果为向量线宽度变成两倍的 128 位向量。
向量宽型减法:vsub -> Vr[i]:=Va[i]-Vb[i]
Va、Vr 具有相等的向量线宽度
向量饱和减法:vqsub
向量半减:vhsub
高位半部分向量减法:vsubhn
高位半部分向量舍入减法:vrsubhn
向量乘法:vmul -> Vr[i] := Va[i] * Vb[i]
向量乘加:vmla -> Vr[i] := Va[i] + Vb[i] * Vc[i]
向量长型乘加:vmlal -> Vr[i] := Va[i] + Vb[i] * Vc[i]
向量乘减:vmls -> Vr[i] := Va[i] - Vb[i] * Vc[i]
向量长型乘减:vmlsl -> Vr[i] := Va[i] - Vb[i] * Vc[i]
向量高位饱和加倍乘法:vqdmulh
向量高位饱和舍入加倍乘法:vqrdmulh
向量长型饱和加倍乘加:vqdmlal
向量长型饱和加倍乘减:vqdmlsl
向量长型乘法:vmull
向量长型饱和加倍乘法:vqdmull
如果对于一条向量线比较结果为 true,则该向量线的结果为将所有位设置为一。如果对于一条向量线比较结果为 false,则将所有位设置为零。返回类型是无符号整数类型。这意味着可以将比较结果用作 vbsl 内在函数的第一个参数。
向量比较:== , >= , <= , > , < ,绝对值比较
以下内在函数提供包含差值绝对值的运算。
参数间的差值绝对值:vabd --> Vr[i] = | Va[i] - Vb[i] |
差值绝对值 - 长型:vabdl --> Vr[i] = | Va[i] - Vb[i] |
差值绝对值累加:vaba --> Vr[i] = Va[i] + | Vb[i] - Vc[i] |
差值绝对值累加长型:vabal --> Vr[i] = Va[i] + | Vb[i] - Vc[i] |
以下内在函数提供最大值和最小值运算。
vmax -> Vr[i] := (Va[i] >= Vb[i]) ?Va[i] :Vb[i]
vmin -> Vr[i] := (Va[i] >= Vb[i]) ?Vb[i] :Va[i]
按对加:vpadd 前半段r[i] = a[2i] + a[2i+1] ,后半段为b
长型按对加:vpaddl
长型按对加并累加:vpadal
vpmax -> 获取相邻对的最大值,a/b向量内相邻比较,再组合成新向量 vpmin -> 获取相邻对的最小值 vrecpe(q)_type: 求近似倒数,type是f32或者u32 vrecps(q)_type:(牛顿 - 拉夫逊迭代) vrsqrte(q)_type: 计算输入值的平方根的倒数,type是f32或者u32。输入值不能是负数,否则计算出来的值是nan。 vrsqrts(q)_type 向量左移:vshl --> Vr[i] := Va[i] << Vb[i](负值右移) 向量按常数右移: vshr_n --> Vr[i] := Va[i] >> n 向量右移并插入vsri_n 加载并存储某类型的单个向量。 加载:将一个元素复制为向量 加载向量并部分赋值,语义为: 存储:将neon向量数据保存到数组中 存储:将向量中一个元素保存到内存*ptr=val[lane] 以下内在函数加载或存储 n-元素结构。数组结构的定义方式类似,例如 int16x4x2_t 结构定义如下: 加载32,48,64个unit8到寄存器 图像处理中可以用来拆分RGB或YUV通道数据。 存储32,48,64个unit8到内存,结果同样是交叉的。 复制2,3,4个unit8,每个成unit8x8向量 加载2条向量并部分赋值,语义为: 存储2条向量中lane元素到ptr 从向量提取向量线:uint8_t vget_lane_u8(uint8x8_t vec, __constrange(0,7) int lane); 组合: 从浮点转换:int32x2_t vcvt_s32_f32(float32x2_t a); 表查找:uint8x8_t vtbl1_u8(uint8x8_t a, uint8x8_t b); vtbl2(/3/4)_type: 数组长度扩大到2个vector 向量与标量进行的乘加 向量与标量进行的乘加 vrev(bit)_(type) 按位非 vtrn(q)_type: 将两个输入vector的元素通过转置生成一个有两个vector的矩阵 vzip(q)_type: 将两个输入vector的元素通过交叉生成一个有两个vector的矩阵 vuzp(q)_type: 将两个输入vector的元素通过反交叉生成一个有两个vector的矩阵(通过这个可实现n-way 交织) 绝对值:Vd[i] = |Va[i]| vabs(q)_type vcls : counts the number of consecutive bits, starting from the most 计算前导零数目vclz_type vclz: counts the number of consecutive zeros, starting from the most 计算设置位数 vcnt_type 数据中1的个数
vpmax -> Vr[i] := (Va[2i] >= Va[2i+1]) ?Va[2i] :Va[2i+1], iint8x8_t vpmax_s8(int8x8_t a, int8x8_t b); // VPMAX.S8 d0,d0,d0
float32x2_t vpmax_f32(float32x2_t a, float32x2_t b); // VPMAX.F32 d0,d0,d0
倒数/平方根
近似倒数
倒数
注:vrecpe_type计算倒数能保证千分之一左右的精度,如1.0的倒数为0.998047。执行完如下语句后能提高百万分之一精度
float32x4_t recip = vrecpeq_f32(src);此时能达到千分之一左右的精度,如1.0的倒数为0.998047
recip = vmulq_f32 (vrecpsq_f32 (src, recip), recip);执行后能达到百万分之一左右的精度,如1.0的倒数为0.999996
recip = vmulq_f32 (vrecpsq_f32 (src, recip), recip);再次执行后能基本能达到完全精度,如1.0的倒数为1.000000近似平方根倒数
平方根倒数
变量移位
按有符号变量移位
向量饱和左移:vqshl(负值右移)
向量舍入左移:vrshl(负值右移)
向量饱和舍入左移:vqrshl(负值右移)按常数移位
向量按常数左移: vshl_n --> Vr[i] := Va[i] << n
向量舍入按常数右移:vrshr_n
向量按常数右移并累加:vsra_n
向量舍入按常数右移并累加:vrsra_n
向量饱和按常数左移: vqshl_n
向量有符号‑>无符号饱和按常数左移:vqshlu_n
向量窄型饱和按常数右移:vshrn_n
向量有符号‑>无符号窄型饱和按常数右移:vqshrun_n
向量有符号‑>无符号舍入窄型饱和按常数右移:vqrshrun_n
向量窄型饱和按常数右移:vqshrn_n
向量舍入窄型按常数右移:vrshrn_n
向量舍入窄型饱和按常数右移:vqrshrn_n
向量扩大按常数左移:vshll_n移位并插入
向量左移并插入vsli_n加载存储
加载:将数组首地址转换成neon向量数据uint8x16_t vld1q_u8(__transfersize(16) uint8_t const * ptr);
uint8x16_t vld1q_dup_u8(__transfersize(1) uint8_t const * ptr);
src向量拷贝到dst,并且dst[lane] = *ptruint8x16_t vld1q_lane_u8(__transfersize(1) uint8_t const * ptr, uint8x16_t src, __constrange(0,15) int lane);
void vst1q_u8(__transfersize(16) uint8_t * ptr, uint8x16_t val);
void vst1q_lane_u8(__transfersize(1) uint8_t * ptr, uint8x16_t val, __constrange(0,15) int lane);
struct int16x4x2_t
{
int16x4_t val[2];
};
uint8x16x2_t vld2q_u8(__transfersize(32) uint8_t const * ptr);
uint8x16x3_t vld3q_u8(__transfersize(48) uint8_t const * ptr);
uint8x16x4_t vld4q_u8(__transfersize(64) uint8_t const * ptr);
以上数据加载后结果是交叉的,以vld2q_u8为例,uint8x16x2_t dst={
{ptr[0],ptr[2],ptr[4],...},
{ptr[1],ptr[3],ptr[5]...}};
void vst2q_u8(__transfersize(32) uint8_t * ptr, uint8x16x2_t val);
void vst3q_u8(__transfersize(48) uint8_t * ptr, uint8x16x3_t val);
void vst4q_u8(__transfersize(64) uint8_t * ptr, uint8x16x4_t val);
uint8x8x2_t vld2_dup_u8(__transfersize(2) uint8_t const * ptr);
uint8x8x3_t vld3_dup_u8(__transfersize(3) uint8_t const * ptr);
uint8x8x4_t vld4_dup_u8(__transfersize(4) uint8_t const * ptr);
src向量拷贝到dst,并且dst[][lane] = *ptruint16x8x2_t vld2q_lane_u16(__transfersize(2) uint16_t const * ptr, uint16x8x2_t src, __constrange(0,7) int lane);
void vst2q_lane_u16(__transfersize(2) uint16_t * ptr, uint16x8x2_t val, __constrange(0,7) int lane);
初始化、设置值
在向量内设置向量线:
uint8x8_t vset_lane_u8(uint8_t value, uint8x8_t vec, __constrange(0,7) int lane);
从位模式初始化向量:如float16x4_t vcreate_f16(uint64_t a);
将所有向量线设置为相同的值:uint8x8_t vdup_n_u8(uint8_t value);
将向量的所有向量线设置为一条向量线的值:
uint8x8_t vdup_lane_u8(uint8x8_t vec, __constrange(0,7) int lane);组合拆分
int8x16_t vcombine_s8(int8x8_t low, int8x8_t high);
结果r = {low,high}; low指下标小的一半,high指下标大的一半。
拆分:
int8x8_t vget_high_s8(int8x16_t a);
int8x8_t vget_low_s8(int8x16_t a);
向量提取: vext_(type)
vext_type: 取第2个输入vector的低n个元素放入新vector的高位,新vector剩下的元素取自第1个输入vector最高的几个元素(可实现vector内元素位置的移动)
vextq_type:
如:src1 = {1,2,3,4,5,6,7,8}
src2 = {9,10,11,12,13,14,15,16}
dst = vext_type(src1,src2,3)时,则dst = {4,5,6,7,8, 9,10,11}向量类型转换
转换为浮点:float32x2_t vcvt_f32_s32(int32x2_t a);
在浮点之间转换:float16x4_t vcvt_f16_f32(float32x4_t a);
向量窄型整数:int8x8_t vmovn_s16(int16x8_t a);
向量长移:int16x8_t vmovl_s8(int8x8_t a);
向量饱和窄型整数:int8x8_t vqmovn_s16(int16x8_t a);
向量饱和窄型整数有符号‑>无符号的转换: uint8x8_t vqmovun_s16(int16x8_t a);查表
tbl1_type: b是索引,根据索引去a中搜索相应的元素,并输出新的vector,超过范围的索引返回的是0.
如:a = {1,2,3,4,5,6,7,8}
b= {0,0,1,1,2,2,7,8}
dst = vtbl1_u8(a, b)时,则dst = {1,1,2,2,3,3,8,0}
如:a.val[0] = {1,2,3,4,5,6,7,8}
a.val[[1]] = {9,10,11,12,13,14,15,16}
b= {0,0,1,1,2,2,8,10}
dst = vtbl2_u8(a, b)时,则dst = {1,1,2,2,3,3,9,11}
扩展表查找:
uint8x8_t vtbx1_u8(uint8x8_t pad, uint8x8_t table, uint8x8_t index);
区别在于TBL miss时填充0,TBX miss时填充pad对应位置的值。另一种理解可以是:根据index搜索table的元素是用来替换pad中的元素,并输出替换后的新vector,当索引超出范围时,则不替换pad中相应的元素。标量运算
int16x4_t vmla_lane_s16(int16x4_t a, int16x4_t b, int16x4_t v, __constrange(0,3) int l);
vmla_lane_type: r[i] = a[i] + b[i] * v[l];
向量与标量进行的扩大乘加(同上,长度变化)
int32x4_t vmlal_lane_s16(int32x4_t a, int16x4_t b, int16x4_t v, __constrange(0,3) int l);
向量与标量进行的扩大饱和加倍乘加vqdmlal_lane_type
更多信息:
vfma_f32:ri = ai + bi * vi 在加法之前,bi、vi相乘的结果不会被四舍五入
vqdmlal_type: ri = sat(ai + bi * vi) bi/vi的元素大小是ai的一半
vqdmlal_n_type: ri = sat(ai + bi * v)
vqdmlal_lane_type: ri = sat(ai + bi * v[l])
其他所有运算用时参考:
向量与标量进行的扩大乘加
向量与标量进行的扩大饱和加倍乘加
向量与标量进行的乘减
向量与标量进行的扩大乘减
向量与标量进行的扩大饱和加倍乘减
向量乘以标量
向量与标量进行的长型乘法
向量与标量进行的长型乘法
向量与标量进行的饱和加倍长型乘法
向量与标量进行的饱和加倍长型乘法
向量与标量进行的高位饱和加倍乘法
向量与标量进行的高位饱和加倍乘法
向量与标量进行的高位饱和舍入加倍乘法
向量与标量进行的高位舍入饱和加倍乘法
向量与标量进行的乘加
向量与标量进行的扩大乘加
向量与标量进行的扩大饱和加倍乘加
向量与标量进行的乘减
向量与标量进行的扩大乘减
向量与标量进行的扩大饱和加倍乘减逻辑、转置
反转
uint8x8_t src = {1,2,3,4,5,6,7,8};
dst = vrev16_u8(src) --> dst = {2,1,4,3,6,5,8,7}
dst = vrev64_u8(src) --> dst = {8,7,6,5,4,3,2,1}逻辑位运算
按位与
按位或
按位异或(EOR 或 XOR)
位清零
按位或补
按位选择转置元素
如:src.val[0] = {1,2,3,4,5,6,7,8}
src.val[[1]] = {9,10,11,12,13,14,15,16}
dst = vtrn_u8(src.val[0], src.val[[1]])时,
则 dst.val[0] = {1,9, 3,11,5,13,7,15}
dst.val[[1]] = {2,10,4,12,6,14,8,16}交叉存取元素
如:src.val[0] = {1,2,3,4,5,6,7,8}
src.val[[1]] = {9,10,11,12,13,14,15,16}
dst = vzip_u8(src.val[0], src.val[[1]])时,
则dst.val[0] = {1,9, 2,10,3,11,4,12}
dst.val[[1]] = {5,13,6,14,7,15,8,16}反向交叉存取元素
如:src.val[0] = {1,2,3,4,5,6,7,8}
src.val[[1]] = {9,10,11,12,13,14,15,16}
dst = vuzp_u8(src.val[0], src.val[[1]])时,
则dst.val[0] = {1,3,5,7,9, 11,13,15}
dst.val[[1]] = {2,4,6,8,10,12,14,16}其他
饱和绝对值:Vd[i] = sat(|Va[i]|)
求反:Vd[i] = ‑ Va[i] vneg_type
饱和求反:sat(Vd[i] = ‑ Va[i])
计算前导符号位数目vcls_type
significant bit,that are the same as the most significant bit, in each element in a
vector, and places the count in the result vector.
significant bit, in each element in a vector, and places the count in result vector.参考链接