[TOC]

概述

NEON加速指令介绍

汇编指令格式

AArch64 与AArch32 / Armv7-A 的 NEON 汇编指令除了种类上存在差异,格式上也存在很大差异。

其中指令中有一些通用的书写格式, 含义如下:

  • {}, 表示可选项
  • <>, 表示必选项

AArch64汇编指令格式

1
{<prefix>}<op>{<suffix>} Vd.<T>, Vn.<T>, Vm.<T>

<prefix> 表示前缀名字,包括以下几类:

  • S/U/F/P:表示数据类型,分别为 有符号整型/无符号整型/浮点型/布尔型
  • Q:表示饱和(Saturating)计算。
  • R:表示舍入(Rounding)计算, Rounding 操作等价于加上 0.5 之后再截断。
  • H:表示折半(Halving)计算。
  • D:表示翻倍(Doubling)算。

<op> 表示具体的操作,例如 ADDSUB 等等

<suffix> 表示后缀名字,包括以下几类:

  • V:表示 Reduction 计算。

  • P:表示 Pairwise 计算。

  • H:表示结果只取每个通道的高半部分(High)。

  • L/N/W/L2/N2/W2:表示数据长度的变化

    • L / L2 :表示输出向量是输入向量长度的 2 倍,其中 L 表示输入寄存器的低 64bit 数据有效,L2 表示输入寄存器的高 64bit 数据有效。

img

​ L/L2

    • N/N2:表示输出向量是输入向量的 1/2 倍,N 表示输出向量只有低 64bit 有效,N2 则表示输出向量只有高 64bit 有效。

img

​ N/N2

    • W/W2:表示输出向量和第一个输入向量长度相等,且这两个向量是第二个向量长度的 2 倍,其中 W 表示第二个输入向量的低 64bit 有效,W2 表示第二输入向量的高 64bit 有效。

img

​ W/W2

4)<T> 表示单个通道的数据类型,8B/16B/4H/8H/2S/4S/2D,B 表示 8bit,H 表示 16bit,S 表示 32bit,D 表示 64bit。

汇编指令示例

SQRSHRN2 表示对向量进行 Rouding 类型的右移操作,并对结果做饱和计算,最后将结果赋给目的向量的高半部分,并保持低半部分不改变,具体示例如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 指令语句作用:
// 将 V2 向量中每个元素按照 Rounding 方式右移动 2 位,然后对结果做饱和操作,
// 并将结果保存到V0上半部分,而且保证V0的下半部分保持不变
// 指令格式说明:
// S -- 表示有符号操作
// Q -- 表示饱和操作
// R -- 表示舍入操作
// SHR -- 表示向右位移
// N2 -- 表示将结果保存到输出向量的高 64bit
// V2.2D -- 表示输入向量寄存器,长度为 128bit,一共两个通道,每个通道 64bit
// V0.4S -- 表示输出向量寄存器,长度为 128bit,一共四个通道,每个通道 32bit

SQRSHRN2 V0.4S,V2.2D,2

// 伪代码如下:
int shift = 2;
int round_const = (1 << (shift - 1));
V0[2] = SAT((V2[0] + round_const) >> shift)
V0[3] = SAT((V2[1] + round_const) >> shift)

img

AArch32 / Armv7汇编指令格式

1
V{<mod>}<op>{<shape>}{<cond>}{.<dt>}<dest1>{,<dest2>},<src1>{,<src2>}

1)V AArch32 / Armv7 的汇编指令以”V”开头

2)<mod> 该修饰字可以表示为以下类型:

  • Q, 表示饱和(Saturating)计算。
  • R, 表示舍入(Rounding)计算,Rounding 操作等价于加上 0.5 之后再截断。
  • H, 表示折半(Halving)计算。
  • D, 表示翻倍(Doubling)计算。

3)<op> 表示具体的操作,例如 ADDSUB等等

4)<shape> 表示数据长度的变化,L/N/W。

5)<cond> 表示指令执行的条件

6).<dt> 表示数据类型,默认为第二个操作数的数据类型。如果第二个操作数不存在,为第一个操作数类型,仍不存在为结果操作数类型。

7)<dest> 表示输出操作数

**8)<src1> <src2>**表示两个输入操作数

汇编指令示例

VQDMULL 表示两向量相乘,结果乘以 2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 指令语句作用:
// 64bit 向量 D1 和 D3 中每个元素对应相乘,并将结果乘以 2
// 最后的结果做饱和之后赋值给 128bit 向量 Q0
//
// 指令格式说明:
// Q -- 表示饱和操作
// D -- 表示 doubling 操作,即乘以 2
// MUL -- 表示乘法操作
// L -- 输出向量是输入向量长度的 2 倍
// .S16 -- 表示操作元素的数据类型为有符号 16bit
// Q0 -- 表示输出向量寄存器,长度为 128bit
// D1 -- 表示输入向量寄存器,长度为 64bit
// D3 -- 表示输入向量寄存器,长度为 64bit

