目前正在理解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在循环之后仍然具有相同的值。)