7.6 控制冒險的處理


計算機組成

7 流水線處理器

7.6 控制冒險的處理

Screen Shot 2018-09-29 at 9.14.18 pm

轉移指令由於其自身的特殊性,總是會給我們帶來一些麻煩。那對於流水線處理器來說,更是如此,轉移指令會帶來更多不良的影響。那我們應該如何應對和解決呢?這一節我們就來探索這個問題。

Screen Shot 2018-09-29 at 9.14.28 pm

我們先來看一看轉移指令對流水線的影響。這是一條時間軸,每一小格都代表着一個時鍾周期。那對於5級流水線來說,5個時鍾周期執行完一條指令,但是每一個時鍾周期都可以讀入一條指令,並且從第五個時鍾周期之后,每個時鍾周期也都可以完成一條指令。如果流水線始終處於這樣充滿的狀態,那就達到了我們流水線的性能目標,也就是獲得最大的指令吞吐率。

那我們來看一看這樣一段程序代碼。我們假設在T1的這個時鍾周期去取指的就是這條add指令,那么到了T2周期,取指部件取回add指令,並交給譯碼部件進行譯碼。與此同時,取指部件開始取下一條指令,也就是這條sub指令。然后到T3周期,接着取下一條指令,就是這條beq指令。那通過這段程序我們可以看出,這里很可能會有一段循環。但這只是從我們旁觀者的視角,我們能看到所有的程序代碼。而對於處理器來說,現在正處於T3這個周期時,它正在去取下一條指令,它根本不知道這條指令是什么。當T3這個周期的取指工作完成之后,雖然這條beq指令的指令編碼被取回,也是在T4周期這條beq指令被送到譯碼部件。而取指部件則會依次去取下一條指令,那就是這條load指令,當取回這條load指令的時候,beq指令譯碼也已經完成,但我們仍然不知道它的轉移條件是否滿足,也就是s3、s4這兩個寄存器是否相等。所以,這時候我們只能繼續取指,那再往下取回的就是這條store指令,就是當T5這個時鍾周期完成的時候,beq這條指令也完成了執行的工作,也就是比較完成S3和S4這兩個寄存器的值,這時我們才能知道是否要發生這次轉移。那我們假設轉移的條件是滿足的,這樣已經進入流水線的這條load和store指令,實際上是不應該被執行的。我們只能把它清除,然后重新從正確的地址開始取指,也就是取回這條減法指令,然后再依次取到這條條件轉移指令。但是現在我們所構造出的處理器並不能記住剛才曾從某個地方取回了這條條件轉移指令。所以,在這時處理器只是簡單地去取指令,因此,它會仍然繼續往下取指,再會取到這條load指令,然后再取到一條store指令。只有當這條store指令被取回的時候,剛才取到的這條beq指令才會執行完成,處理器可能又發現原來是要發生轉移的,必須把load和store指令清除掉,然后重新取指。

那么就發現在這個循環的執行過程中,總是反復地執行了兩條正確的指令,然后取回了兩條不應該被執行的指令。從這個圖上來看,就有兩個紅色的橢圓和兩個黑色的橢圓交替出現。那在這段循環執行的過程中,實際上有50%的性能就被浪費了。這也是因為轉移指令本身和流水線的模式是沖突的,因為轉移指令會改變指令的流向,而流水線則希望能夠依次地取回指令,將流水線填滿。

如果這種情況是非常罕見的,也許我們還可以容忍,但實際上轉移指令是非常常用的指令。

Screen Shot 2018-09-29 at 9.14.36 pm

通過對大量程序的分析可以看出,大約每隔4到7條指令就會有一條轉移指令。轉移指令所占的比例大約為15%-25%,而且轉移指令往往會導致若干條不應該被執行的指令進入流水線,而清除這些指令則會帶來時鍾周期的損失。

我們把轉移指令所占的比例乘上轉移指令帶來的時鍾周期的損失,就可以大致地測算出轉移指令對性能的影響。那對於比較簡單的流水線來說,轉移指令帶來的損失可能還不大。但是我們知道現代的處理器都是超標量深度流水的處理器。

Screen Shot 2018-09-29 at 9.14.46 pm

例如像Core i7是4發射16級流水,我們可以簡單地認為,流水線在充滿的時候,可能會有4乘以16,總共64條指令在流水線中。而再看智能手機當中經常使用的ARM Contex-A15處理器,這是一個3發射15級流水線的處理器,我們也可以簡單地認為在流水線中,總共有3乘以15,45條指令在同時執行。一旦出現轉移指令,就有可能導致其后的幾十條指令都是不應該被執行的。所以說,流水線越深,超標量數越多,轉移指令帶來的影響就越大。如果不解決這個問題,那我們花費大量的精力設計的這些深度流水線和超標量結構都將失去意義。

Screen Shot 2018-09-29 at 9.14.55 pm

