assembly 重新排序指令后,如何以mips为单位展开点积循环?

uxhixvfz  于 2022-12-19  发布在  其他
关注(0)|答案(1)|浏览(138)

我得到了这个关于循环展开的问题,但我不知道如何一旦我到了下面的步骤,我会告诉你,我甚至不知道这个步骤。我是新的计算机拱,我刚刚有这个代码片段,这是在汇编:

Loop: ld $f0, 0($s1) 
      ld $f4, 0($s2) 
      multd $f0, $f0, $f4 
      addd $f2, $f0, $f2 
      subi $s1, $s1, 8 
      subi $s2, $s2, 8 
      bneqz $s1, Loop

还提供了以下补充信息:

  • 一个周期延迟分支和下表:

| 产生结果的指令|使用结果说明|时钟周期延迟|
| - ------|- ------|- ------|
| FP ALU运算|另一个FP ALU操作|三个|
| FP ALU运算|存储双倍|第二章|
| 负载加倍|FP ALU运算|1个|
| 负载加倍|存储双|无|
我所做的就是插入stall然后尝试重新排序指令:

Loop: ld $f0, 0($s1) 
      ld $f4, 0($s2)
      STALL
      multd $f0, $f0, $f4
      STALL
      STALL
      STALL 
      addd $f2, $f0, $f2 
      subi $s1, $s1, 8 
      subi $s2, $s2, 8 
      bneqz $s1, Loop

我将addd指令移到bneqz指令之后,然后卡住了,有人能帮我解释一下吗?

o7jaxewo

o7jaxewo1#

无需展开或执行任何棘手的操作,您可以隐藏除一个失速周期之外的所有失速周期。

# $f0 = 0.0
# $t0 = end_pointer = $t1+size 
Loop: ld    $f6, 0($t1)
      ld    $f8, 0($t2)
      addiu $t1, $s1, 8         # fills load-delay slot

      multd $f6, $f6, $f8
      addiu $t2, $s2, 8         # multd in flight cycle 1

      bneq $t1, $t0, Loop       # multd in flight cycle 2
                             STALL  waiting for $f6
      addd $f0, $f0, $f6

multdaddd之间只有2条指令。使用**software pipelining**,我们可以避免这种停顿,而无需展开。您也可以将其称为循环旋转。因为出现在源代码中的循环的1次迭代窗口不是以加载开始,也不是以addd结束。循环旋转支持软件流水线(将负载置于multd的阴影中,以设置下一次迭代)。

  • 剥离第一次迭代中的2个负载,以便循环的顶部可以从multd开始
  • 从最后一次迭代中剥离消耗这些加载的multd/addd,因此在循环之后需要这些指令的额外副本。
ld    $f6, 0($t1)    # partially peeled first iteration
      ld    $f8, 0($t2)
Loop:
      # this iteration's loads are already done
      multd  $f4, $f6, $f8   # into a different reg so loads won't overwrite it
      ld     $f6, 8($t1)
      ld     $f8, 8($t2)     # offset=8 for next iter before the pointer increment
      addiu  $t1, $s1, 8 
      addiu  $t2, $s2, 8

      bneq   $t1, $t0, Loop
      addd   $f0, $f0, $f4
# loop ends with pointers pointing one-past-end of the arrays

      multd  $f4, $f6, $f8
      addd   $f0, $f0, $f4

将乘法写入临时寄存器而不是覆盖我们加载的寄存器之一,本质上避免了WAW风险,使这种手动重新排序成为可能。拥有大量架构寄存器可以实现大量的软件流水线操作,即使对于较长的依赖链也是如此。

  • multd与使用其结果的addd之间的5条指令。
  • 最后一个ld和消耗其结果的下一个multd迭代之间的4条指令。时钟频率较高的CPU不会有单周期加载使用延迟,即使L1 d缓存命中也是如此,因此这是一件好事,即使在您正在调整的管道中没有必要。我们可以在指针递增之后加载,但这不会保存任何代码大小。并且在大多数MIPS CPU上无论16位立即数位移量是否为零都可能具有相同的性能。
  • 在迭代上的addd和下一个addd之间的6个指令,在该缩减中的循环携带依赖性。

