原文链接:[Coding for Neon - Part 2: Dealing With Leftovers]
https://community.arm.com/developer/ip-products/processors/b/processors-ip-blog/posts/coding-for-neon—part-2-dealing-with-leftovers
目录:
Coding Neon - Part1:Load and Stores
Coding Neon - Part2:Dealing With Leftovers
Coding Neon - Part3:Matrix Multiplication
Coding Neon - Part4:Shifting Left and Right
Coding Neon - Part5:Rearranging Vectors
第1部分中,探讨了如何在Neon处理单元和内存之间传输数据。
在本文中,我们处理一个经常遇到的问题:输入数据不是要处理的向量长度的倍数。你需要在数组的开头或结尾处,处理剩余的元素——在Neon上处理这个问题的最佳方法是什么?😁
Leftovers(剩菜剩饭👺)
使用Neon通常涉及这样的操作:处理长度为4~16个元素(位)的向量数据。通常你会发现你的数组不是该长度的倍数,然后不得不另外处理那些剩余的元素。
例如,你想要用Neon在每次迭代中加载、处理或存储8个元素,但是你的数组长度是21个元素。前2次迭代进行得很好,但是到了第3次,仅剩下5个元素要处理,你要怎么做?
Fixing Up(解决办法)
有3个办法来处理这些剩下来的东西,这些方法在需求、性能、代码大小上各不相同。下面依次列出这几种方法,最快的方法排在前面。
1. Larger Arrays(增大数组)
如果你可以改变你正在处理的数组的长度,可通过增加元素(padding elements)将数组的长度增加到向量大小的倍数。这可以让你在数据的末尾读取和写入数据,而不会破坏相邻的存储。
在上面的例子中,把数组的长度增加到24个元素,让第3次迭代得以完成而不会造成潜在的数据破坏。
Notes(注意)
- 分配更大的数组将会消耗更多内存。在短数组很多的时候,这种开销会变得非常显著。
- 为了不影响计算结果,在数组末尾新增元素后,这些元素应该被初始化为指定的值。例如 (1) 如果你正在对数组求和,新增元素必须被初始化为0,从而不影响计算结果。 (2) 如果正在寻找数组最小值,新增元素必须设到Max这么大。
- 在一些情况下,想要不影响计算结果,但是没办法找到合适的初始值,例如求数组均值的时候。
Code Fragment(代码片段)
以上图为例
@ r0 = 输入数组指针
@ r1 = 输出数组指针
@ r2 = 数组的数据长度
@ 我们可以假设数组长度比0要大,是个整形向量,比数组中数据的长度大或者相等
add r2, r2, #7 @ r2 = r2 + 7, 7的由来:向量长度-1
lsr r2, r2, #3 @ r2 = r2 / 8, 总长除以每一段的长度,算出有几段向量要处理(右移3就是除以8)
loop:
subs r2, r2, #1 @ r2 = r2 - 1, 剩余要循环的次数, 每次减一
vld1.8 {d0}, [r0]! @ 从r0指向的地址加载8个元素(位)到d0寄存器,!表示马上刷新r0指向下一块地址
...
... @ 处理加载到d0的数据
...
vst1.8 {d0}, [r1]! @ 写8个元素到输出数组,更新r1的值,指向下一块地址
bne loop @ 如果r2不为0,继续循环
2. Overlapping(重叠)
如果操作合适,可以用重叠(overlapping)来处理剩余元素。这涉及到两次处理数组中的一些元素。
在下图的例子中,第一次迭代处理[0-7]的元素,第二次处理[5-12]的元素,第三次处理[13-20]的元素。注意[5-7]的元素在第1和第2个向量中重复了,被处理了2次。
Notes(注意)
- 当对输入数据的操作,不随操作次数的变化而变化时,才能使用重叠(overlapping),操作必须是幂等的。例如, (1) 当你正在找数组的最大元素时,就可以使用。 (2) 对数组求和就不能使用,重叠部分将被计数2次。
- 数组中元素的数量必须能够填满至少1个完整的向量。
Code Fragment(代码片段)
@ r0 = 输入数组指针
@ r1 = 输出数组指针
@ r2 = 数组的数据长度
@ 假设操作是幂等的,数组的长度至少为1个向量的长度
ands r3, r2, #7 @ 求处理完所有整长的向量后,数组还剩多少剩余没法整除的。即求r2与8的余数
@ r3 = r2 & 7, 7的由来:向量长度-1.
@ and:逻辑与. ands:逻辑与之后立即更新cpsr中的标志位.
beq loopsetup @ 如果上面`ands`操作的结果是0, 数组的长度就是向量长度的整数倍
@ 所以没有重叠, 可以跳到下面`loopsetup`循环哪里开始处理
@ 否则单独分开处理第一个向量
vld1.8 {d0}, [r0], r3 @ 从数组加载第一组8个元素, 根据元素重叠数量数量更新指针, r3就是重叠的数量
...
... @ 处理加载到d0的数据
...
vst1.8 {d0}, [r1], r3 @ 写8个元素到输出数组, 更新输出数组指针的位置
loopsetup: @ 循环前的处理
lsr r2, r2, #3 @ r2 = r2 / 8, 算算有多少个向量需要处理(即循环多少次)
loop: @ 像前一个example一样正常循环了
subs r2, r2, #1 @ r2 = r2 - 1, 剩余要循环的次数, 每次减一
vld1.8 {d0}, [r0]! @ 从r0指向的地址加载8个元素(位)到d0寄存器,!表示马上刷新r0指向下一块地址
...
... @ 处理加载到d0的数据
...
vst1.8 {d0}, [r1]! @ 写8个元素到输出数组,更新r1的值,指向下一块地址
bne loop @ 如果r2不为0,继续循环
解读:也就是首先对重叠部分进行处理、指针偏移,然后就跟上一个例子一样开始正常循环。
3. Single Elements(单个元素)
Neon提供加载和存储的操作,可以处理向量里的单个元素。通过这个方法,可以加载包含1个元素的部分向量,对其进行操作,然后把该元素写回到内存中。
对于下图的问题,前两次迭代正常执行,处理[0-7]和[8-15]的元素。第三次迭代只需要处理5个元素。他们在单独的循环中处理:加载、处理和存储单个元素。
Notes(注意)
- 这个方法比之前的两种方法慢,因为每个元素都要被单独地加载、处理和存储。
- 这种处理剩余元素的方式要两次循环:1次是处理向量,第2次是处理单个元素。这导致代码的数量会翻倍。
- Neon加载单数据仅改变目标元素的值,向量的其余部分保持不变。如果某些向量化的计算指令如
VPADD
,需要跨向量运作,那在载入第一个元素之前,该寄存器必须先被初始化。
Code Fragment(代码片段)
@ r0 = 输入数组指针
@ r1 = 输出数组指针
@ r2 = 数组的数据长度
lsrs r3, r2, #3 @ 算出可以处理多少个完整的向量(lsrs是新的语法, 与lsr是一样的)
@ 这行代码看是把r2逻辑右移3位,即r3=r2/3
beq singlesetup @ 如果上面`lsrs`的结果为0, 则代表没有一个完整的向量
@ (向量长度为8个element或以上),则跳到下面`singlesetup`开始运行
vectors: @ 循环处理向量
subs r3, r3, #1 @ 更新剩余循环次数(r3=r3-1)
vld1.8 {d0}, [r0]! @ 从r0指向的内存加载8个element到d0寄存器, 并且更新指针
...
... @ 处理d0中的数据
...
vst1.8 {d0}, [r1]! @ 把d0中的8个元素写到r1指向的内存, 并且更新指针
bne vectors @ 如果r3不为0, 则跳到`vectors`处继续循环
singlesetup:
ands r3, r2, #7 @ 求r2和8的余数, 得到r3表示剩余几个单独的element要处理
beq exit @ 若r3的结果为0, 则跳到下面`exit`处
singles: @ 循环, 处理单个element
subs r3, r3, #1 @ 更新循环计数器, r3=r3-1
vld1.8 {d0[0]}, [r0]! @ 加载单个element到d0, 然后更新指针
...
... @ 处理输入到d0[0]的数据
...
vst1.8 {d0[0]}, [r1]! @ 写单个element到r1, 然后更新指针
bne singles @ 若r3的结果不为0, 则跳到`singles`处继续循环
exit:
Further Considerations(进一步思考)
Beginning or End(开始或结束)
重叠(overlapping)和单元素(single element)技术可以在处理数组的开始或者结束时使用。上面的代码很容易修改,在数组的开始或结束处理元素,以适配你的应用。
Alignment(对齐)
加载和存储地址应与高速缓存行对齐,以实现更有效的内存访问。
这要求在Cortex-A8上至少对齐16个字节。如果你不能在输入或输出数组的开头对齐,那你必须在数组的开头或结尾处理元素。在开头处理是为了对齐,在结尾处理是为了处理那些剩余的元素。
如果想要地址对齐以加快内存访问,记住要在加载和存储指令中使用:64
或:128
或:256
地址限定符,以实现最佳性能。你可以参考对应内核的技术手册,来看看它们加载或存储时所需周期数。
Cortex-A8技术参考手册相关页面
Using Arm to Fix Up(用Arm的时候)
对于单元素的情况,可以用Arm指令集单独操作每个元素。然而,同时用Arm和Neon指令集来保存同样一块内存,会降低性能。因为Neon的流水线完成后才到轮到Arm。(as the writes from the Arm pipeline are delayed until writes from the Neon pipeline have been completed)
总的来说,应该避免同时用Arm和Neon的代码来写同一块内存,特别是同一行缓存、
================================================