1. 什么是數(shù)據(jù)冒險
當前指令需要使用之前指令的運算結果,但是結果還沒有寫回。
2. 數(shù)據(jù)冒險舉例
比如下圖中,第2條add指令需要使用第1條sub指令的結果,第1條sub指令在900ps時才會將運算結果$t0寫回寄存器堆,而第2條add指令在500ps時就需要從寄存器堆中讀取sub指令的結果了。這里就造成了數(shù)據(jù)冒險。

3. 數(shù)據(jù)冒險的解決
3.1 解決方法一:人為的延后add指令-軟件延后
如下圖所示,軟件加入2條nop指令,add指令在第4拍進入流水線,當add指令需要讀取寄存器的值時,sub指令剛好已經(jīng)完成了寄存器的寫。

軟件插入nop指令解決數(shù)據(jù)冒險的方法并不好,因為軟件不知道應該插入幾條nop指令。上面這樣插入2條nop指令可以解決5級流水線的數(shù)據(jù)冒險問題,那么如果軟件移植到8級流水就不可行了。
3.2 解決方法二:人為的延后add指令-硬件延后
對于5級流水,可能插入2個nop指令就可以解決這個數(shù)據(jù)冒險,但是如果移植程序到一個新型CPU上執(zhí)行,該新型CPU采用8級流水,這個方法可能就不行了。一般對于軟件來說,都要屏蔽硬件的實現(xiàn)細節(jié),因此這里考慮第二種解決數(shù)據(jù)冒險的方法,硬件插入bubble,如下圖所示,

這里有個問題需要解決,就是硬件如何知道哪里會產(chǎn)生數(shù)據(jù)冒險和應該插入幾個bubble?
流水線每個階段都在為不同的指令服務,那么檢查ID階段也就是寄存器的讀口上的地址,再檢查后面各個階段的硬件操作的寄存器是否和寄存器的讀地址相等,就可以判斷數(shù)據(jù)冒險。
這種解決方法不好,因為這樣降低了流水線的效率。
3.3 解決方法三:數(shù)據(jù)前遞Forwarding
如下圖所示,sub指令實際上在600ps時就已經(jīng)通過ALU計算出了t0的值,不需要等sub完成寫回,就可以將ALU的計算結果傳遞給下一條指令,即add指令,這就是數(shù)據(jù)前遞。如下圖所示,600ps時,sub指令計算結果存放在了EX/MEM流水線寄存器中,將這個結果傳遞給ALU的輸入端完成數(shù)據(jù)前遞。

從硬件實現(xiàn)上來看,如下圖所示,直接將ALU經(jīng)過流水線寄存器的輸出接回ALU的輸入,完成數(shù)據(jù)前遞。ALU的輸入端需要加2選1多路器,控制信號是數(shù)據(jù)冒險的檢測結果。

再后一級的流水線寄存器的輸出也可以做成數(shù)據(jù)前遞,如下圖所示,

上面紫色線所指示的前遞可以解決下面這個數(shù)據(jù)冒險的例子,第三條指令是一個and指令,當這個and指令ALU需要使用t0的值時,t0剛好完成MEM階段操作,在MEM/WB流水線寄存器中。

我們注意,再往后的指令,例如instruction 3如果也要使用t0,ID階段讀取寄存器時,sub指令剛好已經(jīng)完成了WB,就不存在數(shù)據(jù)冒險了。
因此,對于運算指令,綠色和紫色的前遞已經(jīng)可以解決數(shù)據(jù)冒險的問題。但是還有一種例外情況。
3.4 load-use冒險
上面對于運算指令的數(shù)據(jù)冒險已經(jīng)可以通過前遞或者說旁路bypass解決。但是下面這個例子,使用前遞就無法解決,

第4條指令是一個load指令,lw指令使用t0寄存器,這部分已經(jīng)不存在數(shù)據(jù)冒險,因為lw指令在ID階段讀取t0寄存器時,sub指令剛好已經(jīng)完成了WB。
注意這條lw指令在ALU階段計算出了要訪問數(shù)據(jù)存儲器的地址,接著在1400ps時完成了數(shù)據(jù)存儲器的讀取,但是lw指令的下一條指令or指令最晚在1200ps就需要t1的數(shù)據(jù),這樣無論如何都不能將1400ps才可以產(chǎn)生的數(shù)據(jù)前遞至1200ps使用。
解決方法就是那個最簡單又最笨的辦法,插入bubble。如下圖所示,流水線stall一個周期后,再使用紫色的數(shù)據(jù)前遞就可以解決這個load-use冒險。
