大前端CPU优化技术--NEON intrinsics进阶

前言

今天我们继续介绍NEON intrinsics的指令知识,上篇大前端CPU优化技术--NEON intrinsics开篇中已经介绍了部分指令的作用。本篇文章除了介绍指令还会附上场景示例,方便大家更深刻的理解,废话不多说我们继续前面的指令讲解。

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

Data processing 

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:

NEON intrinsics应用

RGB 去隔行示例

一个 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]);
}

 题解:

  • vld3q_u8:通过加载 3*16 字节内存的连续区域来返回uint8x16x3_t的函数。加载的每个字节都以交替模式放置在三个uint8x16_t数组之一。
  • vst1q_u8:将uint8x16_t存储在给定地址的函数

矩阵乘法 

矩阵乘法是在许多数据密集型应用程序中执行的操作。矩阵乘法过程如下:

  • A- 在第一个矩阵中取一行

  • B- 使用第二个矩阵中的列执行此行的点积

  • C- 将结果存储在新矩阵的相应行和列中

大前端CPU优化技术--NEON intrinsics进阶_第1张图片

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:由4个 32 位浮点数组成的数组。
  • vld1q_f32:将4个 32 位浮点数加载到float32x4_t中的函数。
  • vfmaq_lane_f32:使用融合乘法累加指令的函数。将float32x4_t值乘以另一个float32x4_t元素,然后将结果加第三个float32x4_t,然后再返回结果。
  • vst1q_f32:将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性能调优进阶,敬请期待。

你可能感兴趣的:(cpu,大前端,性能优化,cpu,simd)