assembly 放置NOP,以确保MIPS组装中无RAW数据危险

lokaqttq  于 2022-12-27  发布在  其他
关注(0)|答案(1)|浏览(164)

目前,我正致力于理解MIPS架构和汇编语言,有人要求我将NOP放在下面的汇编代码中:
我根据我所受的教育来排列NOP,得到了这个:
这让我感觉很不舒服,我相信有一种方法可以放置更少的NOP,并且仍然可以处理这个代码中的所有原始数据危险。
编辑:
在看到第2行和第3行没有依赖性之后,我已经能够将NOP数量减少3,因为软件没有更改R3中存储的值。

s6fujrry

s6fujrry1#

这让我感觉很不舒服,我相信有一种方法可以放置更少的NOP,并且仍然可以处理这个代码中的所有原始数据危险。
在我看来,所有这些NOP都是必需的,因为解码一条指令后的3个周期是它在classic 5-stage RISC中写回的时间。在这个周期内,另一条指令可以输入ID并从寄存器文件中读取值,而无需专门检测RAW危险来设置旁路转发或暂停流水线。
2个NOP的情况看起来正确地放置在指令和使用其结果的指令之间有一个独立指令的位置。
指令使用前一条指令的结果是正常的,特别是在它不是加载的情况下,这就是为什么真正的CPU需要避免像这样的暂停才能不出错。例如,第一代商用MIPS R2000具有旁路转发功能,除了缓存未命中的加载/存储或mult/div之外,任何指令都不会暂停。一条指令可以使用前一条指令的结果,而不会有任何损失。
它甚至不会因为加载而停止,相反,软件必须遵守"加载延迟槽",直到1条指令之后才使用加载结果(否则,根据缓存未命中的情况,无法保证您看到的是旧的还是新的寄存器值!)后来的MIPS修订版改变了这一点,因为编译器通常不能用NOP以外的任何值填充它,这会损害指令缓存的密度,而分支延迟槽则是另一回事;它无法更改,但它是使经典MIPS隐藏分支延迟且不需要分支预测forwarding from the first half-cycle of beq to the 2nd half-cycle of the IF stage的另一部分。
(Also,此代码被故意安排得很糟糕(特别是在加载之后立即存储),以便在赋值的下一步中为您提供一些操作。它也完全是人工的,计算一些在循环内部没有使用的结果,而只能在加载之后执行(也就是从循环中退出),或之前完成(提升)像14 SLT R21,R11,R28,它不依赖于任何其他指令。但这将使调度"更难",因为这些无用的指令像NOP一样作为填充符工作。)
Presumablythe point of this exercise is to show you that CPUs would have tons of stalls that are hard to schedule around if they didn't have bypass forwarding. Mostly only highly unrolled (and software-pipelined) loops could spend most of their cycles doing work instead of being stalled. https://en.wikipedia.org/wiki/Classic_RISC_pipeline#Solution_A._Bypassing
我似乎找不到一种通过重新调度来有效减少NOP数量的方法,我所能减少的大多数是5 - 6个NOP,这让我觉得还有很多事情可以做。如果你要重新调度我上面展示的MIPS汇编代码,你会怎么做?
有一些WAW/WAR危险会阻止重新排序;如果我们可以使用不同的寄存器以及重新排序,这将打开更多的可能性。例如,SUBI R15,R15,99可以减到临时中,因此它可以在SW R14,2000(R15)之前/与SW R14,2000(R15)并行地完成。或者,存储可以在寻址模式中使用2099(R15)来抵消减。(有关这种优化的示例,请参见 * 如何在重新排序指令后以mips为单位展开点积循环?*)。
我刚刚意识到我还没有考虑到在重新排序加载和存储时内存别名的可能性。例如,如果SW R7,2000(R16)/LW R15,1000(R7)可以是该存储的重载,那么稍后调度存储将改变程序。让我们假设我们可以假设没有别名。

查看相关性链并使用相关性链确定最先执行哪些指令(最长dep链的开头优先)之后:

## Without considering memory aliasing, reordering some stores after loads.

1        ADD R2,R1,R3
            3 NOPs
3        ADD R4,R3,R2  # dep on 1 (R2).  R4 is read-only after this, in the loop
2        SW R3,0(R2)   # dep on 1 (R2)
            1 NOPs    # put this NOP outside the loop because 4 only needs to stall on the first iter.
    LOOP: 
8        LW R7,1000(R16)       # no deps on anything in the shown code
4        LW R8,2000(R4)     # dep on 3 (R4) before loop.  R8 is only written here.
14       SLT R21,R11,R28    # independent of everything.
              1 NOPs
10       LW R15,1000(R7)       # dep on 8 (R7)
5        SUB R5,R4,R8          # dep on 4 (R8).  (which also depends on the same R4 value from 3 we read)
9        SW R7,2000(R16)       # dep on 8 (R7).  R16 is read-only
6        ORI R23,R8,370        # dep on 4 (R8).  R23 is write-only
11       SW R14,2000(R15)      # dep on 10 (R15).  R14 is read-only
12       SUBI R15,R15,99       # dep on 10 (R15), WAR on 11
7        ADD R19,R5,R8         # dep on 5 (R5), and 4 (R8) indirectly via 5 as well as directly.  R19 is write-only
            2 NOPs
13       AND R6,R14,R15        # dep on 12 (R15).  R14 is read-only
            3 NOPs
15       BEQ R21,R6,LOOP       # dep on 14 (R21), 13 (R6)

循环内的NOP从17个减少到6个(循环前还节省了2个NOP,这有助于减少代码大小/I缓存占用空间)。

    • 这可能不是最佳的,但我没有看到任何可以节省任何东西的单指令的移动。**将11 SW R14,2000(R15)转换为SW R14,2099(R15)将允许12 SUBI R15,R15,99更快地开始一个周期,并且存储可以填充循环尾部附近的一个停顿槽。和/或将subi转换为不同的寄存器将允许sub首先运行,允许相同的益处而不延长相关性链。

涉及R15的依赖链:8 LW R7,1000(R16)-〉10 LW R15, 1000(R7)-〉12 SUBI R15,R15,99-〉13 AND R6,R14,R15-〉15 BEQ R21,R6,LOOP11 SW R14,2000(R15)还在SUBI之前读取R15负载结果。
4 LW R8,2000(R4)开头的Dep链:循环中没有写R4,所以这个加载可能只是被提升到循环之外,除非其他指针为同一地址提供别名。多个东西读R8,但只有加载写它。从R8的这些读操作中:

  • 6 ORI R23,R8,370是一个死胡同,R23中没有读取任何内容(因此我们应该将其从循环中删除,以防循环之后的内容需要R23中的R8 | 370;我们知道循环中的任何东西都不起作用。)
  • 5 SUB R5,R4,R8-〉7 ADD R19,R5,R8在那里是死胡同; R5或R19没有读取,因此它们可以类似地从循环中被吸收。(R4和R8在循环之后仍然具有相同的值。)

相关问题