計算機系統基礎 數據冒險的處理

參考鏈接:
7.5 數據冒險的處理
https://www.cnblogs.com/houhaibushihai/p/9737442.html

其他友情鏈接:
(1)流水線的基本原理
https://www.cnblogs.com/houhaibushihai/p/9735883.html
(2)流水線的優化
https://www.cnblogs.com/houhaibushihai/p/9736358.html
(3) 超標量流水線
https://www.cnblogs.com/houhaibushihai/p/9736470.html
(4)流水線的冒險
https://www.cnblogs.com/houhaibushihai/p/9736616.html
(5)數據冒險的處理
https://www.cnblogs.com/houhaibushihai/p/9737442.html
(6)控制冒險的處理
https://www.cnblogs.com/houhaibushihai/p/9737711.html

7流水線處理器——數據冒險的處理

PPT來源:《計算機組成》 陸俊林

在程序當中,我們經常會對同一個變量進行反覆的使用和修改。這樣對於流水線處理器來說,就會經常出現數據冒險的情況,我們必須很好的應對和解決。在這一節,我們就來看一看有哪一些不同的解決方法。
在這裏插入圖片描述

我們先來看這個數據冒險的例子。產生這個數據冒險,是因爲第二條加法指令會用到第一條減法指令的運算結果。但是在流水線當中,這條加法指令在讀取t0寄存器的時候,它前一條減法指令還沒有把運算結果寫到t0寄存器當中去。所以,這裏就存在一個數據冒險。要解決這個數據冒險,最簡單的方法,實際上是在軟件層面進行解決。

假設我們這個處理器的流水線並不能解決這樣的數據冒險。那其實,我們只要通過編程的手段,人爲的將這條加法指令推後執行,讓他讀取寄存器堆的時間推後到減法指令寫寄存器堆的時間之後。那這應該怎麼做呢?
在這裏插入圖片描述
我們有一條指令叫做nop,它的作用是什麼也不幹。我們就在這個減法指令和加法指令之間插入兩個nop指令。這兩個nop指令只是簡單的通過流水線,並佔用了相對應的時間。那這樣剛纔的這個數據冒險至少是不存在了。而因爲這兩個nop指令的作用,加法指令推後了兩個週期才進入流水線,那麼當這條加法指令需要讀寄存器堆的時候,前面減法指令已經完成了對寄存器堆的寫,那加法指令就可以從寄存器堆當中讀到正確t0的值,從而完成正確的加法運算。所以,解決這個數據冒險最簡單的方法就是插入nop指令,但是這個方法也有很大的問題。

首先,到底應該插入幾個nop指令,這是和流水線的結構相關的。如果我們這一段程序放在一個5級流水線上是正常運行的。那過幾天,又出了一個更新的處理器,它的流水線是8級的,那這個程序放上去,可能運行就會發生錯誤。因爲流水線變深之後,解決數據冒險需要等待的週期數可能會變多。所以,插入nop的這個方法可行,但是並不好。在一般情況下,我們還希望軟件屏蔽硬件的這些實現細節。那既然加了兩個nop指令能夠解決問題,那麼就可以嘗試在硬件上完成相同的工作。

在這裏插入圖片描述
剛纔通過插nop的方法,其實已經給我們提供了借鑑。我們只要發現存在這樣的數據冒險,我們就在硬件的流水線上讓各個控制信號都變成執行nop指令一樣的值。那在這兩個週期,就會產生流水線停頓的效果。而這些和nop指令效果一樣的控制信號,它們所產生的狀態就成爲一個空泡。這個空泡隨着時鐘週期一級一級往後面傳,從效果上來看,和nop指令在流水線當中一級一級的執行是一樣的。只是區別在於,這樣的信號是由硬件來產生的。

那現在又有了一個新的問題,如果剛纔是在軟件中插入了nop指令,那對於這個流水線來說,它是嚴格的按照取回一條指令進行執行,這樣的方式來運轉的。那現在需要在硬件上自動的插入空泡,就需要一個方式來檢測是否出現了數據冒險。當然這也不難,如果我們不是看這一段程序代碼,而是看處理器當中的這五個部件,那我們怎麼來判斷存在數據冒險呢?

