关键字: ARM NEON Cortex-A8 cache 对齐
剩余的元素Leftovers
通常NEON会向量处理从4个到16个元素长度的数据,如果你发现你的数组不是这个这个长度的整数倍,你就需要单独处理那些剩下来的几个元素。如你每次可以使用NEON来加载处理并存储8个元素的数据,但是你的数组有21个元素,你就需要先迭代两次,然后第三次,你只剩下5个元素,此时应该如何处理呢?
Fixing Up修复处理方式
有三种处理方式来处理剩下来的元素,这些方法的需求、性能和代码大小不同,下面顺序介绍,从速度最快的方法开始。
Larger Arrays更大的数组
如果改变你要处理的数组大小,比如增加数组大小到向量大小的整数倍,这样就能在最后一次数据处理时也按照向量大小处理而不会把临近的数据损坏。如上面的例子里,把数组大小增加到24个元素,这样就能用NEON用3次迭代完成所有的数据处理而不会损坏周边数据。
图1. 填补数组到向量的整数个大小
注意事项Notes
- 分配更大的数组需要更多的存储空间,这会增加相当大的空间如果包含非常多的短数组;
- 在数组后面填补的数据元素需要初始化为一个不会影响到结果的值,例如你要做加法,那这个新元素需要初始化为0以影响计算结果。
- 一些情况下,可能没法初始化填充的数据,无论填充什么都会影响计算的结果;
Code Fragment代码片段实例
@ r0 是输入的数组指针;
@ r1 是输出数组指针;
@ r2 是数组数据的长度;
假设数组长度大于0,是向量大小的整数倍,并且大于或者等于数组的长度;
add r2, r2, #7 @ 数据长度加上向量长度-1
lsr r2, r2, #3 @ 把数组长度变成向量个数,即除以向量大小8
loop:
subs r2, r2, #1 @ 减少循环计数器个数
vld1.8 {d0}, [r0]! @ 从数组加载8个元素,从地址r0到寄存器d0,然后更新地址寄存器r0到下一个向量地址;
...
... @ 处理在d0寄存器的数据
...
vst1.8 {d0}, [r1]! @ 把8个结果元素保存到输出数组,更新地址r1到下一个向量
bne loop @ 如果r2不等于0,继续循环
Overlapping重叠计算
如果进行数据处理的操作合适的话,可以考虑把剩余部分的元素通过重叠计算的方式处理,这就会把某些重叠部分的元素计算两次。如下面的例子里,第一次迭代计算元素0到7,第一次计算5到12,第三次计算13到20。从而第一次计算和第二次计算重叠的元素5到7就被计算了两次。
图2. 重叠向量,在橙色区域的数据计算两次
Notes需要事项
- 重叠处理只适用于需要处理的数组长度不会随着每次迭代而改变的情况,但不适用于每次迭代结果改变的情况,如累加计算,这样重叠部分的数据会被计算两次;
- 数组内元素的个数至少大于一次完整迭代的向量大小;
Code Fragment代码片段实例
@ r0 是输入的数组指针;
@ r1 是输出数组指针;
@ r2 是数组数据的长度;
假设数据操作幂等,并且数组长度大于等于一个向量大小长度。
ands r3, r2, #7 @ 计算每次处理完整个向量后剩余元素个数,使用与操作
beq loopsetup @ 如果剩余元素个数为0,则数组长度是整数个向量大小,不用重叠计算,单独处理第一个元素部分
vld1.8 {d0}, [r0], r3 @ 加载数组第一个向量,然后更新数组大小为剩余元素个数r3内保持
...
... @ 处理d0寄存器内的输入数据
...
vst1.8 {d0}, [r1], r3 @ 保持8个元素到输出数组,更新指针,然后开始处理循环
loopsetup:
lsr r2, r2, #3 @ 把数组长度除以8,计算循环迭代次数,若干元素跟第一次迭代的重叠
loop:
subs r2, r2, #1 @ 减少循环计数器个数
vld1.8 {d0}, [r0]! @ 从数组加载8个元素,从地址r0到寄存器d0,然后更新地址寄存器r0到下一个向量地址;
...
... @ 处理在d0寄存器的数据
...
vst1.8 {d0}, [r1]! @ 把8个结果元素保存到输出数组,更新地址r1到下一个向量
bne loop @ 如果r2不等于0,继续循环
单个元素的计算过程Single Elements
NEON提供了能处理向量里的单一元素的加载和存储指令,用这些指令,你能加载包含一个元素的部分向量,处理它然后把结果保存到内存。如下面的例子,前两次的迭代处理跟前面类似,处理元素0到7以及8到15,剩下的5个元素可以在第三次迭代处理,加载处理并存储单一的元素。
图3. 处理单一的元素实例
注意事项
- 这种方法比前面的两种方法速度要慢,每个元素的处理都需要单独进行;
- 这种的剩余元素处理方法需要两个迭代循环,第一个处理向量的循环,还有处理剩余元素的循环,这会增加代码大小;
- NEON的单一元素加载只改变目标元素的值,而保留其他的元素不变,如果你向量计算的指令会在一个向量间反复计算,如VPADD,这些寄存器需要在第一个元素加载时初始化。
代码片段
@ r0 是输入的数组指针;
@ r1 是输出数组指针;
@ r2 是数组数据的长度;
lsrs r3, r2, #3 @ 计算向量循环迭代的次数
beq singlesetup @ 如果没有完整的一次迭代向量计算,则跳转到单一元素处理循环
@ 处理向量循环
vectors:
subs r3, r3, #1 @减少循环计数器个数
vld1.8 {d0}, [r0]! @ 从数组加载8个元素,从地址r0到寄存器d0,然后更新地址寄存器r0到下一个向量地址;
...
... @ 处理在d0寄存器的数据
...
vst1.8 {d0}, [r1]! @ 把8个结果元素保存到输出数组,更新地址r1到下一个向量
bne vectors @如果r3不等于0,继续循环
singlesetup:
ands r3, r2, #7 @ 计算单一元素迭代的次数
beq exit @ 如果单一元素计算次数为0,则跳转退出
@ 处理单一元素的循环
singles:
subs r3, r3, #1 @减少循环计数器个数
vld1.8 {d0[0]}, [r0]! @从数组加载单一元素,从地址r0到寄存器d0,然后更新地址寄存器r0到下一个地址
...
... @ 处理在d0[0]内的输入数据
...
vst1.8 {d0[0]}, [r1]! @ 保存单一元素结果到输出数组,更新指针地址
bne singles @如果r3不等于0,继续循环
exit:
其他的考虑
在开始处还是结束处
用重叠计算的方式以及用单一元素处理都能在数组开始处或者结束处处理,因而代码就要考虑两种实现方式哪种效率高些,哪个更适合你的系统应用。
数据对齐
加载或者存储指令的地址应该对齐到cache line,这样内存的访问效率更高。这样就需要在Cortex-A8的处理器上至少16字对齐,如果你不能把输入和输出数组的起始地址对齐到16字,你就必须处理开始和结束数据处理的那若干个元素以使得后续的数据访问是对齐到cache行的。为了使用内存对齐的方式访问内存以提高速度,你在使用NEON指令时需要使用诸如64或者128或者256等地址限定符来制定加载和存储指令。你可以比较发出一个对齐的访问和非对齐访问的性能, 以下是始终周期的页面Cortex-A8 TRM.
使用ARM来做修复
在使用单个元素处理的情况下,你可以使用ARM指令来进行单个元素的操作,但是同时使用ARM和NEON来访问同一块区域的内存会降低系统性能,因为从ARM的流水线发出的写操作会在NEON的流水线完成之后才能进行。因而你要尽量的避免在ARM和NEON的代码里同时访问同一块内存区域(当然,这同一块内存区域也对应于同一个cache line)
上一篇:ARM高效C编程和优化--编译器,内存和Cache优化以及功耗管理
下一篇:ARM处理器NEON编程及优化技巧——矩阵乘法的实例
推荐阅读最新更新时间:2024-03-16 15:00