在更高性能的CPU上,循环携带的依赖性将成为瓶颈,您需要unroll with multiple accumulators来隐藏它。例如,Intel Haswell每个时钟可以运行两个FMA(融合乘加),具有5个周期的延迟,因此您需要至少10个累加器(求和寄存器)来隐藏延迟。每个FMA需要两次加载,因此会遇到瓶颈。(超标量乱序执行,每个时钟最多4个前端微操作,x86能够将FMA +加载合并到一个uop中,以便流水线为指针增量留出空间。这与1-IPC MIPS流水线非常不同,后者即使不展开,也不会遇到任何迭代间瓶颈。)
使用展开,您不需要在此管道上进行软件管道化:

Loop: ld $f6, 0($t1)
      ld $f8, 0($t2)
      ld $f10, 8($t1)
      ld $f12, 8($t2)

      multd $f6, $f6, $f8           # 2 insn gap after loading $f8
      multd $f10, $f10, $f12        # 1 insn gap after loading $f12
      addiu $t1, $s1, 16
      addiu $t2, $s2, 16

      addd $f0, $f0, $f6            # 3 insn gap after multd into $f6
      bneq $t1, $t0, Loop       # 2 insn gap after pointer increment
      addd $f2, $f2, $f10           # 4 insn gap after multd into $f10

# after the loop: reduce the two accumulators to one
      addd  $f0, $f0, $f2       # runs once per dot product, not per element
                                # so a stall hardly matters if you can't schedule it after other post-loop work.

我们几乎可以隐藏FP延迟,而无需多个累加器或软件流水线,以避免循环外的额外addd,如果我们调度到加载延迟的限制。由于我们混合了一些东西,我对与两个不同mul/add操作相关的指令进行了不同的缩进。

Loop:  ld $f6, 0($t1)
       ld $f8, 0($t2)
         ld $f10, 8($t1)
       multd $f6, $f6, $f8         # 1 insn gap after loading $f8
         ld $f12, 8($t2)
     addiu $t1, $s1, 16

         multd $f10, $f10, $f12       # 1 insn gap after loading $f12
       addd $f0, $f0, $f6          # 3 insn gap after multd into $f6
     addiu $t2, $s2, 16

     bneq $t1, $t0, Loop
         addd $f0, $f0, $f10          # STALL: 2 INSN GAP after addd into $f0

因此,事实证明,如果没有多个累加器或软件流水线,我们无法通过这种方式隐藏所有延迟。
如果必须匹配纯串行代码的数值结果(比如不使用-ffast-math、strict-FP进行编译),可以通过软件管道来隐藏一个停顿周期,旋转循环,以便加载填充乘加之后的间隙。
但是多个累加器通常在数值上更好,假设你求和的是非负的东西均匀分布,所以你把同样大小的小数字加到越来越大的值上,舍入误差越来越差。通常它在数值上更精确。就像在 * 这是用SSE overkill处理数组尾部的方法吗?* 其中SIMD向量的元素是多个累加器,我们看到比简单的串行求和更少的错误。
使用两个累加器只需要在循环外额外花费一个addd(超出任何展开检查),而循环旋转剥离循环体的整个迭代(除了指针增量),部分在循环之前,部分在循环之后。

我对你的循环做了一些其他的修改:

  • 使用$t临时寄存器;不需要为循环计数器使用调用保留的$s寄存器。
  • 指针向前递增,直到到达另一个寄存器中的结束指针。类似于do{ p1++; p2++; } while(p1 != endp);,而不是向下计数,直到指针为空(地址0)。
  • 将指针视为无符号指针;如果其中一个数组恰好跨越内存的低2GiB和高2GiB边界,您不希望在有符号溢出时陷阱。(MIPS add/sub指令在有符号溢出时陷阱,addu/subu指令不会。它们是对数据的相同二进制运算。)

相关问题