我得到了这个关于循环展开的问题,但我不知道如何一旦我到了下面的步骤,我会告诉你,我甚至不知道这个步骤。我是新的计算机拱,我刚刚有这个代码片段,这是在汇编:
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
指令之后,然后卡住了,有人能帮我解释一下吗?
1条答案
按热度按时间o7jaxewo1#
无需展开或执行任何棘手的操作,您可以隐藏除一个失速周期之外的所有失速周期。
在
multd
和addd
之间只有2条指令。使用**software pipelining**,我们可以避免这种停顿,而无需展开。您也可以将其称为循环旋转。因为出现在源代码中的循环的1次迭代窗口不是以加载开始,也不是以addd
结束。循环旋转支持软件流水线(将负载置于multd
的阴影中,以设置下一次迭代)。multd
开始将乘法写入临时寄存器而不是覆盖我们加载的寄存器之一,本质上避免了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流水线非常不同,后者即使不展开,也不会遇到任何迭代间瓶颈。)
使用展开,您不需要在此管道上进行软件管道化:
我们几乎可以隐藏FP延迟,而无需多个累加器或软件流水线,以避免循环外的额外addd,如果我们调度到加载延迟的限制。由于我们混合了一些东西,我对与两个不同mul/add操作相关的指令进行了不同的缩进。
因此,事实证明,如果没有多个累加器或软件流水线,我们无法通过这种方式隐藏所有延迟。
如果必须匹配纯串行代码的数值结果(比如不使用
-ffast-math
、strict-FP进行编译),可以通过软件管道来隐藏一个停顿周期,旋转循环,以便加载填充乘加之后的间隙。但是多个累加器通常在数值上更好,假设你求和的是非负的东西均匀分布,所以你把同样大小的小数字加到越来越大的值上,舍入误差越来越差。通常它在数值上更精确。就像在 * 这是用SSE overkill处理数组尾部的方法吗?* 其中SIMD向量的元素是多个累加器,我们看到比简单的串行求和更少的错误。
使用两个累加器只需要在循环外额外花费一个
addd
(超出任何展开检查),而循环旋转剥离循环体的整个迭代(除了指针增量),部分在循环之前,部分在循环之后。我对你的循环做了一些其他的修改:
$t
临时寄存器;不需要为循环计数器使用调用保留的$s
寄存器。do{ p1++; p2++; } while(p1 != endp);
,而不是向下计数,直到指针为空(地址0
)。add
/sub
指令在有符号溢出时陷阱,addu
/subu
指令不会。它们是对数据的相同二进制运算。)