那我們就來深入地分析一下轉移指令的影響。在執行轉移指令的時候,如果確實發生轉移,那就需要將其后按順序 預取進入流水線的這些指令廢除,也被稱為“排空流水線”。然后從轉移目標地址重新獲取指令。那細分來看,我們主要要做兩項工作,一是要判斷要不要轉移,也就是轉移的條件是否成立。如果執行了一條轉移指令,但實際不需要發生轉移,那剛才按順序進入流水線的指令就不需要被廢除;第二個問題是轉移到哪里,也就是我們為生成目標地址所需要做的工作。

那想要消除轉移指令帶來的影響,我們就要對每一條轉移指令都解決這兩個問題。

Screen Shot 2018-09-29 at 9.15.03 pm

現在我們把轉移指令進行分類的列舉。我們按兩種分類的方法,可以把轉移指令分為4類。

首先來看無條件轉移當中的直接轉移。這里我們列舉了X86和MIPS當中的無條件直接轉移指令,其中我們來看一個例子。這條 j 指令,它是一條無條件轉移指令,也就是執行到這條指令時,一定會發生轉移,而且它還是叫直接轉移指令。這就是說,它轉移的目標地址是在指令編碼中直接給出的。

與之相對的是間接轉移。這里也有一些例子,我們來看其中的一條。 這條jr指令,它也是無條件轉移指令。執行到它的時候,一定會發生轉移,但它轉移的目標地址並沒有在指令編碼中直接給出,而是放在了一個寄存器當中。所以,需要先去讀取這個寄存器的內容,才能得到轉移的目標地址,這就是間接轉移。

然后我們再來看條件轉移。像這條beq指令,它需要比較t0和t1這兩個寄存器的內容是否相等,如果相等則轉移,不相等就不轉移。而且它也是直接轉移類的指令,它的轉移目標地址是直接在指令編碼當中給出的。

那我們就以這三條指令為例,來看一看如何消除轉移指令帶來的影響。

Screen Shot 2018-09-29 at 9.15.11 pm

首先來看無條件直接轉移。在MIPS當中,這是一條j型指令。對於這條指令,我們不用判斷要不要轉移,我們只需要考慮轉移到哪里這個問題。

那這條指令目標地址的計算方法是這樣的。首先這條指令的編碼當中,帶有一個26位的立即數。這個數就是要轉移的目標地址的主體部分,但是我們的目標地址應該是32位的。所以,還差6位,在差的6位當中,低兩位我們用0補上,因為目標地址肯定是四字節對齊的,地址的低兩位肯定是0。然后還缺4位,我們通過當前的PC寄存器計算而得,先將PC寄存器的內容加4,得到的這個32位數,取其高4位,和26位地址以及最低的兩位的0連接起來,構成了一個32位的數,這就是轉移的目標地址。

我們可以看到,在這個目標地址的計算方法只與兩個內容有關:

  1. 當前PC的值;
  2. 這條指令本身的編碼。

Screen Shot 2018-09-29 at 9.15.20 pm

那我們結合處理器的結構圖進行分析。當這條j指令處在取指階段的時候,指令存儲器會送出指令的編碼,如果我們增加一些簡單的電路,就能判斷出這是一條j指令。同時我們將這條指令編碼當中的低26位取出來,在低位加2個0,然后在這個PC更新的部件當中,已經會完成PC加4的工作,那我們再將這個PC加4的高4位取出,然后拼接而得到一個32位的數,這就是我們要更新的PC的值,也就是這條轉移指令的目標地址。那這些工作都可以在一個時鍾周期內完成,並將這個要更新的PC值送到PC寄存器的輸入端,在下一個時鍾上升沿到來的時候,PC寄存器就可以采樣到這個要更新的PC的值。在下一個時鍾周期,PC寄存器送出的就是這條轉移指令的目標地址了。這樣對這條j指令來說,它所需要的轉移目標地址在取指階段就可以獲得,流水線不用停頓。

Screen Shot 2018-09-29 at 9.15.29 pm

我們再看來無條件的間接轉移,也就是jr指令。這條指令是一條R型指令,它的轉移目標地址的計算方法是用指令編碼當中的rs域指定一個寄存器的編號,用這個編號從寄存器堆當中取出對應寄存器的內容。

Screen Shot 2018-09-29 at 9.15.38 pm

那我們還是結合這個流水線的結構圖來看。因為這是間接轉移,所以在取指階段得到指令編碼之后,並不能獲得轉移的目標地址,因此取指部件至少要等待一個周期。當這條JR指令進入到譯碼階段后,指令編碼當中的rs域就會送到寄存器堆,然后得到對應的寄存器的內容,那如果我們在這里把busA這個信號連接到PC的更新部件,那在JR這條指令譯碼階段結束的時候,轉移的目標地址就可以送到PC寄存器的輸入端了。當下一個時鍾上升沿來臨的時候,這個地址就可以存到PC寄存器當中去,然后在下一個時鍾周期,送到指令存儲器。因此,對於這條指令來說,因為我們在譯碼階段才能獲得轉移目標地址,所以流水線需要停頓一個周期。