所謂數據冒險,就是當前有一條指令要讀寄存器,而它之前的指令要寫寄存器,但又沒有完成。所以,我們只用檢查,在譯碼這個階段,需要讀的寄存器的編號,這個通過鏈接在寄存器讀口的信號就可以得到。然後我們再檢查後面各個階段,其實在每一級,都有些信號能夠表明這條指令是否要寫某個寄存器,以及要寫哪個寄存器。因此,我們只需要檢查後面每一個階段所要寫的寄存器的編號,和當前譯碼階段,所要讀讀寄存器的編號是否有相同。如果存在相同,那就是有數據冒險。只要出現來數據冒險,我們就在流水線中插入空泡。這樣我們就能通過硬件來解決數據冒險的問題。但是,在實際的編程當中,這種先寫了一個寄存器,然後很快使用的狀況是經常出現的。如果說每次出現,我們都要讓流水線停頓的話,對性能的影響就太大了。所以,我們不能只追求做對,還要要求做好。我們還是希望流水線不要停頓。
在這裏插入圖片描述
那這個就是最初我們分析的樣子。減法指令在800ps之後纔開始寫寄存器,而加法指令最晚在500ps的時候就要去讀寄存器。我們無法逆轉這個時間,所以我們肯定不能把800ps纔有的數送到500ps的這個時間去。但是我們可以換一個角度想一想。這條減法指令的運行結果真的是在800ps這個時候纔有的嗎?實上減法運算是在執行階段(EX)由ALU這個部件完成的。所以,最晚在600ps時候,要寫到t0寄存器當中的數就已經運算完成了。所以,從時間角度來看,在600ps之後,我們就可以得到t0寄存器的最新的值。而對於這條加法指令,它真的需要使用t0寄存器的值是在它的執行階段,也就是ALU的部件需要用t0的值作爲其中的一個輸入,那這個階段是在600ps之後纔開始的,我們完全可以將減法運算的結果交給這個加法運算作爲輸入。這種方法就叫做數據前遞,也就是上一條指令將自己的運算結果往前傳遞到下一條指令去。

那我們剛纔已經分析過,在600ps的時候,ALU的輸出結果已經是t0的值了,那在600ps這個時鐘上升沿過去之後,t0的這個值會被保存到執行(EX)和訪存(MEM)之間的這個流水線寄存器當中去。我們如果把它傳遞給ALU的輸入,就可以正確的完成後面這條加法運算了。

那既然從時間上是可行的,我們就可以來看一看硬件上怎麼來修改。
在這裏插入圖片描述
這條減法指令在執行完運算以後,運算結果已經保存到了這個寄存器當中(1處)。 那現在,這條減法指令進入到訪存階段,t0的值將會通過這個階段傳到下一級流水線寄存器(2處)。而與此同時,加法指令正在執行階段,它需要將t0寄存器的值送到ALU的一個輸入端(3處),那顯然,ALU的上一個階段從寄存器堆當中讀到的值(4處),肯定不是最新的。現在這個最新的值在訪存階段的連線上。所以,我們從硬件連線上可以把這個信號引回來,從新引到ALU的輸入端(5處)。

當然,這裏(3處)我們還需要增加一個多選器,而且我們剛纔也講過,如何去判斷在流水線當中出現了數據冒險。那我們就可以用這樣的判斷結果作爲這個多選器的選擇信號,在出現數據冒險的時候,我們選擇這個前遞的信號。當然,這條加法指令也有可能在第二個原操作數(s4)上使用了t0寄存器。所以,這個前遞的信號還應該傳送到ALU的另一個輸入端,當然在這裏(ALU的另一個輸入端,即3處下面)也需要加上多選器來進行選擇。

那這樣的方式就被成爲前遞,它還有個名稱叫作旁路。從根本上來說,前遞和旁路指的都是這件事情。只不過是觀察和描述的角度不同而已。前遞是從指令執行順序的角度來描述的,而旁路則是從電路的結構角度來描述。本來前一條指令應該將運行的結果寫入到寄存器堆(6處),然後再交給後一條指令使用。而我們現在搭建了一條新的通路,相當於繞過了寄存器堆,直接進行了數據的傳遞。所以,從硬件實現的角度來看,這是一個旁路。

那這就是前遞和旁路的關係。那我們進一步來看,其實不僅僅在這個點(7處)可以建立旁路,我們在下一個流水級也可以建立旁路(8處)。

