ARM64体系结构编程与实践--NEON指令集

22.3 NEON指令集

在 NEON 指令集中,指令通常可以分成两大类:一类是矢量(vector)运算指令;另一类是标量(scalar)运算指令。矢量运算指的是对矢量寄存器中所有通道的数据同时进行运算,而标量运算指的是只对矢量寄存器中某个通道的数据进行运算。

22.3.1 SISD与SIMD

SISD指的是单指令但数据,SIMD指的是单指令多数据,他能同时对多个数据元素执行相同的操作。SIMD非常适合做图像处理

22.3.2 矢量运算与标量运算

NEON指令集可以分为矢量指令集和标量指令集。

矢量运算指的是对矢量寄存器中所有通道的数据同时进行运算,而标量运算指的是只对矢量寄存器中某个通道的数据进行运算。

22.3.3 加载与存储指令
22.3.3.1 LD1与ST1

LD1 指令用来把多个数据元素加载到 1 个、2 个、3 个或 4 个矢量寄存器中。

LD1 指令最多可以使用 4 个矢量寄存器。

LD1 指令支持没有偏移量和后变基两种模式。

没有偏移量模式的指令格式如下。

LD1 { <Vt>.<T> }, [<Xn|SP>] 
LD1 { <Vt>.<T>, <Vt2>.<T> }, [<Xn|SP>] 
LD1 { <Vt>.<T>, <Vt2>.<T>, <Vt3>.<T> }, [<Xn|SP>] 
LD1 { <Vt>.<T>, <Vt2>.<T>, <Vt3>.<T>, <Vt4>.<T> }, [<Xn|SP>]

后变基模式的 ST1 指令格式如下。

LD1 { <Vt>.<T> }, [<Xn|SP>], <imm> 
LD1 { <Vt>.<T>, <Vt2>.<T> }, [<Xn|SP>], <imm> 
LD1 { <Vt>.<T>, <Vt2>.<T>, <Vt3>.<T> }, [<Xn|SP>], <imm> 
LD1 { <Vt>.<T>, <Vt2>.<T>, <Vt3>.<T>, <Vt4>.<T> }, [<Xn|SP>], <imm>

上述指令表示从 Xn/SP 指向的源地址中加载多个数据元素到 Vt、Vt2、Vt3 以及 Vt4 矢量寄存器中,加载的数据类型由矢量寄存器的 T 来确定,加载完成之后,更新 Xn/SP 寄存器的值为 Xn/SP 寄存器的值加 imm。
请添加图片描述
与 LD1 对应的存储指令为 ST1。

ST1 指令把 1 个、2 个、3 个或 4 个矢量寄存器的多个数据元素的内容存储到内存中。

ST1 指令最多可以使用 4 个矢量寄存器。

ST1 指令支持没有偏移量和后变基两种模式。

没有偏移量模式的指令格式如下。

ST1 { <Vt>.<T> }, [<Xn|SP>] 
ST1 { <Vt>.<T>, <Vt2>.<T> }, [<Xn|SP>] 
ST1 { <Vt>.<T>, <Vt2>.<T>, <Vt3>.<T> }, [<Xn|SP>] 
ST1 { <Vt>.<T>, <Vt2>.<T>, <Vt3>.<T>, <Vt4>.<T> }, [<Xn|SP>]

后变基模式的 ST1 指令格式如下。

ST1 { <Vt>.<T> }, [<Xn|SP>], <imm> 
ST1 { <Vt>.<T>, <Vt2>.<T> }, [<Xn|SP>], <imm> 
ST1 { <Vt>.<T>, <Vt2>.<T>, <Vt3>.<T> }, [<Xn|SP>], <imm> 
ST1 { <Vt>.<T>, <Vt2>.<T>, <Vt3>.<T>, <Vt4>.<T> }, [<Xn|SP>], <imm> 

上述指令表示把 Vt、Vt2、Vt3 以及 Vt4 矢量寄存器的数据元素存储到 Xn/SP 指向的内存地址中,数据类型由矢量寄存器的 T 来确定,存储完成之后,更新 Xn/SP 寄存器的值为 Xn/SP寄存器的值加 imm。

22.3.3.2 LD2与ST2

LD1 和 ST1 指令按照内存顺序来加载和存储数据,而有些场景下希望能按照**交替(interleave)**的方式来加载和存储数据。LD2 和 ST2 指令就支持以交替方式加载和存储数据,它们包含没有偏移量和后变基两种模式。

没有偏移量模式的 LD2 和 ST2 指令格式如下。

