一、引子
1、解決不同指令之間的數據依賴問題。
上一講,我為你講解了結構冒險和數據冒險,以及應對這兩種冒險的兩個解決方案。一種方案是增加資源,通過添加指令緩存和數據緩存,讓我們對於指令和數據的訪問可以同時進行。
這個辦法幫助CPU解決了取指令和訪問數據之間的資源沖突。另一種方案是直接進行等待。通過插入NOP這樣的無效指令,等待之前的指令完成。這樣我們就能解決不同指令之間的數據依賴問題
2、上一講的這兩種方案這兩種方案都有點兒笨。
着急的人,看完上一講的這兩種方案,可能已經要跳起來問了:“這也能算解決方案么?”的確,這兩種方案都有點兒笨。
第一種解決方案,好比是在軟件開發的過程中,發現效率不夠,於是研發負責人說:“我們需要雙倍的人手和研發資源。”而第二種解決方案,好比你在提需求的時候,研發負責人告訴你說:“來不及做,你只能等
我們需求排期。” 你應該很清楚地知道,“堆資源”和“等排期”這樣的解決方案,並不會真的提高我們的效率,只是避免沖突的無奈之舉。
那針對流水線冒險的問題,我們有沒有更高級或者更高效的解決方案呢?既不用簡單花錢加硬件電路這樣“堆資源”,也不是純粹等待之前的任務完成這樣“等排期”。
答案當然是有的。這一講,我們就來看看計算機組成原理中,一個更加精巧的解決方案,操作數前推
二、NOP操作和指令對齊
要想理解操作數前推技術,我們先來回顧一下,第5講講過的,MIPS體系結構下的R、I、J三類指令,以及第20講里的五級流水線“取指令(IF)-指令譯碼(ID)-指令執行(EX)-內存訪問(MEM)-數據寫回
(WB) ”。我把對應的圖片放進來了,你可以看一下。如果印象不深,建議你先回到這兩節去復習一下,再來看今天的內容
在MIPS的體系結構下,不同類型的指令,會在流水線的不同階段進行不同的操作。
我們以MIPS的LOAD,這樣從內存里讀取數據到寄存器的指令為例,來仔細看看,它需要經歷的5個完整的流水線。STORE這樣從寄存器往內存里寫數據的指令,不需要有寫回寄存器的操作,
也就是沒有數據寫回的流水線階段。至於像ADD和SUB這樣的加減法指令,所有操作都在寄存器完成,所以沒有實際的內存訪問(MEM)操作。
有些指令沒有對應的流水線階段,但是我們並不能跳過對應的階段直接執行下一階段。不然,如果我們先后執行一條LOAD指令和一條ADD指令,就會發生LOAD指令的WB階段和ADD指令的WB階段,
在同一個時鍾周期發生。這樣,相當於觸發了一個結構冒險事件,產生了資源競爭。
所以,在實踐當中,各個指令不需要的階段,並不會直接跳過,而是會運行一次NOP操作。通過插入一個NOP操作,我們可以使后一條指令的每一個Stage,一定不和前一條指令的同Stage在一個時鍾周期執行。
這樣,就不會發生先后兩個指令,在同一時鍾周期競爭相同的資源,產生結構冒險了。
三、流水線里的接力賽:操作數前推
通過NOP操作進行對齊,我們在流水線里,就不會遇到資源競爭產生的結構冒險問題了。除了可以解決結構冒險之外,這個NOP操作,也是我們之前講的流水線停頓插入的對應操作。
但是,插入過多的NOP操作,意味着我們的CPU總是在空轉,干吃飯不干活。那么,我們有沒有什么辦法,盡量少插入一些NOP操作呢?不要着急,
下面我們就以兩條先后發生的ADD指令作為例子,看看能不能找到一些好的解決方案。
add $t0, $s2,$s1 add $s2, $s1,$t0
這兩條指令很簡單。
1. 第一條指令,把 s1 和 s2 寄存器里面的數據相加,存入到 t0 這個寄存器里面。
2. 第二條指令,把 s1 和 t0 寄存器里面的數據相加,存入到 s2 這個寄存器里面。
因為后一條的 add 指令,依賴寄存器 t0 里的值。而 t0 里面的值,又來自於前一條指令的計算結果。所以后一條指令,需要等待前一條指令的數據寫回階段完成之后,才能執行。
就像上一講里講的那樣,我們遇到了一個數據依賴類型的冒險。於是,我們就不得不通過流水線停頓來解決這個冒險問題。我們要在第二條指令的譯碼階段之后,插入對應的NOP指令,
直到前一天指令的數據寫回完成之后,才能繼續執行。這樣的方案,雖然解決了數據冒險的問題,但是也浪費了兩個時鍾周期。我們的第2條指令,其實就是多花了2個時鍾周期,運行了兩次空轉的NOP操作。
不過,其實我們第二條指令的執行,未必要等待第一條指令寫回完成,才能進行。如果我們第一條指令的執行結果,能夠直接傳輸給第二條指令的執行階段,作為輸入,那我們的第二條指令,
就不用再從寄存器里面,把數據再單獨讀出來一次,才來執行代碼。
我們完全可以在第一條指令的執行階段完成之后,直接將結果數據傳輸給到下一條指令的ALU。然后,下一條指令不需要再插入兩個NOP階段,就可以繼續正常走到執行階段。
這樣的解決方案,我們就叫作 操作數前推(Operand Forwarding),或者操作數旁路(OperandBypassing)。其實我覺得,更合適的名字應該叫 操作數轉發。這里的Forward,
其實就是我們寫Email時的“轉發”(Forward)的意思。不過現有的經典教材的中文翻譯一般都叫“前推”,我們也就不去糾正這的“轉發”(Forward)的意思。不過現有的經典教材的中文翻譯一般都叫“前推”,我們也就不去糾正這個說法了,你明白這個意思就好。
轉發,其實是這個技術的 邏輯含義,也就是在第1條指令的執行結果,直接“轉發”給了第2條指令的ALU作為輸入。另外一個名字,旁路(Bypassing),則是這個技術的 硬件含義。為了能夠實現這里的“轉發”,
我們在CPU的硬件里面,需要再單獨拉一根信號傳輸的線路出來,使得ALU的計算結果,能夠重新回到ALU的輸入里來。這樣的一條線路,就是我們的“旁路”。它越過(Bypass)了寫入寄存器,再從寄存器讀出的過程,也為我們節省了2個時鍾周期。
操作數前推的解決方案不但可以單獨使用,還可以和流水線冒泡一起使用。有的時候,雖然我們可以把操作數轉發到下一條指令,但是下一條指令仍然需要停頓一個時鍾周期。比如說,我們先去執行一條LOAD指令,再去執行ADD指令。LOAD指令在訪存階段才能把數據讀取出來,所以下一條指令的執行階段,需要在訪存階段完成之后,才能進行。
總的來說,操作數前推的解決方案,比流水線停頓更進了一步。
流水線停頓的方案,有點兒像游泳比賽的接力方式。下一名運動員,需要在前一個運動員游玩了全程之后,觸碰到了游泳池壁才能出發。
而操作數前推,就好像短跑接力賽。后一個運動員可以提前搶跑,而前一個運動員會多跑一段主動把交接棒傳遞給他。
四、總結延伸
這一講,我給你介紹了一個更加高級,也更加復雜的解決數據冒險問題方案,就是操作數前推,或者叫操作數旁路。
操作數前推,就是通過在硬件層面制造一條旁路,讓一條指令的計算結果,可以直接傳輸給下一條指令,而不再需要“指令1寫回寄存器,指令2再讀取寄存器“這樣多此一舉的操作。
這樣直接傳輸帶來的好處就是,后面的指令可以減少,甚至消除原本需要通過流水線停頓,才能解決的數據冒險問題。
這個前推的解決方案,不僅可以單獨使用,還可以和前面講解過的流水線冒泡結合在一起使用。因為有些時候,我們的操作數前推並不能減少所有“冒泡”,只能去掉其中的一部分。
我們仍然需要通過插入一些“氣泡”來解決冒險問題。
通過操作數前推,我們進一步提升了CPU的運行效率。那么,我們是不是還能找到別的辦法,進一步地減少浪費呢?畢竟,看到現在,我們仍然少不了要插入很多NOP的“氣泡”。
那就請你繼續堅持學習下去。下一講,我們來看看,CPU是怎么通過亂序執行,進一步減少“氣泡”的。