NEON是ARM的单指令多数据流(Single Instruction Multiple Data,SIMD)扩展。NEON技术为指令集架构提供了专用扩展,提供了额外的指令,可以在多个数据流上并行执行数学运算。
有了NEON,可以提高处理器在音频/视频处理,语音/面部识别,计算机视觉,深度学习等领域的性能。
ARM的基本指令集是单指令单数据的(Single Instruction Single Data,SISD)。每条指令每次只处理一个数据。例如下面的四条指令:
ADD w0, w0, w5
ADD w1, w1, w6
ADD w2, w2, w7
ADD w3, w3, w8
这四条指令的含义是把W5-W7寄存器中的值分别于W0-W3中的值相加,把结果存在W0-W3。每条ADD指令只能处理一组寄存器,对于一些特定处理来说,其效率较低,有没有什么办法可以在一条指令里就完成全部的动作呢?
以图形处理的RGBA色彩空间为例,除了红/绿/蓝外,还有一个Alpha,每一个可以用8-bit表示。在Armv8-A中,每次需要把8-bit数据存到64-bit的寄存器,再进行运算。可想其资源利用的效率很低。如果把这4个8-bit存在一个寄存器中,在一条指令中分别完成4个数据的运算,其效率将大大提高。
SIMD技术就是为了支持一次操作同时处理多个数据而生。SIMD是业内的通用表达,而NEON是ARM公司的SIMD方案。
NEON体系架构提供了32个128-bit的通用寄存器,用Vn表示,每个寄存器可以用作单一的标量寄存器(浮点或者整型),也可以用作64-bit的向量(vector)寄存器(包含一个或多个元素),或者用作128-bit的向量寄存器(包含两个或者多个元素)。NEON通用寄存器中的数据元素(element)是相同的数据格式,数据元素按照通道(lane)来划分。128-bit的NEON向量可以是:
- 2个64-bit的通道,操作数后缀是.2D,D表示双字(double word);
- 4个32-bit的通道,操作数后缀是.4S,S表示单字(word);
- 8个16-bit的通道,操作数后缀是.8H,H表示半字(halfword);
- 16个8-bit的通道,操作数后缀是.16B,B表示字节(byte);
对于64-bit的向量寄存器,大家可以参考下图类推。
每个数据元素按照低位有效(least significant)排列。
下面的指令表示向量寄存器V0/1/2中有8个通道,每个通道有16-bit数据,将V1中的8个数据同时与V2中的8个数据分别相加,并将运算结果保存在V0中的8个通道。
ADD V0.8H, V1.8H, V2.8H
其运算过程参考下图。
某些NEON指令也可以处理向量寄存器与标量(scalar)的运算,例如下面这条指令,用V2寄存器中的4个32-bit数据分别与V3寄存器中的第2个通道的32-bit相乘,结果保存在V0寄存器中的4个通道中。
MUL V0.4S, V2.4S, V3.S[2]
其运算过程参考下图。
ARM为了SIMD扩展增加了额外的指令集,Armv8-A架构手册中的C7章节就是讲这些指令的。我们来挑一些看看,首先是加载和存储指令,加载指令有LD1/2/3/4,对应的存储指令是ST1/2/3/4。
- LD1是最简单的形式,从内存加载一到四个数据寄存器。LD1指令没有解交织(deinterleaving)功能,可以用LD1处理非交错数据数组
- LD2加载两个或四个数据寄存器,可以将偶数和奇数元素解交织加载到寄存器中,可以用LD2处理分成左/右声道的立体声音频数据
- LD3加载三个寄存器并解交织。可以使用LD3将RGB像素数据拆分为独立的颜色通道
- LD4加载四个寄存器并解交织,可以使用LD4出力ARGB图像数据
所谓的交织(Interleave),就是说数据按照某种固定的分类方式排列在一起。以24-bit的RGB图像数据为例,通常这些数据是按照R,G,B,R,G,B……的排列形式存储在内存中。如果用LD1指令加载,其结果如下图,按照像素把数据加载到V0-V2寄存器中。
如果使用LD3指令,RGB数据按照颜色加载到V0-V2寄存器,如下图。对于RGB格式图像数据而言,这种加载方式可能更利于数据处理。
比如,交换颜色顺序,将RGB转换为BGR,用LD3和ST3指令就很容易完成:先将RGB数据分别加载到V0-V2寄存器,然后交换V0和V2寄存器的数据(V1保持不变),再用ST3指令把V0-V2寄存器中的数据写回到内存。这里解释一下,之所以要用三条MOV指令交换V0和V2的数据,是因为LD3/ST3指令要求三个Vn寄存器的编号必须是连续递增的。
LD3指令也可以加载数据到指定的通道,比如下例,[4]指的是第4个通道。
LD2和LD4指令同LD3类似,区别就是Vn通用寄存器数目不一样。LD1/2/3/4指令寻址模式有两种,基址寄存器模式和后索引寻址模式(Post-index addressing modes),这两种模式在前面的文章中介绍过。后索引寻址模式是先访问内存地址,然后更新地址偏移,适用于加载连续数据。
在实际应用中,数据元素很可能不是通道数的整倍数。比如一个数组有21个16-bit的数据,我们知道,Vn寄存器是128-bit,可以分成8个通道,每个通道16-bit。此时,两个Vn寄存器不足以保存这个数组,三个Vn寄存器又有富余。对于这种数据剩余(leftovers),有三种办法:
- 填充数组至整数倍,如下图中的灰色块表示填充的数据,也就是用三个无效的数据把数组填充为24个数据。用填充数据的方法需要注意的是,填充进的数据不能影响后续处理的结果,比如后续对所有数据求和,那么就要填充0,否则结果就是错的。
- 交叠(overlap)数据元素,如下图,蓝色的5/6/7数据被加载两次。用这种方法也要注意,后面的数据处理不会被数据重复加载而影响,比如数组数据求最大值操作。
- 把剩余元素视为独立的数据来处理,如下图。这种方法无疑是效率最低的
通过前面的例子,我们也能看出,SIMD处理器的性能通常与数据在内存中的排列相关。也就是说,如果内存中的数据排列与要进行的数据处理不匹配时,处理器性能会受到影响。一个解决方案是在数据处理之前重新排列内存中的数据顺序,但这种方案的性能成本比较高,而且某些场景下不适用,比如处理连续的数据流。另一种解决方案是在处理数据值时对其重新排序,也称为置换(permutation)。NEON为此提供了很多指令:
- Move instructions
- Reverse instructions
- Extraction instructions
- Transpose instructions
- Interleave instructions
- Lookup table instructions
交换(reverse)指令将向量分解为有序容器(container),然后将这些容器拆分为有序的子容器,在每个容器中的子容器的顺序颠倒,最后将结果写入目标寄存器。比如下面的REV16指令,把V1寄存器拆分成八个16-bit的半字容器,每个半字容器拆分成两个8-bit的子容器,交换这两个子容器的的数据,并写入V0寄存器。
类似的还有REV32,REV64,例如下图的例子,可以将向量寄存器拆分成两个64-bit的容器,每个容器包含四个16-bit的子容器,交换子容器内的数据。
抽取(extraction)指令,EXT,通过从两个不同的源向量中提取连续通道来创建新向量,索引号指定要包含在目标向量中的第一个源向量的最低通道。下面的例子,#3是索引号,从V1寄存器的第三个通道开始,抽取V1寄存器的最低3个字节和V2寄存器的最高13个字节,并将结果写入V0寄存器。
转置(transpose)指令,可以交织两个源向量的元素。TRN1交织来自两个源向量的奇数通道,而TRN2交织偶数通道。转置指令非常适用于矩阵操作。
交织(interleave)指令,可以将数据重新排列,形成新的向量。ZIP1取两个源向量的下半部,并通过交错这两个下半部中的元素来填充目标向量。ZIP2对源向量的上半部分执行相同的操作。
查找表(lookup table)指令,也是将数据重新排列,但与上面那些指令不同。回想一下上面那些指令,都是按照固定的模式排列数据,不能任意排列。为了执行任意排列,Neon提供了查找表指令TBL和TBX。如下图,V1和V2是源寄存器,拼接成一个数据源,V2在高位,V1在低位。V0中存储的是索引值(index)。用V0中的索引值,去找V1和V2相对应位置的数据,写到目的寄存器V4中。TBL和TBX指令的区别仅在于它们如何处理超出范围的索引。如果索引超出了范围,TBL写入零,而TBX在目标寄存器中保持原始值不变。
NEON提供的扩展指令远不止上面这些,大概有几百条之多。
今天就到这里吧,是不是没用的知识又增加了一些呢?
作者: 老秦谈芯
文章来源:老秦谈芯
推荐阅读
更多IC设计技术干货请关注IC设计技术专栏。
迎添加极术小姐姐微信(id:aijishu20)加入技术交流群,请备注研究方向。