嵌入式程序优化(2)-neon内建函数讲解
1. neon intrinsics介绍
neon intrinsics 是官方提供的 neon内建函数,使开发者不再需要手动使用内嵌汇编也能够使用 neon指令 来优化程序。本来着重讲解此类内建函数的使用方法及简单的代码实例
本文也可用作neon内建函数的快速查找表
PS:本文默认读者们已经熟悉了neon指令的基本情况
2. neon数据类型
- 普通向量:分64bit向量和128bit向量
- 数组向量: 格式如:
type{n}x{m}_{z}, n 为向量元素的宽度, m 为向量元素的个数, z 为向量的个数。一般用于解交织操作, 这种类型一般为一个结构体 请自行查阅资料(解交织)
比如int8x8x3_t类型指的就是该变量中含有 3 个int8x8_t的向量,分别对应成员:int8x8x3_t.val[0], int8x8x3_t.val[1], int8x8x3_t.val[2]
3. neon内建函数
在讲解之前,这里先说明一下内建函数的命名套路,让读者们可以更快的理解内建函数
内建函数的命名规则通常如下:
- vop:指的是neon操作
- {n}or{lane}:其中两者是互斥的,n 代表的是支持输入标量,lane支持选择向量中的一个元素作为输入
- {datatype}:代表处理元素的类型
注意:
1. 带n的函数都含有立即数,此时输入参数不能为变量,且该立即数有范围限制, 具体请查看手册
2. 带qd操作的函数一般只支持32x4跟64x2类型的输入和输出
笔者通过阅读 neon手册 并结合自己的理解将 neon内建函数 分为以下几个部分
- 常用操作
- 装载和存储操作
- 转换类操作
- 加减法类操作
- 乘法类操作
- 数据处理类操作
- 偏移类操作
- 排列类操作
3.1. 常用操作
1. vcreate 创建一个64bit的向量,输入为一个64位的标量
1 uint64_t vcreate_1; 2 int8x8_t vcreate_result; 3 vcreate_result = vcreate_s8(vcreate_1);
2. vdup_n 将标量赋值到向量每一个元素中, vdupq_n支持q寄存器操作, 下面的vmov_n作用同vdup_n
1 /* 2 vldn注意事项, 只有n为 1 时结果才能定义为type{n}x{m}_t类型, 即普通向量, 3 当 n 大于 1 时,此时的载入操作都是解交织的, 所以需要定义为数组向量 4 */ 5 int8_t vldn_data[4] = {0, 1, 2, 3}; 6 int8x8x2_t vld2_result; 7 vld2_result = vld2(vldn_data);
2. vldn_lane 将内存中的数据按n路解交织存入到向量的某一个元素中
1 int8_t vldn_dup_data[4] = {0, 1, 2, 3}; 2 int8x8x4_t vld4_dup_result; 3 vldn_lane_data = vld4_dup_s8(vldn_dup_data);
5. vstn 将向量的值按 n 路交织存放到内存中, 比如有int8x8x3_t的数组向量, 那么将按照int8x8x3_t.val[0][0], int8x8x3_t.val[1][0], int8x8x3_t.val[2][0], int8x8x3_t.val[0][1], int8x8x3_t.val[1][1], int8x8x3_t.val[2][1]这样的顺序存入内存
1 /* 除了跟vldn一样需要注意类型的定义外,还需要知道vstn是无返回的, 即为void */ 2 int8x8x4_t vst4_1; 3 int8_t vst4_data[6] = {0}; 4 vst3_lane_s8(vst4_data, vst4_1, 2);
7. vset_lane 设置向量中某个通道的元素的值, 并将新的向量返回到结果中
1 int8x8_t vset_lane_result; 2 int8x8_t vset_lane_1; 3 int8_t new_val; 4 int vset_index = 3; 5 /* 6 将向量vset_lane_1中下标为vset_index的值设置为new_val, 7 并将设置了新值的新向量返回到vset_lane_result中 8 */ 9 vset_lane_result = vset_lane_s8(new_val, vset_lane_1, vset_index);
8. vget_lane 获取向量中某个通道的值, 并将新的向量返回到结果中
uint16x4_t vector_uint16x4;
int16x4_t vector_int16x4;
vector_uint16x4 = vreinterpret_u16_s16(vector_int16x4);
2. vcombine_type 向量连接,将 2 个 64bit的 d寄存器向量 连接为一个 128bit 的 q寄存器向量,被连接的2个向量必须是相同类型的向量, 格式为 vcombine_type
int16x4_t vgh_int16x4;
int16x8_t vgh_int16x8;
vgh_int16x4 = vget_high_s16(vgh_int16x8);
3.4. 加减法类操作
1. 加法操作 vadd_type常规加法,vaddl_type支撑长整型操作, vaddw_type支持宽整型操作, vhadd_type将结果减半, vrhadd_type将结果减半后舍入(因为减半后会带入小数)
2. vaddq_type, 和vadd_type类似,只是使用的寄存器不同. vaddhn_type支持窄型计算,将四字节加法转换为双字节结果, 该计算会返回四字节的上半部分, vraddhn_type支持结果舍入
3. 减法同理
int16x4_t add_1;
int16x4_t add_2;
int16x4_t add_result;
add_result = vadd_s16(add_1, add_2);
3.5. 乘法类操作
注意:在做neon乘法指令的时候会有大约2个clock的阻塞时间,如果你要立即使用乘法的结果,则就会阻塞在这里,在写neon指令的时候需要特别注意。乘法的结果不能立即使用,可以将一些其他的操作插入到乘法后面而不会有时间的消耗。
1. vmul为常规乘法, vmull支持长整型操作, vmla支持结果累加, vmlal支持长整型运算, vmls支持结果累减(同vmla_type一样), vmlsl是vmls支持长整型操作的变体
2. vqdmulh, 将相乘结果加倍,然后返回结果的高半部分并截断高半部分, 带q表示饱和操作, vqdmulhq支持q寄存器操作, vqrdmulh返回结果不截断而是进行舍入
3. vqdmlal, 支持长整型操作, 将第三个向量和第二个向量相乘,并与第一个向量累加返回结果,结果支持饱和操作, vqdmlsl同, 只是改为相乘结果累减
4. vqdmull, 第一个q为饱和操作, d代表结果加倍, 最后的l代表支持长整型操作
5. vmla_lane_type, lane代表支持下标操作, 参数n是下标,表明第三个向量中,取出下标为n的标量, 并将其与向量二中的每一个向量相乘,将结果和向量一累加
6. vmlal_lan_type支持长整型操作, vqdmlal_lan_type支持饱和、结果加倍和长整型操作, vmls_lane_type系列同理
7. vmul_n_type 即使用标量相乘和向量中的每一个元素相乘, 其余函数类似仅支持的操作不同
int16x4_t result;
int8x8_t vpaddl_1;
result = vpaddl_s8(vpaddl_1);
4. vpmax, p代表相邻向量, 将向量内部的相邻元素进行比较, 输出较大的元素, vpmin同理
int16x4_t vmax_result;
int16x4_t vmax_1;
int16x4_t vmax_2;
resulvmax_resultt = vabd_s16(vmax_1, vmax_2);
9. vabs向量内部元素绝对值化, vqabs支持饱和操作
int16x4_t vabs_result;
int16x4_t vabs_1;
vabs_result = vabs_s16(vabs_1);
10. vneg, 向量内部元素取反, vqneg支持饱和操作
int16x4_t vneg_result;
int16x4_t vneg_1;
vneg_result = vneg_s16(vneg_1);
11. vcls对向量内的元素进行以下操作, 从最高位开始的连续的多个bit进行计数, 如果某一个bit和最高位相同,则计数加1, 计数时不算最高位. 比如0b10110101为0, 0b11110101为3
12. vclz从最高位开始对连续bit进行计数,如果bit为0, 则计数加1,计数时最高位参与计算
13. vcnt计算向量内部每个元素有多少bit位1
int16x4_t vcls_result;
int16x4_t vcls_1;
int16x4_t vcls_2;
vcls_result = vneg_s16(vcls_1, vcls_2);
14. vrecpe对向量内部每个元素求近似导数, 近支持32x2类型的向量
float32x2_t vrecps_result; float32x2_t vrecps_1; float32x2_t vrecps_2; vrecps_result = vrecpe_f32(vrecps_1, vrecps_2);
16. vrsqrte对向量内部的元素求平方根倒数
float32x2_t vrsqrts_result;
float32x2_t vrsqrts_1;
float32x2_t vrsqrts_2;
vrecps_result = vrecpe_f32(vrecps_1, vrecps_2);
18. vmovn 将q寄存器中的高部分赋值到结果中, 可以看出该操作是窄型操作, vqmovn支持饱和操作, vqmovun中的u是指可以将符号类型的输入向量计算出无符号类型的结果
int16x8_t vmovl_result;
int8x8_t vmovl_1;
vmovl_result = vmovn_s8(vmovl_1);
3.7. 偏移类操作
1. vshl 将输入向量(第一个向量)按偏移向量(第二个向量)中的每一个元素的值向左进行偏移, 每个元素中从左侧移出的位将丢失, 偏移向量按照lsb排序并且是有符号的,如果偏移向量中的元素为负值, 那么将向右进行偏移
2. vqshl 支持饱和操作
3. vrshl 支持位移后对数值进行舍入取整数
4. vqrshl 同时支持舍入和饱和操作
5. vshl_n 将输入的标量作为偏移量, 对向量中的每一个元素进行坐移
6. vqshl_n 作用同vshl_n, 支持饱和操作
7. vqshlu_n 作用同vqshl_n, 支持将有符号数转换为无符号数 (u代表可以将输入的有符号数在进行运算后输出为无操作数)
8. vshll_n 作用同vshl_n, 支持长整型操作
int8_t offset = 2;
int8x8_t vshr_n_input;
int8x8_t vshr_n_result;
vshr_n_result = vshr_n_s8(vshr_n_input, offset);
14. vsra_n a表示累加, r表示右移, n表示立即数, 将 2 个输入向量的每一个元素都按照立即数n进行右移, 再将右移后的结果累加到结果向量中
15. vrsra_n 支持将累加后的结果进行舍入
int8x8_t vsri_n_result; int8x8_t vsri_n_input1; int8x8_t vsri_n_input2; int offset = 2; /* 此处立即数n为2, 则有(vsri_n_result[0] & 0b11000000) | (vsri_n_input2[0] & 0b00111111) */ vsri_n_result = vsli_n_s8(vsri_n_input1, vsri_n_input2, offset);
18. vceq 比较第一个输入向量与第二输入向量,然后将比较结果放在对应结果向量的对应元素,相等为 1 , 不相等为 0
19. vcge 比较第一个输入向量与第二输入向量,然后将比较结果放在对应结果向量的对应元素,如果第一个向量元素大于等于第二个向量,则结果为 1, 否则为 0
20. vcle 比较第一个输入向量与第二输入向量,然后将比较结果放在对应结果向量的对应元素,如果第一个向量元素小于等于第二个向量,则结果为 1, 否则为 0
21. vcgt 比较第一个输入向量与第二输入向量,然后将比较结果放在对应结果向量的对应元素,如果第一个向量元素大于第二个向量,则结果为 1, 否则为 0
22. vclt 比较第一个输入向量与第二输入向量,然后将比较结果放在对应结果向量的对应元素,如果第一个向量元素小于第二个向量,则结果为 1, 否则为 0
23. vcage 与vcge类似,但比较的值是向量元素的绝对值
24. vcale 与vcle类似,但比较的值是向量元素的绝对值
25. vcagt 与vcgt类似,但比较的值是向量元素的绝对值
26. vcalt 与vclt类似,但比较的值是向量元素的绝对值
int8x8_t vtst_input1;
int8x8_t vtst_input2;
int8x8_t vtst_result;
vtst_result = vtst_s8(vtst_input1, vtst_input2);
30. vmvn对输入向量中的每个元素进行按位反转
int8x8_t vand_input1;
int8x8_t vand_input2;
int8x8_t vand_result;
vand_result = vand_s8(vand_input1, vand_input2);
**36. **vbsl 有 3 个输入向量,如果第三个向量中的一个元素中的某一个bit为0, 则选择第一个向量中的对应元素的对应bit到目标向量中的对应元素对应bit中, 如果为0, 则从第二个向量中选择的对应元素的对应bit赋给目标向量中的对应元素的对应bit
int n = 5;
int8x8_t vext_input1;
int8x8_t vext_input2;
int8x8_t vext_result;
vext_result = vext_s8(vext_input1, vext_input2, n);
2. vtbln 该指令用于查表赋值, 一共有 2 个输入向量, 第二个输入向量的元素是作为索引, 第一个输入向量作为查找表, 该指令会在使用索引然后在查找表中找到指定索引的元素,然后将找到的元素赋值给结果向量中的元素(与索引所在的元素对应, 假设索引是在第一个元素,则存放在结果向量的第一个元素)
n范围为[0, 4] 其表示的是使用的d寄存器数量,当n大于1时, 第一个参数使用的是数组向量, 如果索引超过查找表范围, 则返回0
3. vtbln:与 vtbln 一样, 但在超出范围时结果向量中的元素不会变为0, 而是保持不变
int8x8_t vrev64_input; int8x8_t vrev64_result; vrev64_result = vrev64_s8(vrev64_input);
7. vtrn将其输入向量的元素视为2 x 2矩阵的元素,并将其转置。 本质上,它将第一个输入向量中的奇数索引的元素与第二个输入向量中的偶数索引的元素交换。
trn示意.png
int8x8_t vzip_input1;
int8x8_t vzip_input2;
int8x8x2_t vzip_result;
vzip_result = vzip_s8(vzip_input1, vzip_input2);
return 0;
4. 代码实例
笔者使用neon内建函数写了几个示例代码,可供各位读者理解
完整代码在笔者的github上:https://github.com/wipping/neon,欢迎各位下载
int32_t neon_intrinsics_matrixMul_float4x4(const float* matrix_left, float* matrix_right, float* matrix_result)
{
if(NULL == matrix_left)
{
return -1;
}
if(NULL == matrix_right)
{
return -1;
}
if(NULL == matrix_result)
{
return -1;
}
int offset = 4;
int result_addr_offset = 0;
int i = 0;
int j = 0;
float32x4_t matrix_right_row[4];
float32x4_t matrix_left_row;
float32x4_t result;
for(i = 0; i < 4; i++)
{
matrix_right_row[i] = vld1q_f32(matrix_right + offset * i);
}
for(j = 0; j < 4; j++)
{
matrix_left_row = vld1q_f32(matrix_left + j * offset);
result = vdupq_n_f32(0);
for(i = 0; i < 4; i++)
{
switch(i)
{
case 0:
result = vmlaq_lane_f32(result, matrix_right_row[i], vget_low_f32(matrix_left_row), 0);
break;
case 1:
result = vmlaq_lane_f32(result, matrix_right_row[i], vget_low_f32(matrix_left_row), 1);
break;
case 2:
result = vmlaq_lane_f32(result, matrix_right_row[i], vget_high_f32(matrix_left_row), 0);
break;
case 3:
result = vmlaq_lane_f32(result, matrix_right_row[i], vget_high_f32(matrix_left_row), 1);
break;
}
}
vst1q_f32(matrix_result, result);
matrix_result = matrix_result + offset;
}
return 0;
}
int32_t neon_intrinsics_matrixMul_float3x3(const float* matrix_left, float* matrix_right, float* matrix_result)
{
if(NULL == matrix_left)
{
return -1;
}
if(NULL == matrix_right)
{
return -1;
}
if(NULL == matrix_result)
{
return -1;
}
int offset = 3;
int left_addr_offset = 0;
int result_addr_offset = 0;
int i = 0;
int j = 0;
float right_tmp[4] = {0};
float left_tmp[4] = {0};
float result_tmp[4] = {0};
float32x4_t matrix_right_row[4];
float32x4_t matrix_left_row;
float32x4_t result;
for(i = 0; i < 3; i++)
{
bzero(right_tmp, sizeof(float) * 4);
memcpy(right_tmp, matrix_right + i * offset, sizeof(float) * offset);
matrix_right_row[i] = vld1q_f32(right_tmp);
}
for(j = 0; j < 3; j++)
{
bzero(left_tmp, sizeof(float) * 4);
memcpy(left_tmp, matrix_left + j * offset, sizeof(float) * offset);
matrix_left_row = vld1q_f32(left_tmp);
result = vdupq_n_f32(0);
for(i = 0; i < 4; i++)
{
switch(i)
{
case 0:
result = vmlaq_lane_f32(result, matrix_right_row[i], vget_low_f32(matrix_left_row), 0);
break;
case 1:
result = vmlaq_lane_f32(result, matrix_right_row[i], vget_low_f32(matrix_left_row), 1);
break;
case 2:
result = vmlaq_lane_f32(result, matrix_right_row[i], vget_high_f32(matrix_left_row), 0);
break;
case 3:
result = vmlaq_lane_f32(result, matrix_right_row[i], vget_high_f32(matrix_left_row), 1);
break;
}
}
bzero(result_tmp, sizeof(float) * 4);
vst1q_f32(result_tmp, result);
memcpy(matrix_result, result_tmp, sizeof(float) * offset);
matrix_result = matrix_result + offset;
}
return 0;
}
int neon_intrinsics_rgb888Tobgr888(uint8_t* image_src, uint8_t* image_dst, uint32_t pixel_num)
{
uint8_t *src = image_src;
uint8_t *dst = image_dst;
int count = pixel_num;
uint8_t bit_mask = 0xff;
uint8x16x3_t vsrc;
uint8x16x3_t vdst;
uint8x16_t add_tmp;
uint8x16_t tmp;
uint8x16_t mask;
/* 注意: 仅支持像素个数8对齐的图像 */
mask = vdupq_n_u8(bit_mask);
add_tmp = vdupq_n_u8(0);
while (count >= 8)
{
vsrc = vld3q_u8(src);//装载源数据
tmp = vdupq_n_u8(0);
//vswp无内建函数 使用其他方法实现
tmp = vaddq_u8(vsrc.val[0], add_tmp);
vsrc.val[0] = vbicq_u8(vsrc.val[0], mask);
vsrc.val[0] = vaddq_u8(vsrc.val[2], add_tmp);
vsrc.val[2] = vbicq_u8(vsrc.val[2], mask);
vsrc.val[2] = vaddq_u8(tmp, add_tmp);
// /* 循环 */
vst3q_u8(dst, vsrc);
dst += 8*3;
src += 8*3;
count -= 8;
}
return 0;
}
int neon_intrinsics_rgb565Torgb888(uint16_t* image_src, uint8_t* image_dst, uint32_t pixel_num)
{
uint16_t *src = image_src;
uint8_t *dst = image_dst;
int count = pixel_num;
uint16x8_t vsrc;
uint8x8x3_t vdst;
/* 注意: 仅支持像素个数8对齐的图像 */
while (count >= 8)
{
vsrc = vld1q_u16(src);//装载源数据
/*
注意: rgb565转rgb88因为通道的bit位宽不同,所以需要使用低位进行补偿, 具体请自行查阅
1. 使用vreinterpretq_u8_u16将源向量中的16bit元素转为8bit元素
2. 使用vshrq_n将8bit元素作移 5 位, 这样即可将位于高位的红色数据从5bit转为8bi
3. 将vreinterpretq_u16_u8 将8bit数据转换为16bit, 此时高8bit为红色通道数据
4. 然后在使用vshrn_n_u16向右移动8个bit, 此时低8bit为红色数据, 并因为窄型数据操作的原因, 所以可以将16bit数据移动并转换为8bit
*/
vdst.val[0] = vshrn_n_u16(vreinterpretq_u16_u8(vshrq_n_u8(vreinterpretq_u8_u16(vsrc), 3)), 5);
/* 使用支持窄型数据操作的右移指令vshrn_n_u16, 将绿色通道的数据移动的低8位, 此时需要清空非绿色通道的数据并且进行补偿, 所以需要使用vshl_n_u8向左移动2bit, 移动时因为是窄型操作, 所以数据从16bit变为8bit, 从而提取出绿色通道数据 */
vdst.val[1] = vshl_n_u8(vshrn_n_u16(vsrc, 5), 2);
/* 先使用vshlq_n_u16将数据向左移动2bit, 然后使用窄型移动指令, 所以数据从16bit变为8bit, 从而提取出蓝色通道数据 */
vdst.val[2] = vmovn_u16(vshlq_n_u16(vsrc, 3));
/* 循环 */
vst3_u8(dst, vdst);
dst+= 8*3;
src += 8;
count -= 8;
}
}
作者:wipping的技术小栈
链接:https://www.jianshu.com/p/3212bdb50469
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。