LD2 { <Vt>.<T>, <Vt2>.<T> }, [<Xn|SP>] 
ST2 { <Vt>.<T>, <Vt2>.<T> }, [<Xn|SP>] 

后变基模式的 LD2 和 ST2 指令格式如下。

LD2 { <Vt>.<T>, <Vt2>.<T> }, [<Xn|SP>], <imm> 
ST2 { <Vt>.<T>, <Vt2>.<T> }, [<Xn|SP>], <imm> 

下面这条指令把 X0 寄存器指向的内存数据加载到 V0 和 V1 矢量寄存器中。

LD2 {V0.8H, V1.8H}, [X0] 

这些数据会通过交错的方式加载到 V0 和 V1 矢量寄存器中。从“8H”可知,每个矢量寄存器包括 8 个 16 位的数据元素。如图 22.15 所示,地址 0x0 中的数据元素 E0(偶数)会加载到 V0 寄存器的 8H[0]里,地址 0x2 中的数据元素 O0(奇数)会加载到 V1 寄存器的 8H[0]里,地址中 0x4 的数据元素 E1(偶数)会加载到 V0 寄存器的 8H[1]里,以此类推。
在这里插入图片描述

22.3.3.3 LD3与ST3

LD3 指令从内存中获取数据,同时将值分割并加载到不同的矢量寄存器中,这叫作解交错(de-interleaving)。LD3 与 ST3 指令包含没有偏移量模式和后变基模式。

没有偏移量模式的 LD3 和 ST3 指令格式如下。

LD3 { <Vt>.<T>, <Vt2>.<T>, <Vt3>.<T> }, [<Xn|SP>] 
ST3 { <Vt>.<T>, <Vt2>.<T>, <Vt3>.<T> }, [<Xn|SP>] 

后变基模式的 LD3 和 ST3 指令格式如下。

LD3 { <Vt>.<T>, <Vt2>.<T>, <Vt3>.<T> }, [<Xn|SP>], <imm> 
ST3 { <Vt>.<T>, <Vt2>.<T>, <Vt3>.<T> }, [<Xn|SP>], <imm> 

注意,矢量寄存器列表中的矢量寄存器必须是 3 个编号连续递增的矢量寄存器,否则编译会出错。

原因是LD3 和 ST3 指令的编码中,Vtt 表示0~31 的整数)矢量寄存器通过指令编码中的 Rt字段获取矢量寄存器的索引值,而 Vt2 则通过使 Rt 字段加 1 获取矢量寄存器的索引值,Vt3 通过使Rt 字段加 2 获取矢量寄存器的索引值。

以下代码使用 LD3 指令把 RGB24 格式的数据加载到矢量寄存器中。

LD3 { V0.16B, V1.16B, V2.16B }, [x0],#48

其中,X0 表示 RGB24 格式数据的源地址,这条指令会把 RGB24 的数据加载到 V0、V1以及 V2 矢量寄存器。如图 22.17 所示,LD3 指令将 16 个红色(R)像素加载到 V0 矢量寄存器中,把 16 个绿色(G)像素分别 V1 矢量寄存器中,把 16 个蓝色(B)像素加载到 V2 矢量寄存器中。这条 LD3 指令一次最多可以加载 48 字节的数据。

在这里插入图片描述

22.3.3.4 LD4与ST4

ARGB 格式在 RGB 的基础上加了 Alpha(透明度)通道。为了加快 ARGB 格式数据的加载和存储操作,NEON 指令提供了 LD4 和 ST4 指令。与 LD3 类似,不过 LD4 可以把数据解交叉地加载到 4 个矢量寄存器中。

LD4 和 ST4 指令包含没有偏移模式与后变基模式。

没有偏移模式的 LD4 和 ST4 指令格式如下。

LD4 { <Vt>.<T>, <Vt2>.<T>, <Vt3>.<T>, <Vt4>.<T> }, [<Xn|SP>] 
ST4 { <Vt>.<T>, <Vt2>.<T>, <Vt3>.<T>, <Vt4>.<T> }, [<Xn|SP>] 

后变基模式的 LD4 和 ST4 指令格式如下。

LD4 { <Vt>.<T>, <Vt2>.<T>, <Vt3>.<T>, <Vt4>.<T> }, [<Xn|SP>], <imm> 
ST4 { <Vt>.<T>, <Vt2>.<T>, <Vt3>.<T>, <Vt4>.<T> }, [<Xn|SP>], <imm> 
22.3.3.5 LDnR

LDn 指令还有一个变种——LDnR 指令,R 表示重复的意思。它会从内存中加载一组数据元素,然后把数据复制到矢量寄存器的所有通道中。

下面的 LD3R 指令从内存中加载单一的三元素数据,然后将该数据复制到 3 个矢量寄存器的所有通道中。

LD3R { V0.16B, V1.16B, V2.16B } , [x0] 

V0 寄存器中 16 个通道的值全为 R0,V1 寄存器中 16 个通道的值全为G0,V2 寄存器中 16 个通道的值全为 B0。

在这里插入图片描述

22.3.3.5 LDn特殊用法

LDn 指令可以加载数据到矢量寄存器的某个通道中,而其他通道的值不变。

下面的代码使用了 LD3 指令。

LD3 { V0.B, V1.B, V2.B }[4] , [x0]

这里指令只从 X0 地址处加载 3 个数据元素,把它们分别存储在 V0.16B[4]、V1.16B[4]以及 V2.16B[4],这 3 个矢量寄存器中其他通道的值不变。

在这里插入图片描述

22.3.4 搬移指令MOV

NEON 指令集也提供了 MOV 指令,用于矢量寄存器中数据元素之间的搬移。

22.3.4.1 从通用寄存器中搬移数据

从通用寄存器中搬移数据的 MOV 指令格式如下。

MOV <Vd>.<Ts>[<index>], <R><n> 

其中,每个部分的含义如下。

  • Vd:表示目标矢量寄存器。
  • Ts:表示数据元素大小,例如,B 表示 8 位,H 表示 16 位,S 表示 32 位,D 表示
  • 64 位。
  • index:数据元素的索引值。
  • R:表示通用寄存器,X 表示 64 位通用寄存器,W 表示 32 位通用寄存器。

下面的代码使用了 MOV 指令。

mov w1, #0xa 
mov v1.h[2], w1 

第一条指令把常量 0xa 搬移到通用寄存器 W1 中,第二条指令把 W1 寄存器的内容搬移到V1 矢量寄存器的第三个 16 位数据元素中(h[2]),V1 矢量寄存器中其他通道的数据元素保存不变。注意,W1 和 V1 是两个不同的寄存器,一个是整数运算的通用寄存器,另一个是 FP/NEON 运算的矢量寄存器。

22.3.4.2 矢量寄存器搬移

矢量寄存器搬移指令 MOV 的格式如下。

MOV <Vd>.<T>, <Vn>.<T> 

其中,每个部分的含义如下。

  • Vd:表示目标矢量寄存器。
  • Vn:表示源矢量寄存器。
  • T:表示要搬移数据元素的数量,例如,8B 表示搬移 8 字节数据,16B 表示搬移 16
  • 字节数据。

下面的代码也使用了 MOV 指令。

MOV V3.16B, V0.16B 
MOV V3.8B, V0.8B 

第一条 MOV 指令把 V0 矢量寄存器中所有的内容搬移到 V3 矢量寄存器中,第二条 MOV指令把 V0 矢量寄存器中低 8 字节数据搬移到 V3 矢量寄存器中。

22.3.4.3 搬移数据元素到矢量寄存器

搬移某个数据元素到矢量寄存器的 MOV 指令格式如下。

MOV <V><d>, <Vn>.<T>[<index>] 

其中,每个部分的含义如下。

  • Vd:表示目标矢量寄存器。这里矢量寄存器必须和 T 保存一致。
  • Vn:表示源矢量寄存器。
  • T:表示数据元素大小,例如,B 表示 8 位,H 表示 16 位,S 表示 32 位,D 表示 64 位。
  • index:数据元素的索引。

下面的两条 MOV 指令中,哪一条的写法正确?

mov h2, v1.8h[2] 
mov v2, v1.8h[2] 

第一条指令把 V1 矢量寄存器中的第三个数据元素搬移到 H2 矢量寄存器中。而第二条指令是错误的指令,第一个操作数的大小必须和第二个操作数的大小保持一致。

22.3.4.4 搬移数据元素

矢量寄存器之间搬移数据元素的 MOV 指令格式如下。

MOV <Vd>.<Ts>[<index1>], <Vn>.<Ts>[<index2>] 

其中,每个部分的含义如下。

  • Vd:表示目标矢量寄存器。
  • Vn:表示源矢量寄存器。
  • Ts:表示数据元素大小,例如,B 表示 8 位,H 表示 16 位,S 表示 32 位,D 表示 64 位。
  • index:数据元素的索引。

如下指令的作用是什么?

mov v1.8h[2], v0.8h[2] 

这条 MOV 指令把 V0 矢量寄存器中第三个数据元素(H[2])搬移到 V1 矢量寄存器的第三个数据元素(H[2])中。

22.3.5 反转指令REV

REV 指令用于反转数据元素(reverse element),在很多场景下非常有用。REV 指令一共有3 条变种指令。

  • REV16 指令:表示矢量寄存器中的 16 位数据元素组成一个容器。在这个容器里,反转 8 位数据元素的顺序,即颠倒 B[0]和 B[1]的顺序。
  • REV32 指令:表示矢量寄存器中的 32 位数据元素组成一个容器。在这个容器里,反转 8 位数据元素或者 16 位数据元素的顺序。
  • REV64 指令:表示矢量寄存器中的 64 位数据元素组成一个容器。在这个容器里,反转 8 位、16 位或者 32 位数据元素的顺序。

下面的代码使用了 REV16 指令。

REV16 v0.16B, v1.16B 

在这条指令中,16 位数据组成一个容器,一个矢量寄存器一共有 8 个容器。在容器里有两个 8 位的数据。REV16 指令会分别对这 8 个容器里的 8 位数据进行反转。

在这里插入图片描述

22.3.6 提取指令EXT

EXT 指令从两个矢量寄存器中分别提取部分数据元素,组成一个新的矢量,并写入新的矢量寄存器中。EXT 是 Extraction 的缩写。EXT 指令的格式如下。

EXT <Vd>.<T>, <Vn>.<T>, <Vm>.<T>, #<index> 

其中,每个部分的含义如下。

  • Vd:表示目标矢量寄存器。
  • Vn:表示第一个源矢量寄存器。
  • Vm:表示第二个源矢量寄存器。
  • T:表示数据元素的大小和数量,例如,8B 表示有 8 个 8 位的数据元素,16B 表示有16 个 8 位的数据元素。
  • index:表示从第二个源矢量寄存器中提取多少个数据元素。

下面是一条 EXT 指令。

EXT v0.16B, v2.16B, v1.16B, #3 

16B 表示矢量寄存器一共有 16 个 8 位的数据元素。3 表示从第一个源矢量寄存器中提取 3 个

数据元素,剩下的 13 个数据元素需要从第二个源矢量寄存器的高 13 位中提取。

在这里插入图片描述

22.3.7 交错变换指令TRN

在数学矩阵计算中常常需要变换行和列的数据,例如,转置矩阵(transpose of a matrix)。

NEON 指令集为加速转置矩阵运算提供了两条指令——TRN1 和 TRN2 指令。

TRN1 指令从两个源矢量寄存器中交织地提取奇数编号的数据元素来组成一个新的矢量,写入目标矢量寄存器中。

TRN2 指令从两个源矢量寄存器中交织地提取偶数编号的数据元素来组成一个新的矢量,写入目标矢量寄存器中。

【例 22-18】下面是 TRN1 指令。

TRN1 V1.4S, V0.4S, V3.4S 

这条指令分别从 V0 和 V3 矢量寄存器中交织地去取奇数编号的数据元素,例如,从 V0 矢量寄存器中取第一个奇数编号的数据元素 D,存储到目标矢量寄存器 V1 的 S[0]上,从 V3 矢量寄存器中取第一个奇数编号的数据元素 H,存储到目标矢量寄存器 V1 的 S[1]上,以此类推

【例 22-19】下面是 TRN2 指令。

TRN2 V2.4S, V0.4S, V3.4S 

这条指令分别从 V0 和 V3 矢量寄存器中交织地去取偶数编号的数据元素,例如,从 V0 矢量寄存器中取第一个偶数编号的数据元素 C,存储到目标矢量寄存器 V2 的 S[0]上,从 V3 矢量寄存器中取第一个偶数编号的数据元素 G,存储到目标矢量寄存器 V2 的 S[1]上,以此类推。

在这里插入图片描述

22.3.8 查表指令TBL

TBL(查表)指令的格式如下。

TBL <Vd>.<Ta>, { <Vn>.16B }, <Vm>.<Ta> 
TBL <Vd>.<Ta>, { <Vn>.16B, <Vn+1>.16B }, <Vm>.<Ta> 
TBL <Vd>.<Ta>, { <Vn>.16B, <Vn+1>.16B, <Vn+2>.16B }, <Vm>.<Ta> 
TBL <Vd>.<Ta>, { <Vn>.16B, <Vn+1>.16B, <Vn+2>.16B, <Vn+3>.16B }, <Vm>.<Ta> 

TBL 指令查询的表最多可以由 4 个矢量寄存器组成,它们分别为 Vn~Vn+3。其中,相关

选项的含义如下。

  • Vd:表示目标源矢量寄存器。
  • Vn~Vn+3:表示表的内容。
  • Vm:存储查表的索引值。
  • Ta:表示矢量寄存器的大小和数量,例如,8B 表示有 8 个 8 位的数据元素,16B 表示有 16 个 8 位的数据元素。