那暫時就先這樣,我們先接着看其他指令。

Screen Shot 2018-09-29 at 9.15.46 pm

條件轉移指令,它是一條I型指令。這條指令目標地址的計算方法是這樣的。首先比較rs和rt所指向的寄存器的內容。如果它們相等,它們目標地址是在指令編碼當中的16位立即數,進行符號擴展,然后乘以4,再加上當前PC值加4;而如果這兩個寄存器的比較結果是不相等,那新的PC的值就只是當前PC值加4。不管寄存器比較的結果是否相等,這個新的PC的值都只跟當前的PC值和指令編碼的內容相關,而這兩項內容在取指階段都是可以確定的。所以這么看來,目標地址的生成不會造成流水線的停頓。而問題在於,是否要轉移,這個條件的判斷。我們還是結合結構圖來看一看。

Screen Shot 2018-09-29 at 9.15.54 pm

因為要判定轉移是否成立,需要比較兩個寄存器的內容,而寄存器的內容,我們只能在譯碼階段才能獲得。這樣與剛才的間接轉移類似,我們也得讓流水線停頓一個周期,才可以獲得這兩個寄存器的內容。但是與剛才間接指令不同的是,即使到譯碼階段的結束,我們依然不能知道轉移的條件是否成立,因為我們還需要到執行階段,用ALU來對這兩個數進行比較,從而得到比較的結果。所以在這個結構下,我們需要讓流水線停頓兩個周期,才能知道轉移條件的判定結果。其實要等到執行階段結束,無非是要對兩個32位數進行比較,而比較兩個數相等是一個非常簡單的功能,不需要用到ALU這么復雜的部件。那我們就可以在譯碼階段進行一些小的改造。

Screen Shot 2018-09-29 at 9.16.02 pm

我們在寄存器堆的輸出,busA和busB這兩個信號給它連接一個額外的比較電路。這個電路是很簡單的,速度也很快,不至於影響整個譯碼階段的時間。那我們把比較的結果再送到PC的更新部件,這樣在譯碼階段結束的時候,我們就可以將下一條指令的地址送到PC寄存器了。

經過這樣的改動,條件轉移指令也只需要讓流水線停頓一個周期就可以讓指令正確地執行了。

Screen Shot 2018-09-29 at 9.16.11 pm

那通過上面的分析,我們發現,不同的轉移指令帶來的控制冒險是不一樣的。經過我們的改進之后,無條件的直接轉移可以讓流水線不停頓;而無條件的間接轉移以及條件轉移都不得不讓流水線停頓一個周期,才能消除控制冒險的影響。但是如果我們還想進一步地消除這個影響,不讓流水線停頓是否可以做到呢?

Screen Shot 2018-09-29 at 9.16.37 pm

那我們就來介紹一個簡單的方法,就是延遲轉移技術。我們結合這張代碼來進行分析,這里有一條條件轉移指令(左圖中的beq指令),在它之前依次是減法、加法和異或指令。那按照通常的規則,這些指令依次進入流水線執行,當執行到這條beq指令的時候,如果t1、t2兩個寄存器的內容相同,就會跳到Next所指向的地方。在beq進入流水線之后,必須還需要再等一個周期,才能知道轉移條件是否滿足,那流水線必須停頓一個周期。我們現在就是想辦法把這個浪費的周期重新利用起來。既然我們從硬件上現在無法解決這個問題,我們不妨就修改這指令行為的定義,我們就規定它之后的那條指令是一定會被執行的。如果是這樣,流水線中就不會出現被浪費的那個周期了。但是我們還要注意,這樣的修改不應該改變程序本來想要達到的結果,所以我們就需要修改一下這段代碼(左圖),我們要在這個beq指令之后填上一條一定會被執行的指令。那我們只能往上找,但是之前的這條減法指令和加法指令,它們的運算結果正好是beq指令所要比較的這兩個寄存器。所以,這條加法指令和減法指令必須在beq指令之前執行。而我們再往上看,這條異或指令與我們的判定條件沒有關系,現在我們就把這條異或指令挪到beq指令之后,因為我們現在已經修改了轉移指令的定義,那我們在流水線的硬件結構上,就可以確定地將beq之后的這條指令(xor,異或指令)進入流水線。而當這條異或指令完成取指進入譯碼階段的時候,這條beq指令的條件判斷也已經完成。如果條件成立,這時候就可以從Next所指向的這個地方開始取下一條指令了,否則也可以順序地取下一條指令。但不論是哪一種情況,流水線都不會發生停頓。

Screen Shot 2018-09-29 at 9.16.47 pm

對於一個流水線處理器來說,流水線級數越深,流水線結構越復雜,轉移指令帶來的影響就會越大。而且,我們現在也沒有非常完美的解決方案。但也正因為如此,在如何處理轉移指令這個方面,有了很多很有意思的研究成果。大家如果感興趣,可以進一步深入地學習和了解。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM