今天我们继续介绍NEON intrinsics的指令知识,上篇大前端CPU优化技术--NEON intrinsics开篇中已经介绍了部分指令的作用。本篇文章除了介绍指令还会附上场景示例,方便大家更深刻的理解,废话不多说我们继续前面的指令讲解。
//将一个64bit的数据装入vector中,并返回元素类型为type的vector。
Result_t vcreate_type(Scalar_t N);
//用类型为type的数值,初始化一个元素类型为type的新vector的所有元素。
Resutl_t vdup_type(Scalar_t N);
Result_t vmov_n_type(Scalar_t N);
Result_t vdup_n_type(Scalar_t N);
Result_t vdupq_n_type(Scalar_t N);
Result_t vmovq_n_type(Scalar_t N);
//用元素类型为type的vector的某个元素,初始化一个元素类型为type的新vector的所有元素
Result_t vdup_lane_type(Vector_t N, int n);
vdupq_lane_type
max\min操作
// 基本的 max, min
Result_t vmax_type(Vector1_t N, Vector2_t M);
Result_t vmin_type(Vector1_t N, Vector2_t M);
// pairwise 类型的 max, min
Result_t vpmax_type(Vector1_t N, Vector2_t M);
Result_t vpmin_type(Vector1_t N, Vector2_t M);
绝对值
// 基本的绝对值计算
Result_t vabs_type(Vector_t N);
// 差的绝对值操作
Result_t vabd_type(Vector1_t N, Vector2_t M);
// L(Long)类型, 差的绝对值
Result_t vabdl_type(Vector1_t N, Vector2_t M);
// 差的绝对值,并和另一个向量相加
Result_t vaba_type(Vector1_t N, Vector2_t M, Vector3_t P);
// L(Long)类型, 差的绝对值,并和另一个向量相加, 输出是输入长度的两倍
Result_t vabal_type(Vector1_t N, Vector2_t M, Vector3_t P);
取反
// 基本的取反操作
Result_t vneg_type(Vector_t N);
// Q(Saturated)类型,带饱和的取反操作
Result_t vqneg_type(Vector_t N);
按位统计 0 或 1 的个数
// 统计每个通道 1 的个数
Result_t vcnt_type(Vector_t N);
// 从符号位开始,统计每个通道中与符号位相同的位的个数,且这些位必须是连续的
Result_t vcls_type(Vector_t N);
// 从符号位开始,统计每个通道连续0的个数
Result_t vclz_type(Vector_t N);
倒数
// 对每个通道近似求倒,f32或者u32
Result_t vrecpe_type(Vector_t N);
// 对每个通道使用 newton-raphson 求倒
Result_t vrecps_type(Vector1_t N, Vector2_t M);
平方根倒数
// 对每个通道平方根近似求倒
Result_t vrsqrte_type(Vector_t N);
// 对每个通道使用 newton-raphson 平方根近似求倒
Result_t vrsqrts_type(Vector1_t N, Vector2_t M);
向量赋值
// N(Narrow) 类型的赋值,取输入每个通道的高半部分,赋给目的向量
Result_t vmovn_type(Vector_t N);
// L(long) 类型的赋值,使用符号拓展或者 0 拓展的方式,将输入通道的数据赋给输出向量
Result_t vmovl_type(Vector_t N);
// QN(Saturated, Narrow) 类型的赋值,饱和的方式赋值,输出是输入宽度的两倍
Result_t vqmovn_type(Vector_t N);
// QN(Saturated, Narrow) 类型的赋值,饱和的方式赋值,输出是输入宽度的两倍,而且输入为有符号数据,输出无符号
Result_t vqmovun_type(Vector_t N);
重新解析
//将元素类型为type2的vector转换为元素类型为type1的vector。数据重新解析
Result_t vreinterpret_DSTtype_SRCtype(Vector1_t N);
两个 64bit 向量组合成一个 128bit 向量
Result_t vcombine_type(Vector1_t N, Vector2_t M);
提取 128bit 向量的高半部分或则低半部分
//获取128bit vector的高半部分元素,输出的是元素类型相同的64bit vector。
Result_t vget_high_type(Vector_t N);
//获取128bit vector的低半部分元素,输出的是元素类型相同的64bit vector。
Result_t vget_low_type(Vector_t N);
// 统计向量每个元素有多少bit位是1
Result_t vcnt_type:
Result_t vcls_type:
Result_t vclz_type:
Result_t vcntq_type:
一个 24 位 RGB 图像,其中图像是一个像素数组,每个像素都有一个红色、蓝色和绿色元素,RGB数据是交错的,我们希望对它们进行去插错,并将这些值放在单独的颜色数组。
int num8x16 = len_color / 16;
uint8x16x3_t intlv_rgb;
for (int i=0; i < num8x16; i++) {
intlv_rgb = vld3q_u8(rgb+3*16*i);
vst1q_u8(r+16*i, intlv_rgb.val[0]);
vst1q_u8(g+16*i, intlv_rgb.val[1]);
vst1q_u8(b+16*i, intlv_rgb.val[2]);
}
题解:
uint8x16_t
存储在给定地址的函数矩阵乘法是在许多数据密集型应用程序中执行的操作。矩阵乘法过程如下:
A- 在第一个矩阵中取一行
B- 使用第二个矩阵中的列执行此行的点积
C- 将结果存储在新矩阵的相应行和列中
4*4的矩阵
NEON代码使用内部函数将两个 4x4
矩阵相乘。由于我们要处理的数值数量很少且固定,所有这些值都可以同时放入处理器的 Neon 寄存器中,因此我们可以完全展开循环。
void matrix_mul_4x4_neon(float32_t *a, float32_t *b, float32_t *c)
{
// these are the columns A
float32x4_t A0;
float32x4_t A1;
float32x4_t A2;
float32x4_t A3;
// these are the columns B
float32x4_t B0;
float32x4_t B1;
float32x4_t B2;
float32x4_t B3;
// these are the columns C
float32x4_t C0;
float32x4_t C1;
float32x4_t C2;
float32x4_t C3;
A0 = vld1q_f32(a);
A1 = vld1q_f32(a+4);
A2 = vld1q_f32(a+8);
A3 = vld1q_f32(a+12);
// Zero accumulators for C values
C0 = vmovq_n_f32(0);
C1 = vmovq_n_f32(0);
C2 = vmovq_n_f32(0);
C3 = vmovq_n_f32(0);
// Multiply accumulate in 4x1 blocks, i.e. each column in C
B0 = vld1q_f32(b);
C0 = vfmaq_laneq_f32(C0, A0, B0, 0);
C0 = vfmaq_laneq_f32(C0, A1, B0, 1);
C0 = vfmaq_laneq_f32(C0, A2, B0, 2);
C0 = vfmaq_laneq_f32(C0, A3, B0, 3);
vst1q_f32(c, C0);
B1 = vld1q_f32(B+4);
C1 = vfmaq_laneq_f32(C1, A0, B1, 0);
C1 = vfmaq_laneq_f32(C1, A1, B1, 1);
C1 = vfmaq_laneq_f32(C1, A2, B1, 2);
C1 = vfmaq_laneq_f32(C1, A3, B1, 3);
vst1q_f32(c+4, C1);
B2 = vld1q_f32(b+8);
C2 = vfmaq_laneq_f32(C2, A0, B2, 0);
C2 = vfmaq_laneq_f32(C2, A1, B2, 1);
C2 = vfmaq_laneq_f32(C2, A2, B2, 2);
C2 = vfmaq_laneq_f32(C2, A3, B2, 3);
vst1q_f32(c+8, C2);
B3 = vld1q_f32(b+12);
C3 = vfmaq_laneq_f32(C3, A0, B3, 0);
C3 = vfmaq_laneq_f32(C3, A1, B3, 1);
C3 = vfmaq_laneq_f32(C3, A2, B3, 2);
C3 = vfmaq_laneq_f32(C3, A3, B3, 3);
vst1q_f32(c+12, C3);
}
题解:
float32x4_t
中的函数。将float32x4_t
值乘以另一个float32x4_t
元素,然后将结果加第三个float32x4_t
,然后再返回结果。float32x4_t
存储在给定地址的函数。对于更大的矩阵,我们可以以4*4作为矩阵块来乘,但是需要0填充矩阵。下面来个更通用的矩阵乘法,代码如下:
void matrix_multiply_neon(float32_t *a, float32_t *b, float32_t *c,
uint32_t n, uint32_t m, uint32_t k)
{
/*
* Multiply matrices A and B, store the result in C.
* It is the user's responsibility to make sure the matrices are compatible.
*/
int A_idx;
int B_idx;
int C_idx;
// these are the columns of a 4x4 sub matrix of A
float32x4_t A0;
float32x4_t A1;
float32x4_t A2;
float32x4_t A3;
// these are the columns of a 4x4 sub matrix of B
float32x4_t B0;
float32x4_t B1;
float32x4_t B2;
float32x4_t B3;
// these are the columns of a 4x4 sub matrix of C
float32x4_t C0;
float32x4_t C1;
float32x4_t C2;
float32x4_t C3;
for (int i_idx=0; i_idx
处理图像边缘时,经常会有使用常数填充边界的情况。
NEON 开发中,可以使用DUP
指令用数据初始化向量,然后使用EXT
指令提取数据组建新向量。
// 构造边界填充向量
uint8_t a_0 =0;
uint8x8_t b_c0 = v_dup_n_u8(a_0);
// 构建a_1
uint8x8_t a_1 = vext_u8(b_c0, a_0, 5)
// 使用 vext 构建边界向量,a0 表示从纵坐标为 0 起始的向量
uint8x8_t c_border = vext_u8(a_1, b_c0, 3)
注:EXT指令还常常用于滤波向量的重组操作。
本文我们完结了NEON intrinsics指令知识的浅析,希望对大家在工作场景有抛砖引玉的作用,但是性能调优是个日积月累,持续反馈的过程,后面我们会继续介绍NEON 汇编开发和NEON性能调优进阶,敬请期待。