在這裏插入圖片描述
那這條旁路在什麼情況下會用上呢?
在這裏插入圖片描述
我們還是結合一個例子來看。這個例子前兩條指令和剛纔的那個例子是一樣的,在此基礎上我們又寫出了第三條指令。這是一個與操作,那麼它其中的一個源操作數也是t0。那我們結合實踐來看,對於這條與操作指令,它真的要開始運算的時候,是在800ps之後,在這個時候,前面這條減法指令已經完成了訪存階段(MEM)。所以,t0寄存器的最新值現在是放在訪存階段和寫回階段之間的流水線寄存器當中的。那我們就需要用到剛纔的結構圖當中紫色的旁路的線,用來將t0的內容傳遞到ALU的輸入端,從而讓這條與運算指令能夠及時的運行。

那如果再往後一條指令又用到了t0會怎麼樣呢?那麼這個標着3的指令在800ps之後的這個時鐘週期正好進入了譯碼階段(ID),它會在這個週期的後半部分讀取寄存器。那麼在這個時候,減法指令已經將t0的值寫入到了寄存器堆中。所以,對於這個3號指令,如果它用到了t0這個寄存器,它就可以按照正常的操作,從寄存器堆當中讀出t0寄存器的值,而不需要使用前遞的技術。 所以,對於這樣運算指令,我們建立的這兩組旁路的通路,就已經可以解決數據冒險了。但是還是有一種例外的情況,我們通過一個新的例子來看。
在這裏插入圖片描述
在這個例子當中,前三條指令還是和剛纔一樣,第四條是一個Load指令,它也會用到t0寄存器,但是我們剛纔已經分析過了,這個時候並不存在數據冒險。而這條Load指令是要把存儲器當中的一個數取出來,存放到t1寄存器當中去。而它之後,一條或運算指令會使用t0寄存器的值,那這種情況就是一條Load指令之後跟了一條指令,會使用Load指令的目的寄存器。那在這種情況下,也會發生數據冒險。它有個專門的名稱,叫作load-use冒險。

那麼這種冒險是否也可以用前遞的技術來解決呢?實際上是做不到的,那我們來分析一下爲什麼做不到。對於這一條Load指令,我們來看要保存到t1寄存器的值,究竟是什麼時候纔得到的?對於剛纔的運算指令,需要寫回寄存器的值,是在執行階段,也就是通過ALU運算而得。但是對於Load指令,用ALU是計算要訪存的地址,而要寫回寄存器堆的數,是在訪存階段的結束纔會得到。所以,是在1400ps這個地方,我們纔會得到t1寄存器的值。而對於下面這一條或運算指令,我們最晚也得在1200ps這個地方,得到t1這個寄存器的值,從而讓ALU可以進行正確的運算。因此,這就要求我們將1400ps這個地方得到的數,傳遞到之前1200ps這個時刻。時光倒流的事情我們是做不到的。所以,我們只能讓信號沿着時間軸向前傳遞,而絕不可能向後傳遞,因此,無論我們怎麼修改電路,也無法構造出一條前遞的通路。那我們應該怎麼來解決這個load-use的這個冒險呢?其實說難很難,說簡單也就很簡單。
在這裏插入圖片描述
還是用我們那個萬能的方法。既然我們不能返回到更早的時間,那我們只能讓這條或運算指令多等一個週期,這樣它就可以在1400ps之後才需要這個t1寄存器的值,而此時Load指令已經完成了從數據存儲器當中取出數的操作。這就可以通過剛纔我們已經建立的第二組旁路通道,也就是用紫色的連線表達的這個旁路通道,將t1寄存器的內容傳送到ALU的輸入端口。那當然,既然我們要讓或運算指令延後一個週期,我們就必須在流水線中插入空泡,讓流水線產生一次停頓。所以,對於這種冒險,我們需要用流水線停頓再加上數據前遞的方式來解決。

那這個解決方案沒有讓流水線獲得最高的指令吞吐率,這當然是一個遺憾,但是保證指令執行正確纔是我們的首要目標。所以,我們也只能接受這樣的方案了。

現在,對於一個基本的流水線結構,我們已經能夠處理數據冒險了。但是,如果繼續增加流水線的深度,或者擴展成超標量流水線,又會出現新的數據冒險的情況。當然,與之對應的又有很多精巧的解決方案。如果你對此感興趣,還可以進一步的深入學習。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章