放置NOP以确保MIPS组件中没有RAW数据危险



目前正在理解MIPS体系结构和汇编语言,我被要求将NOP放在以下汇编代码中:

1 ADD R2,R1,R3
2 SW R3,0(R2)
3 ADD R4,R3,R2
4 LOOP: LW R8,2000(R4)
5 SUB R5,R4,R8
6 ORI R23,R8,370
7 ADD R19,R5,R8
8 LW R7,1000(R16)
9 SW R7,2000(R16)
10 LW R15,1000(R7)
11 SW R14,2000(R15)
12 SUBI R15,R15,99
13 AND R6,R14,R15
14 SLT R21,R11,R28
15 BEQ R21,R6,LOOP

我已经根据我被教导的方式放置了NOP,得到了这个:

1        ADD R2,R1,R3
3 NOPs
2        SW R3,0(R2)
3        ADD R4,R3,R2
3 NOPs
4        LOOP: LW R8,2000(R4)
3 NOPs
5        SUB R5,R4,R8 
6        ORI R23,R8,370
2 NOPs
7        ADD R19,R5,R8
8        LW R7,1000(R16)
3 NOPs
9        SW R7,2000(R16)
10       LW R15,1000(R7)
3 NOPs
11       SW R14,2000(R15) 
12       SUBI R15,R15,99 
3 NOPs
13       AND R6,R14,R15 
14       SLT R21,R11,R28 
3 NOPs
15       BEQ R21,R6,LOOP

这对我来说感觉很糟糕,我相信有一种方法可以减少NOP,并且仍然可以在这个代码中处理所有RAW数据隐患。

编辑:

在看到第2行和第3行没有依赖关系后,由于SW没有更改R3中存储的值,我已经能够将NOP的数量减少3。

我觉得这太离谱了,我相信有一种方法可以减少NOP,并且仍然可以处理该代码中的所有RAW数据隐患。

在我看来,所有这些NOP都是必需的,因为解码一条指令后的3个周期是它在经典的5阶段RISC中写回的时候。这是另一条指令可以输入ID并从寄存器文件读取值的周期,而无需对RAW危害进行任何特殊检测,即可设置旁路转发或暂停管道。

2个NOP的情况看起来是正确的,在指令和消耗其结果的指令之间有一个独立的指令。

指令使用上一个的结果是正常的,特别是如果它不是负载,这就是为什么真正的CPU需要避免像这样停滞以避免失败。例如,第一代商业MIPS R2000具有旁路转发,除了缓存未命中加载/存储或mult/div之外,没有任何暂停。指令可以使用前一条指令的结果而不会受到惩罚。

它甚至没有因负载而停滞,相反,软件必须尊重";"负载延迟时隙";,直到1条指令之后才使用加载结果(否则,无法保证您会看到旧的还是新的寄存器值,这取决于缓存未命中!)。后来的MIPS修订版改变了这一点,因为编译器通常不能用除NOP之外的任何东西来填充它,这会影响I-cache密度。分支延迟槽是另一回事;它不能改变,但它是让经典MIPS隐藏分支延迟而不需要分支预测的另一部分,从beq的前半周期转发到IF阶段的后半周期。

(此外,这段代码被故意安排得很糟糕(尤其是在加载后立即存储),以便在下一步的任务中给您一些事情要做。它也是完全人工的,计算一些不在循环中使用的结果,这些结果可以在循环之后完成(也就是从循环中沉出来),也可以在循环之前完成(提升),比如14 SLT R21,R11,R28,它不依赖于任何其他指令。但这将使日程安排";更硬";,因为这些无用的指令就像NOP一样起填充作用。)

据推测,本练习的目的是向您展示,如果CPU没有旁路转发,它们将有大量难以调度的暂停。大多数情况下,只有高度展开(和软件流水线)的循环才能将其大部分周期用于工作,而不是停滞。https://en.wikipedia.org/wiki/Classic_RISC_pipeline#Solution_A._Bypassing

我似乎找不到通过重新安排来有效减少NOP数量的方法,我最多能减少5-6个NOP,这让我觉得还有很多事情可以做。如果你要重新安排我上面显示的MIPS汇编代码,你会怎么做?

有一些WAW/WAR危害会阻止重新排序;如果我们可以使用不同的寄存器以及重新排序,这将打开更多的可能性。例如CCD_ 3可以减法到临时中,从而可以在CCD_。或者存储器可以在寻址模式中使用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为6,低于17。(在循环之前也保存了2个NOP,这有助于代码大小/I-缓存占用空间。)

这可能不是最佳的,但我看不到任何可以保存任何内容的单个指令的移动11 SW R14,2000(R15)转换为SW R14,2099(R15)将允许12 SUBI R15,R15,99更快地开始循环,并且存储可以填充循环尾部附近的一个暂停槽。和/或将subi放入不同的寄存器将允许子首先运行,从而在不延长依赖链的情况下获得相同的好处。

涉及R15:8 LW R7,1000(R16)->10 LW R15, 1000(R7)->12 SUBI R15,R15,99->13 AND R6,R14,R15->CCD_ 16。11 SW R14,2000(R15)还在SUBI之前读取R15加载结果。

4 LW R8,2000(R4)开始的Dep链:没有任何东西在循环中写入R4,所以这个负载可能会被从循环中提升出来,除非其他指针别名相同的地址。多个东西读取R8,但只有负载写入

  • 6 ORI R23,R8,370是一个死胡同,没有什么能读取R23(所以我们应该把它从循环中删除,以防循环后的某个东西稍后需要在R23中使用R8 | 370;我们知道循环中没有什么能做到。)

  • 5 SUB R5,R4,R8->CCD_ 22在那里是死胡同;没有读取R5或R19,因此这些可以类似地从循环中被吸收。(R4和R8在循环之后仍然具有相同的值。)

相关内容

  • 没有找到相关文章

最新更新