1 計算機指令架構
1.1 基本概念
MIPS ——內部無互鎖級微處理器( Microprocessor without interlocked piped stages ),采用RISC 指令集,所有的指令長度相同,運行周期也相同。
計算機硬件的基本功能就是執行 指令 ,指令在馮 · 諾伊曼計算機中由二進制數字進行編碼。計算機的全部二進制機器指令組成了一種可供人與計算機進行交流的語言,稱為 機器語言 。
助記符形式的指令的集合組成了 匯編語言 。
編譯 ——將高級語言編寫的程序翻譯成等價的二進制指令序列來代替,計算機執行等價的機器語言程序。 解釋 ——以高級語言程序作為輸入數據,順序地檢查它的每一條語句,并直接執行等價的機器語言指令序列。
1.2 MIPS指令格式
簡潔性 ——所有指令長度相同,都是32位
區分性 ——opcode用于區分不同操作
硬件設計角度 ——不同的opcode編碼方式硬 件結構會有差異
1.2.1 R型指令
操作碼 ——R,I,J都會包含6bit,用于區分最多可以用于區 分 2^6=64 種指令,這個數字并不足夠,因此還需要于后面的 6bit 的功能碼一起確定不同的指令。
源操作數 1、2 (rs,rt)——R 型指令的兩個操作數均來自于寄存器,按照寄存器的編號確定使用哪兩個寄存器。因為在 MIPS 當中一共只有 32 個寄存器,所以 5bit 足以編號。
目標寄存器 (rd)——與源操作數一樣,按照寄存器的編號確定使用哪個寄存器,并用 5bit 進行編號。
位移量 (shamt)——用于對寄存器內的數字進行位移。
功能碼 (funct)——在同一操作碼下區分不同的操作
1.2.2 I型指令
6+5+5+16=32bit
操作碼 (opcode)——與 R 相同,I 型指令不需要功能碼進行輔助 區分。
源操作數 (rs,rt)——I 型指令的源操作數可能有一個,也可能有兩個,其中第一個源操作數的寄存器編號存儲在 rs 中。
目標寄存器 (rd)——當 I 型指令沒有第二個源操作數時,則第二個寄存器編號代表了目標 寄存器。
立即數 (Imm)——16bit 數字,根據操作碼的區別對應不同的含義。
1.2.3 J型指令
偽直接尋址 ——在當前指令的一定的范圍內進行尋址。跳轉地址為指令中的 26 位常數與 PC 中的高位拼接得到,也就是說:新的 PC = { PC[31..28], target address, 00 } (00:+4)
1.3 尋址方式
1.4 指令系統
CPU 可以利用RIJ三類指令構成一套指令系統,完成一系列指定的任務。
1.4.1 數據處理指令
算數運算指令:加法與減法
add $t0, $t1, $t2 # $t0 = $t1 + $t2
sub $t2, $t3, $t4 # $t2 = $t3 - $t4
算數運算指令:立即數加法與減法
addi $t2, $t3, 5 # $t2 = $t3 + 5
addi $t2, $t3, -5 # $t2 = $t3 - 5
邏輯運算指令:或、與等
and $t0, $t1, $t2 # $t0 = $t1 & $t2
or $t0, $t1, $t2 # $t0 = $t1 | $t2
xor $t0, $t1, $t2 # $t0 = $t1 ⊕ $t2
nor $t0, $t1, $t2 # $t0 = ~($t1 | $t2)
nor $t0, $t1, $zero # $t0 = ~$t1
邏輯運算指令:移位運算
sll $t0, $t1, 10 # $t0 = $t1 < < 10,立即數邏輯左移
srl $t0, $t1, 10 # $t0 = $t1 > > 10,立即數邏輯右移
sra $t0, $t1, 10 # $t0 = $t1 > > 10,立即數算術右移
sllv $t0, $t1, $t3 # $t0 = $t1 < < ($t3%32),邏輯左移
srlv $t0, $t1, $t3 # $t0 = $t1 > > ($t3%32),邏輯右移
srav $t0, $t1, $t3 # $t0 = $t1 > > ($t3%32),算術右移
比較指令 :u 代表無符號(unsigned), i 代表立即數(immediate)
slt $t1,$t2,$t3 # if ($t2 < $t3) $t1=1;
slt $t1,$t2,$t3 # else $t1=0
sltu $t1,$t2,$t3 # 無符號比較
slti $t1, $t2, 10 # 與立即數比較
sltui $t1, $t2, 10 # 與無符號立即數比較
1.4.2 數據傳送指令
lw 和 sw ——寄存器與存儲器之間的數據傳輸
lw $t1, 30($t2) # Load worda
sw $t3, 500($t4) # Store word
壓棧操作 ——根據**sp 指向的存儲器的位置,可以將寄存器的數據進行壓棧操作,每壓棧一次,**sp 的內容就會指向原來位置-4。
addi $sp, $sp, -12
sw $s1, 8($sp)
sw $s2, 4($sp)
sw $s3, 0($sp)
出棧操作 ——將數據出棧并保存到寄存器中,每出棧一次,$sp 的內容就會 指向原來位置+4。
lw $s1, 8($sp)
lw $s2, 4($sp)
lw $s3, 0($sp)
addi $sp, $sp, 12
裝入高位立即數
lui $t1, 0x1234
ori $t1, $t1, 0xabcd #將32位立即數0x1234abcd裝入$t1寄存器
1.4.3 分支與跳轉指令
分支指令
beq $t0, $t1, Target # 如果$t0 =$t1,則分支執行標號為 Target 的指令
bne $t0, $t1, Target # 如果$t0!=$t1,則分支執行標號為 Target 的指令
無條件跳轉
j Label#無條件跳轉到標號 Label 處
這是一條 J 型指令 ,前面 在介紹 J 型指令 也提到過 ,對于 Label 對應地址的方式為偽直接尋址 。
while循環
Loop: sll $t1, $s3, 2 # 以 4 的倍數尋址,將 i 的值存入 $t1
add $t1, $t1, $s6 # 將偏移量加上基地址( save )存入 $t1
lw $t0, 0($t1) #從 $t1 指向的位置讀出數據,存入 $t0
bne $t0, $s5, Exit #判斷 $t0 是否等于 k ,若等于則結束
addi $s3, $s3 #上面一句不等于的話執行這步,i=i+1
j Loop #繼續循環
Exit:…… #執行其他代碼
過程調用
jal Procedure #將返回地址 (PC+ 保存在 $ra 寄存器中,程序跳轉到過程 Procedure處執行
jr $ra #跳轉到寄存器指定的地址,子程序返回通過寄存器跳轉指令jr進行
葉過程 ——不調用其他過程的過程,僅需要將返回地址寄存器 ra和在被調過程中修改了的保存寄存器s0~~$s7進行壓棧操作即可。
主過程 ——調用了葉過程的過程,除了需要將ra和s0~~**s7 壓棧外,還需要使用的參數寄存器 **a0~**a3 和臨時寄存器 **t0~~$t9 壓棧,用于保存當前程序執行的中間變量。
1.5 評價計算機性能的指標
響應延時 ——系統從開始做一項任務到任務完成所需要的總時間,包括CPU 運算,磁盤讀寫,內存讀寫等
吞吐量 ——系統單位時間內處理的任務總數,服務器以及工作站更看重這一點,吞吐量更大的計算系統在面對大量任務請求時,能夠更快的完成所有任務(這時大部分的任務都在隊列中等待完成,等待的時間遠大于任務的響應延時)。
指令平均周期數( Clock cycles Per Instruction CPI ) ——平均一條指令所需要的周期數
CPU執行時間 =指令數×CPI×時鐘周期。通過優化編譯器減少指令總數,或者通過增加 CPU 的復雜性降低 CPI ,或者優化 CPU 的關鍵路徑降低時鐘周期。但是對其中任意一項的優化,都有可能導致另外兩項的提升,同時也有可能增加成本,功耗等參數。
不影響其他兩項的情況下對其中一項進行優化:編譯器優化使得同樣一段高級語言的代碼翻譯成匯編后的指令數更少,代價是增加了編譯程序的時間;選擇 更快的電路實現與生產工藝 ,增加生產成本和功耗;流水線或超標量通過設計處理器的體系架構,在時鐘周期變化不大的情況下,讓原本只能一個周期完成一條指令的系統變為可以一個周期完成多條指令,也可以成倍的增加系統的性能。
2 存儲器
2.5 虛擬內存和外設
面臨的問題:
編譯時編譯器無法獲知物理地址 ——由于編譯器在編譯時無法獲知程序運行時使用的地址空間分配情況,程序必須使用邏輯地址尋址,這就需要在邏輯地址和物理地址之間轉換。
多個程序需要共享物理內存空間 ——同一系統上可能需要同時運行多個程序,這些程序無法共享各自的邏輯地址,但物理內存空間有限,如果為所有程序都分配獨立的物理地址空間將造成嚴重的浪費。我們希望更高效地共享物理內存,避免某一程序長期獨占物理內存,浪費空間。
2.5.1 頁式管理
將地址空間劃分為小的、等大的頁( page ),以頁為單位進行分配和共享 。在運行時進行物理地址和邏輯地址之間翻譯的硬件稱為存儲器管理單元( Memory Management Unit, MMU )。操作系統以頁為單位管理內存、完成邏輯地址與物理地址之間的轉換;操作系統負責將每個程序中活動的頁放入物理內存,這樣一來各個程序只需要使用邏輯地址,并且邏輯地址空間可以高于其實際使用的物理內存大小,看起來有一種獨享內存和地址空間的感覺,大大簡化了編程的難度。同時,由于各個程序的頁相互獨立,頁式管理也便于操作系統提供保護機制 ,避免程序之間內存空間的互相訪問。有了分頁管理,不用的頁不必放在內存中,這大大提高了物理內存的使用效率。
頁式管理需要維護一個頁位置和地址轉換關系的數據結構,稱為頁表。與頁式管理相對的還有段式管理,即以不等大的內存段為單位管理內存。這種管理方式目前很少使用。
2.5.2 虛擬存儲器
頁式管理當中不活動的頁不需要放入內存中。實際計算機系統中,不用的頁通常是被放入磁盤上的。實際上,磁盤可以看作是主存的一種擴充,可以把主存和磁盤一起看成一個大的虛擬的存儲器 。這樣就用磁盤來將那些在主存儲器內放不下的數據保存起來 ,這樣做的好處有 :編程時不必考慮因為內存不足而帶來的各種約束可以通過操作系統有效的管理有限的內存從而在各個程序進程 之間共享 。
虛擬存儲器(虛擬內存)是建立在主存輔助存儲器結構基礎之上,由軟件操作系統 和硬件 ( 相結合管理的存儲系統 。編好的程序由操作系統裝入輔助存儲器中,程序運行時,操作系統把輔存的程序一塊塊自動調入主存由 CPU 執行 。當發生主存儲器分配溢出時,操作系統透明地將主存儲器中的段或頁移到輔助存儲器中,并將其標記為無效,然后可以將物理內存分配用做其他用途。
引入虛擬存儲管理后,程序的邏輯地址也稱為虛擬地址。在發生缺頁異常(訪問的頁不在主存中)時,CPU 自動轉到缺頁中斷處理程序進行處理 。缺頁中斷處理程序由操作系統提供。它通過頁故障寄存器中的虛擬內存地址,計算出相應的頁表項地址,根據頁表項中查得的外存地址,從磁盤中讀出新的頁到主存中,然后允許程序重新訪問。
在實際的虛擬存儲器應用中,頁的大小通常是4-16 KB 。虛擬存儲器是全相聯的,一個虛擬的頁可以映射到內存中(幾乎)任何一個位置。頁的替換規則通常采用 : LRU Least Recent Used) 。由于磁盤的寫入代價非常大,通常采用寫回機制處理臟頁。
2.5.3 地址和地址轉換
每個進程都有自己的地址空間,操作系統和硬件協同工作,為每個進程 進行各自的 地址轉換 。為了完成這一操作,需要維護一個地址轉換 表 頁表 。只有操作系統才能修改頁表 。通過適當操作頁表、轉換地址,可以限制 一個進程 無法 獲得訪問其他進程地址空間的權限 。
由于現代存儲器技術的發展,頁表越來越大,將全部頁表存在內存中往往不現實,也不高效。解決這一問題的方法是使用兩級頁表,第一級頁表始終放在內存中,第二級頁表的 1024 項有一部分在內存中 另外一部分在磁盤上或者可以不分配。
快表 (TLB)—— 頁式管理使每 次存儲器訪問都帶來額外的存儲器訪問,即在訪問所需數據之前要訪問 1 次頁表 采用多級頁表結構時額外的訪問次數更多 。這些花銷通常可以通過TLB(Translation Look aside Buffer ,地址變換高速緩存 來避免 。TLB 是一種 Cache ,用來存儲最近用過的頁轉換關系,由硬件實現 。TLB 也稱為快表,而頁表則稱為慢表 。
2.5.4 虛擬存儲器與Cache
相同之處都把存儲器劃分為一個個信息塊,運行時都能自動地把信息塊從慢速存儲器向快速存儲器調度,信息塊的調度都采用一定的替換策略,新的信息塊將淘汰最不活躍的舊的信息塊,以提高繼續運行時的命中率。新調入的信息塊需遵守一定的映射關系變換地址后來確定其在存儲中的位置。
不同之處Cache 存儲器采用與 CPU 速度匹配的快速存儲元件來彌補主存和 CPU 之間的速度差距,而虛擬存儲器雖然最大限度地減少了慢速輔存對 CPU 的影響,但它的主要目的是為了彌補了主存的容量不足,具有容量大和程序編址方便的優點 。
兩個存儲體系均以信息塊作為存儲層次之間基本信息的傳遞單位,Cache 存儲器每次傳遞是定長的 信息塊,長度只有幾十字節,而虛擬存儲器信息塊劃分方案很多,有頁、段等等,長度均在幾百字節至幾千字節左右 。
主存——Cache 存儲體系中 CPU 與 Cache 和主存都建立了直接訪問的通路,一旦在 Cache未命中, CPU 就直接訪問主存,并同時向 Cache 調度信息塊,從而減少了 CPU 等待的時間;輔助存儲器與 CPU 之間沒有直接通路,一旦在主存中不命中,則只能從輔存調度信息塊到主存.因為輔存的速度與 CPU 的速度差距太大,調度需要毫秒級 時間,因此, CPU 一般將改換執行另一個程序,等到調度完成后再返回原程序繼續工作 。
主存——Cache 存儲器存取信息的過程、地址變換和替換策略全部用硬件實現,所以對程序員來說是透明的。虛擬存儲器則是由硬件 ( 和軟件 操作系統 ) 相結合管理的存儲系統。地址變換由 MMU 硬件負責,頁表的設置由操作系統負責,頁面調度由操作系統實現,所以對設計存儲管理軟件的系統程序員來說,虛擬存儲器是不透明的 。
2.5.5 外圍設備
首先是訪問地址。虛擬內存地址是邏輯的、可以被擴展,不但可以映射到物理內存,還可以映射到外設。
然后是訪問時機。CPU 有兩種訪問外設的策略:輪詢和中斷。輪詢方式是 CPU 每隔一段時間詢問外設是否需要 I/O 。這種方式可能會浪費大量的 CPU 時間 在無意義的外設訪問上,但其實現簡單,不需要復雜的中斷處理程序。中斷方式是當外設需要 I/O 時, 向 CPU 發送 一個中斷 。只有出現中斷時, CPU 才停下來與比較慢的外設通信 。這種方式比較經濟,但處理中斷比較復雜。
3 微處理器設計原理
3.1 單周期MIPS處理器
3.1.1 數據通路設計
不能直接并行連接每種類型需要的數據通路,這樣會增加太多的冗余硬件單元。為了復用硬件單元,要用到多路選擇器和相應的控制邏輯。增加多路選擇器比增加 ALU增加寄存器堆訪問端口要更加經濟。
指令集子集按照實現的功能分為四類:寄存器-寄存器運算、寄存器-立即數運算、訪存操作(寄存器-內存搬運)、分支和跳轉操作。若按照指令格式分類,可以分為 R 型、I 型、J 型。
存儲單元
存儲器 ——內存存儲指令和/或數據
寄存器堆 ——R、I需要訪問。R需要兩個讀端口一個寫端口;不是所有指令都寫所以有寫使能信號RegWr。可以為RS和RT提供讀取,為RT或RD 提供寫入
程序計數器PC ——指向當前正在執行的指令在內存的地址,需要電路根據是否分支跳轉更新PC
運算電路
算數/邏輯運算ALU ——支持操作數的加減、移位等運算。
擴展電路 ——I型操作數是16位立即數時根據指令作符號擴展或零擴展
PC更新電路 ——可以加4或立即數擴展
寄存器-寄存器運算數據通路
指令從Rs, Rt 讀出兩個操作數并送入 ALU 進行某種運算,運算輸出存入Rd中。數據通路主要由PC單元、寄存器堆、ALU之間的連接組成。
寄存器-立即數運算數據通路
ALU 操作數一個從寄存器 (Rs)中讀出,另一個由指令中給出的立即數(imm16)做擴展得到。結果寫回Rt。
訪存操作數據通路
LW指令使用ALU用Rs和16位立即數擴展的結果進行加法運算,得到的結果作為內存地址從內存地址中讀出數據寫回 Rt。
SW指令同樣計算出內存地址。從 Rt 中讀出數據寫入該內存地址,內存的寫使能 WrEn應該置1。
分支和跳轉操作數據通路
Beq 指令判斷 Rs 和 Rt 是否相等,如果相等則修改 PC 至相對當前 PC 位置為 imm16 符號擴展并×4 的字節位置。由于單周期一 個 ALU 模塊不能被用于兩個計算,這個符號擴展和相加的邏輯將在 Instruction Fetch Unit 這個模塊內部由 一個單獨的 ALU 解決,但是符號擴展單元可以復用。所以 數據通路只需要將 ALU 減法的輸出是否為 0 輸入給 PC 模塊即可。
跳轉指令的數據通路較為簡單,只需將 26 位立即數左移兩位作為 內存地址 更新 PC 值即可。
3.2 多周期MIPS處理器
單周期處理器的數據通路主要是組合邏輯,時鐘同步行為主要是PC更新、寫入寄存器和內存。
多周期將指令可能經過的幾個階段拆分開來,不同的指令經過的階段數不同,執行時間不同,希望減少固定周期帶來的時間浪費。
- 首先在單周期基礎上添加寄存器負責在一條指令的幾個周期間的信息傳遞,每一個階段的輸出結果都應該有寄存器保存;
- 取指令和訪問數據內存肯定會分在兩個周期進行,所以存儲器復用,數據存儲和指令存儲采用相同的地址空間,用同一套端口訪問;
- 不同階段之間 ALU 可以共用,不再需要額外為分支指令和順序PC+4 增加加法器;
- 由于 一條指令跨多個周期,控制信號邏輯不再是簡單的組合邏輯,而是一個根據指令類型進行轉移的有限狀態機。
3.3 異常和中斷
異常和中斷是除了分支和跳轉指令以外,特殊的改變程序控制流的方式。
中斷主要指的是來自處理器外部 ,外設在有事件發生比如新的網絡包到來等時向處理器提出中斷處理請求。中斷產生與程序執行是異步的。
異常是來自處理器內部的被處理器檢測到的事件。分為軟件導致的異常比如被零除、 訪問段錯誤等,和硬件異常比如MMU檢測到非法內存訪問時。異常通常與程序執行流是同步的。
為什么需要在硬件上實現中斷和異常處理機制
如果我們不使用中斷的方式處理異常,還可以讓處理器忙輪詢外設來和外設交互 。IO設備比處理器速度慢很多數量級,大量浪費了處理器的能力;如果使用中斷處理 外部事件,可以讓處理器在有外部事件導致中斷時再來處理,其他時間都可以繼續進行計算。現代操作系統中,當一個進程開始等待外部 IO 事件時,這個進程常被 暫時掛起,處理器開始運行另一個進程,直到該進程等待的中斷來臨,才將該進程重新標記為可運行,等待被調度 。
很多異常本身就很不容易通過軟件檢測,比如未知指令錯誤會導致硬件進入未知狀態。另外本來也存在硬件異常,軟件很難處理 。同時我們也想要將設計一些異常處理策略的權利交給軟件設計者,所以我們不能只在硬件上用單一的方式處理異常,而是要實現一個能提供給軟件設計者異常處理彈性的機制。
作為硬件設計者,我們想要實現異常處理的機制,然后把實現機制的具體策略的權利提供給軟件開發者,又是一個將機制和策略明確分離的典例 。所以我們將異常處理做成一個隱式 的過程調用,由軟件設計者提供異常處理程序,硬件上實現發生異常時調用該程序的機制。
硬件知道某種異常要調用哪個異常處理程序一般有兩種方法:
- (x86) 向量方式 ——每個異常都有自己的異常處理程序,其入口保存在 一個固定地址處。
- (MIPS) 原因寄存器 ——只使用一 個通用的異常處理程序,每個異常事件必須將足夠的信息加載到原因寄存器中,以便異常處理程序知道是何種異常發生并采取相應措施 。
3.4 MIPS處理器流水線設計
硬件設計中,流水線通常是通過在較長的組合邏輯之間添加寄存器來實現的。通過插入寄存器,可以將延時較長的組合邏輯分解為多步來完成,不同的步驟可以在多次計算上并行。由于組合邏輯被拆解,延時降低,從而時鐘頻率可以提升,達到加速的目的。所有的寄存器采用同一時鐘觸發。
3.4.1 MIPS處理器流水化
IF ——根據 PC 讀取指令寄存器中的指令,并更新 PC 為 PC+4 或 EX 階段計算出的跳轉地址。
ID ——指令的譯碼,省略了控制信號的解碼部分。同時,根據寄存器地址讀出兩個用于計算的寄存器中的值,完成立即數的擴展。
EX ——擴展的立即數和寄存器讀出的第二個數通過多路選擇器得到 ALU 需要的一個操作數。另一個操作數固定為從寄存器堆中讀出的第一個數。兩者通過 ALU 完成計算。同時,由于 j 指令的需要,擴展的立即數經過移位后與 PC+4 的結果進行相加,得到需要跳轉到的地址。從寄存器讀出的第二個數作為可能的讀寫數據存儲的地址也被單獨寄存。
MEM ——根據需要有三種選擇:將 ALU 輸出結果寫入數據存儲;根據地址讀取數據緩存;將 ALU 輸出結果傳至下一級。
WB ——將 MEM 階段寄存的數據通過多路選擇器進行選擇后,寫回寄存器。 寫回地址在 ID 階段即得到,跟隨流水線逐級傳遞到 WB 階段。為了使數據盡早地寫回寄存器以便被后面的指令使用,可以令寄存器組采用時鐘下降沿觸發寫入 ,而流水線的 中間寄存器采用時鐘的上升沿觸發寫入 。這樣,相當于在前半個時鐘周期寫入寄存器組,后半個時鐘周期將數據讀出。
3.4.2 流水線處理器的性能
吞吐率 ——單位時間內處理的指令數。吞吐率分為實際吞吐率和最大吞吐率。實際吞吐率為系統運行時實際在單位時間內處理的指令數。最大吞吐率則為系統在達到理想的穩定運行狀態時的吞吐率。理想吞吐率為 1 指令每周期 。
加速比 ——不使用流水線的執行時間和使用流水線的執行時間之比。類似于吞吐率的定義,我們可以定義實際加速比和最大加速比。k 級流水線可以達到的最大的最大加速比為 k 。
無法達到最大加速比:
- 最長的一段決定了流水線運行的最小時鐘周期,成為流水線運行的 瓶頸 ;
- 插入寄存器引入額外的建立時間和保持時間,每級流水結算的邏輯路徑長度之和超過純粹的組合邏輯路徑長度,最長的會超過原路徑的1/k。這說明無限制地增加流水級不能無限制提升性能。
流水線設計對于指令數并沒有任何的優化。相比于單周期設計而言,流水線設計可以有效縮短時鐘周期;相比于多周期的設計而言,流水線設計主要降低了 CPI 。流水線的不同階段在執行的是不同的指令,因此是在完成指令級的并行處理。流水線是指令級并行的基本技術之一。其他的指令集并行技術還包括超標量以及超長指令字。
3.4.3 流水線冒險
解決冒險的最簡單和直接的方式是阻塞流水線。通過在指令之間插入空指令(bubble)將相鄰的指令間隔拉開。直觀的認識是,如果插入足夠的空指令使得相鄰指令在流水線上完全不重疊,那么流水線處理器的運行就和單周期處理器的運行方式一樣,也就不會有冒險存在。當然這也嚴重影響了流水線處理器的吞吐率。因此冒險的解決辦法是以盡量不阻塞流水線為目標來制定的。
結構冒險——硬件資源使用沖突
流水線中的兩條指令需要同時訪問相同的硬件資源。通常出現在某個單元未完全按照流水線方式實現時。比如,對 load 指令采用 5 級流水,第5 級時寫回寄存器。而對于 R 型指令,由于 EX 階段之后即得到結果,可以在第 4 階段就寫回,那么當 load 指令和 R 型指令相鄰依次出現時就會出現同時向寄存器寫入兩個數的情況,發生結構冒險。又比如,當數據存儲器和指令存儲器共享同一個 RAM 資源時,取指令和數據的讀寫也可能發生沖突。
為了避免結構冒險,一方面需要將所有硬件資源固定分配到指定的流水級,另一方面需要所有的指令都順序完成所有的流水級,不能跳過或停留。當資源沖突時,往往需要增加資源來避免沖突。比如將數據存儲器和指令存儲器分開,將 ALU 與計算 PC 的單元分開。
數據冒險——數據因指令關聯不可獲得
當前指令讀取的寄存器,應該被之前的一條指令寫入后再使用。但是之前的指令在流水線中尚未完成。
數據冒險出現在當前指令和其相鄰的前兩條指令之間。因此,如果不采用任何措施,需要至多插入 2 條空指令來阻塞流水線。采用的解決策略是盡可能將已經得到的結果向前傳遞(forwarding)至需要的位置,而不等待其寫入寄存器堆之后再去使用。
為了縮短代碼長度,必要的阻塞我們也采用硬件的方式來完成。
1)ALU輸入端數據選擇
(00——寄存器堆;10——EX/MEM;01——MEM/WB)
2)無法解決的load-use冒險
(stall=1阻塞流水線,IF 暫停,并且在 ID 插入空指令。只需要將 RegWrite 和 MemWrite 信號置 0 即可。PC 通過 stall 信號禁止其寫入)
前一條指令為 load 指令,而當前指令需要用 load 得到的數據進行計算。這種情況我們稱之為 load use hazard 。判斷需要阻塞流水線的條件為:當 ID/EX 階段的寄存器表明當前指令需要讀內存,而 IF/ID 階段表明下一條指令需要使用寄存器,且該寄存器將要被當前指令寫入
硬件處理load use hazard 會帶來性能上的損失。防止流水線阻塞的另一個方法是在代碼編譯時進行優化。一些編譯器通過將無關的指令放在 load 指令和需要使用其數據的指令之間,相當于將本來需要插入空指令的地方利用了起來,從而提高了執行效率。
3)MEM寫入端的數據選擇
當load-use是由于復制存儲器引起的,則可以不必阻塞一個時鐘周期,可以選擇增加新的 forward 路徑來完成這個轉發過程,通過判斷寄存器 id 和操作碼來決定是否轉發。
控制冒險——跳轉分支指令使PC無法及時確定
控制冒險定義為:下一條需要執行的指令,依賴于當前正在流水線中執行的指令的結果。控制冒險的出現頻率通常比數據冒險要低,但是也不容易解決。可能的解決方案包括:阻塞流水線、提前判斷、預測、延遲決定。
j和 jr 指令在 ID/Reg 階段能夠確定之后 應該執行的指令。而 beq 指令則需要經過 ALU計算結果之后再判斷才能確定是否要跳轉。因此,如果純粹地插入空指令來等待的話, j 和jr 指令要損失一個周期,而 beq 指令則要損失 2 個時鐘周期。
1)阻塞流水線
阻塞流水線能夠省去無謂的空指令。但是為了支持對 j jr beq 指令的支持, hazard unit 中的邏輯會更復雜一些,需要添加對 IF/ID 寄存器中指令類型以及 ID/EX 寄存器中指令類型的判斷。另外,還需要將 j 指令中跳轉地址的計算也轉移到 ID 階段來執行。
2)提前判斷
由于在指令譯碼之后我們才能確定指令的類型,因此進一步提前j 和 jr 指令的跳轉相對困難。將 beq 指令的判斷放在 ID 階段執行:
- 在 ID 階段的寄存器堆的輸出端增加一個比較器用來判斷是否分支跳轉
- 將判斷的結果接入跳轉地址的多路選擇器的選擇端
- 對于比較器的輸入,需要額外的 forward 邏輯來處理
可能從 EX, MEM 或者 WB 階段轉發。從 EX 階段的轉發是不太實際的。因為需要的是 EX 階段計算的結果而不是輸入,如果進行轉發,意味著存在著一條新的邏輯路徑,從 ID/EX 寄存器出發,經過 ALU ,比較器以及 PC 的選擇端到 PC 寄存器。這很可能會延長關鍵路徑,降低處理器性能。從 WB 階段的轉發沒有必要,因為我們寫入寄存器堆和寫入流水線中的寄存器采用不同的時鐘沿。因此我們只需要從 MEM 階段到 ID 階段的 forward 邏輯。
3)分支預測
一段程序中出現的跳轉應該主要來自于循環操作。如果我們可以通過某種比較可靠的方式來猜測一條 beq 指令是否跳轉,那么我們就能在大多數時候不阻塞處理器。比如通過一個表來存儲 beq 指令的地址和上一次是否跳轉,然后每次執行時查表并執行和之前一次同樣的行為。
然而這樣的預測不可能 100% 準確,因此我們還需要處理預測出錯的情況。通常,當我們發現預測跳轉出錯時,可以將已經在流水線中的指令清空( flush )。相比于提前判斷,分支預測對于長流水線的處理器更友好。
4)延遲槽
在采用了提前判斷的技術之后,我們已經將分支所需要阻塞的時鐘周期降低到1 個。進一步提高處理器性能可以利用這一個周期。具體的做法是在這個周期中插入和分支無關,總是要執行的指令。這便是延遲槽技術。
延遲槽技術屬于軟件技術,通常由高級語言的編譯器或匯編程序的編寫者來決定。延遲槽技術通常會打亂原有程序的順序,造成可讀性降低。并且不總是存在可以放到延遲槽中的指令。但是這種方法不需要額外的硬件。
3.4.4 流水線異常和中斷
為了在硬件上支持異常和中斷處理,我們需要:
- 根據異常或中斷的具體 情況,將 PC跳轉到處理程序的入口;
- 將已經在流水線中的指令清空。考慮以下三種情況:溢出,錯誤的操作碼,外部中斷。認為前兩種情況都在 EX 階段被發現和處理。所以這里我們需要將 EX 、 ID 以及 IF 階段的指令清空。清空的操作很簡單,只需要在流水線寄存器的寫入端添加一個二路選擇器,一端為 0 ,另一端為正常輸入當需要清空時選 0 ,否則選正常輸入。
評論
查看更多