【例 22-20】以下指令使用 V1 和 V2 两个矢量寄存器里的 32 个数据元素组成一张表,这张

表的索引范围为[31:0]。

TBL V4.16B, {V1.16B, V2.16B}, V0.16B 

V0 矢量寄存器中的 16 个数据元素用于查表。例如,如果 V0 矢量寄存器中第一个数据元素的值为 6,那么用索引 6 查找这张表,查询的结果为 g,把这个值写入目标矢量寄存器的 B[0]通道中。注意,表的起始索引为 0。当索引大于表的范围时,TBL 值会写 0 到目标矢量寄存器的对应数据元素中。如图 22.30 所示,索引 40 已经超过这张表的最大范围,所以把 0 写入对应的数据元素中。

在这里插入图片描述

22.3.9 乘加指令MLA

MLA(乘加)指令广泛应用于矩阵运算。另外,MLA 指令还有一个变种——FMLA。它们的区别在于,FMLA 指令操作的数据为浮点数,MLA 指令操作的数据为整数。

MLA 指令不仅可以完成矢量乘加运算,还可以完成矢量与某个通道中数据元素的乘加运算。

矢量乘加运算的格式如下。

MLA <Vd>.<T>, <Vn>.<T>, <Vm>.<T> 

上述指令对 Vn 和 Vm 矢量寄存器中各自通道的数据进行相乘,然后与 Vd 矢量寄存器中各通道的原有数据进行相加。其中,各个选项的含义如下。

Vd:表示目标矢量寄存器。

Vn:表示第一源操作数的矢量寄存器。

Vm:表示第二源操作数的矢量寄存器。

T:表示数据元素的大小和数量,例如,8B 表示 8 个 8 位的数据元素,16B 表示 16个 8 位的数据元素,4H 表示 4 个 16 位的数据元素,8H 表示 8 个 16 位的数据元素,2S 表示两个 32 位的数据元素,4S 表示 4 个 32 位的数据元素。

矢量与某个数据通道中数据元素乘加运算的格式如下。

MLA <Vd>.<T>, <Vn>.<T>, <Vm>.<Ts>[<index>] 

上述指令把 Vm 矢量寄存器中第 index 个通道的数据分别与 Vn 矢量寄存器中每个通道的数据进行相乘,然后与 Vd 矢量寄存器中各通道的原有数据进行相加。其中,相关选项的含义如下。

  • Ts:表示通道大小。其中,H 表示 16 位数据大小,S 表示 32 位数据大小。
  • index:表示通道的索引。

【例 22-21】下面是一条矢量乘加指令。

mla v2.4s, v0.4s, v1.4s 

从上述指令可知,矢量寄存器一共有 4 个通道,每个通道的大小为 32 位。V0 矢量寄存器中 4 个通道的值分别与 V1 矢量寄存器中 4 个通道的值相乘,再与 V2 矢量寄存器中 4 个通道原有的值进行相加,如图 22.31 所示。

在这里插入图片描述

22.3.10 矢量算术指令

NEON 指令集包含大量的矢量算术指令,例如加减运算、乘除、乘加、比较、逻辑运算、移位操作、绝对值、饱和算术等指令,如表 22.6 所示。

在这里插入图片描述

使用 NEON 指令集优化代码有如下 3 种做法。

  • 手工编写 NEON 汇编代码。
  • 使用编译器提供的 NEON 内建函数。
  • 使用编译器提供的自动矢量优化(auto-vectorization)选项,让编译器自动生成 NEON指令来进行优化。

GCC 编译器内置了自动矢量优化功能。GCC 提供如下几个编译选项。

  • -ftree-vectorize:执行矢量优化。这个选项会默认使能“-ftree-loop-vectorize”与“-ftree-slpvectorize”。
  • -ftree-loop-vectorize:执行循环矢量优化。展开循环以减少迭代次数,同时在每个迭代中执行更多的操作。
  • -ftree-slp-vectorize:将标量操作捆绑在一起,以利用矢量寄存器的带宽。SLP 是Superword-Level Parallelism 的缩写。

另外,GCC 的“O3”优化选项会自动使能“-ftree-vectorize”,即使能自动矢量优化功能。

自动矢量优化的一个必要条件是,在循环开始时必须知道循环次数。因为 break 等中断条件意味着在循环开始时循环的次数可能是未知的,所以 GCC 自动矢量优化功能在有些情况下(例如,在有相互依赖关系的不同循环的迭代中,带有 break 子句的循环中,具有复杂条件的循环中)不能使用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值