VQDMULL.S16 Q0, D1, D3

// 伪代码
for (int i = 0; i < 4; i++)
{
q0[i] = SAT(d1[i] * d3[i] * 2)
}

img

intrinsics指令格式

相比于汇编指令,NEON Intrinsics 是一种更简单的编写 NEON 代码的方法,NEON Intrinsics 类似于 C 函数调用,在编译时由编译器替换为相应的汇编指令,使用时需要包含头文件arm_neon.h

向量类型格式

1
2
3
4
5
6
7
// 非数组向量格式
<type><size>x<number_of_lanes>_t
// float32x4_t

// 数组向量格式
<type><size>x<number_of_lanes>x<length_of_array>_t
// float32x4x2t

1)<type> 数据类型,如 int/uint/float/poly

2)<size> 元素数据大小,如8位、16位、32位、64位。

3) 通道数。由于NEON提供了64位和128位寄存器,所以size * lanes 应为64或128。

4) 数组中元素个数。

img

NEON向量数据类型

以128位寄存器为例,数据类型如:int8x16_t,int16x8_t,int32x4_t,int64x2_t,uint8x16_t,uint168_t,uint32x4_t,uint64x2_t,float16x8_t,float32x4_t

数据类型 64-bit type(D-register) 128-bit type(Q-register) 类型说明 备注
int8x8_t int8x16_t 8位int类型数据,8个通道/8位int类型数据,16个通道
int16x4_t int16x8_t 16位int类型数据,4个通道/16位int类型数据,8个通道
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

从上表可以看成,NEON 指令集的向量是由基本上就是常见的数据类型组成,根据 D 寄存器和 Q 寄存器以及数据类型形成多种向量类型。这些向量类型将作为 NEON 指令集的输入输出参数参与计算。

NEON 函数格式

v<mod><opname><shape><flags>_<type>

1
v<mod><opname><shape><flags>_<type>

1)<mod>

q:表示饱和计算,即当计算结果溢出时,结果取类型范围内的最大值或最小值,例如

1
2
// a加b的结果做饱和计算
int8x8_t vqadd_s8(int8x8_t a, int8x8_t b);

h:表示折半计算,例如

1
2
// a减b的结果右移一位
int8x8_t vhsub_s8(int8x8_t a, int8x8_t b);

d:表示加倍计算,例如

1
2
// a乘b的结果扩大一倍, 最后做饱和操作
int32x4_t vqdmull_s16(int16x4_t a, int16x4_t b);

r:表示舍入计算,例如

1
2
// 将a与b的和减半,同时做rounding 操作, 每个通道可以表达为: (ai + bi + 1) >> 1
int8x8_t vrhadd_s8(int8x8_t a, int8x8_t b);

p:表示pairwise计算。例如

1
2
// 将a, b向量的相邻数据进行两两和操作
int8x8_t vpadd_s8(int8x8_t a, int8x8_t b);

2) <opname>

表示具体操作,如加法:add;减法:sub;乘法:mul;加载数据:ld;读取数据:st

3) <shape>

计算形式

l:表示long,输出向量的元素长度是输入长度的2倍,例如

1
uint16x8_t vaddl_u8(uint8x8_t a, uint8x8_t b);

n:表示 narrow,输出向量的元素长度是输入长度的1/2倍,例如

1
uint32x2_t vmovn_u64(uint64x2_t a);

w:表示 wide,第一个输入向量和输出向量类型一样,且是第二个输入向量元素长度的2倍,例如

1
uint16x8_t vsubw_u8(uint16x8_t a, uint8x8_t b);

_high:AArch64专用,而且和 l/n 配合使用。

  • 当使用 l(Long) 时,表示输入向量只有高 64bit 有效;
  • 当使用 n(Narrow) 时,表示输出只有高 64bit 有效。
1
2
// a 和 b 只有高 64bit 参与运算
int16x8_t vsubl_high_s8(int8x16_t a, int8x16_t b);

_n:表示有标量参与向量计算,例如

1
2
// 向量 a 中的每个元素右移 n 位
int8x8_t vshr_n_s8(int8x8_t a, const int n);

_lane: 指定向量中某个通道参与向量计算,例如

1
2
// 取向量 v 中下标为 lane 的元素与向量 a 做乘法计算
int16x4_t vmul_lane_s16(int16x4_t a, int16x4_t v, const int lane);

4) <flags>

q:寄存器长度,若存在q时,表示使用128位的寄存器,否则使用64位寄存器。

5) <type>

表示单个通道的数据类型

u8s8u16s16u32s32f32f64。(s表示int)

img

解释说明

1
2
3
4
5
vadd_u16:// 两个uint16x4相加为一个uint16x4

vaddq_u16:// 两个uint16x8相加为一个uint16x8

vaddl_u16: // 两个uint8x8相加为一个uint16x8