Coding Neon - Part2:Dealing With Leftovers

原文链接:[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的代码来写同一块内存,特别是同一行缓存、


================================================

下一篇文章:Code for Neon - Part3:Matrix Multiplication

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值