4 處理器體系結構
第四章的目标是設計一個 Y86-64 的處理器,并運作設計好的 Y86-64 的指令集。
什麼是指令集
指令集 ISA,也就是處理器可以處理的指令的集合,Y86-64 的指令是簡化版的 X86-64 指令,他把許多指令都細化了,例如 movq 拆分成了多個 irmovq,rrmovq 等等,直接在指令中寫清楚兩個操作數的來源以及他們的轉移方向。
簡化的指令集也讓處理器的設計更加簡潔和友善,本章主要是設計順序處理器與流水線處理器,并不涉及亂序處理器的設計。而順序處理器也分為兩個版本
SEQ
初始版本以及
SEQ+
版本,流水線處理器分為
PIPE-
和
PIPE
版本,也就是說一共有四種不同的處理器版本。
什麼是順序處理器
所有指令都是串行執行的,執行完上一條指令然後再執行下一條。
優點是設計比較簡單,不需要考慮資料冒險,控制冒險之類的問題。
缺點是對處理器的利用效率比較低下,因為一個指令分為多個階段,由于是順序執行,導緻會讓一部分的階段處理硬體空閑,是以可以進行一些改進。
什麼是流水線處理器
這裡我們設計的流水線處理器是将順序執行的指令處理階段拆分成多個不同階段,使得處理器在同一時間内,不同階段可以處理不同的指令,讓所有硬體都得到充分的利用。
優點是利用效率比較高,指令執行速度更快,吞吐量更大。
缺點是設計比較複雜,會有很多的異常情況需要處理,需要處理資料冒險,控制冒險,組合冒險等等情況。
前置知識與指令集設計
涉及到 HCL 硬體控制語言的定義,邏輯門,組合邏輯電路,時鐘寄存器等粗淺的硬體知識,還要有對處理器整體的認知。我們使用組合邏輯電路,時鐘寄存器,随機通路存儲來組成一個最簡單的 CPU。
有了對硬體的初步了解之後,再來設計 Y86-64 彙編代碼到機器碼的指令集。
Y86-64 的指令由 10 個位元組組成,第一個位元組代表指令類型,也就是
icode+ifun
的組合,第二個位元組
rA+rB
是源操作數與目标操作數的寄存器代碼,寄存器代碼就是一個數組的下标,整個寄存器組組成了寄存器檔案,相當于我們使用寄存器代碼去通路寄存器檔案得到寄存器檔案中對應下标的資料。不過也不是所有的指令都有這個位元組,例如
ret
jXX
兩個指令就不需要寄存器,對于剩下的 8 個位元組或者 9 個位元組,進入處理器處理的時候就成了
valC
,是以在順序處理器中更新 PC 的時候有些指令要再加上 10 。
設計 SEQ 處理器
實際上 SEQ 也分了很多的階段,這樣做的目的是用盡可能少的硬體去執行盡可能多的不同的指令。
SEQ 的硬體設計中包括了這些:
- 随機通路存儲,其中就包含了資料記憶體與指令記憶體,他們分屬不同的區域,但确實都在 RAM 中,甚至有些指令可以更改指令記憶體。
- PC 增加器,用于計算下一條指令的 PC
- 寄存器檔案,通過接受外部輸入的寄存器名稱和值來讀寫寄存器。
- ALU 也就是算法邏輯單元,用于計算數值,位址,計算狀态碼,跳轉狀态等等。
這些簡單的硬體就組成了我們的 SEQ 處理器,所有的指令都在一個時鐘周期中完成,并且資料流動是自底向上的,而資料回報寫入是自頂向下的。
為了讓指令更加統一,我們将它們分為了六個階段。
取指 Fetch
将程式計數器寄存器作為位址,指令記憶體讀取指令的位元組,PC 增加器計算 valP 也就是下一條指令的位址。
譯碼 Decode
寄存器檔案有兩個讀端口 A 和 B,從這兩個端口中同時讀取寄存器的值 valA 和 valB,傳入的是 srcA,srcB 這兩個讀取位址一般是來自從指令中解析出來的 rA 和 rB,但有的時候并不需要讀取兩個值,隻需要一個就可以,是以在這種情況下,另一個空閑的讀取端口就會被設定為 15,這也就是一開始設計寄存器的時候隻設計了 15 個但卻保留了 r15 而不使用的原因。
執行 Execute
在這一階段會根據指令的類型,将算數 / 邏輯單元 ALU 用于不同的目的,對于整數加減之類的操作,它會執行指令指定的運算,而對于其他的指令,ALU 作為加法器來增加或減少棧指針,計算有效的記憶體位址,或是不對操作數進行改變,僅僅對它加個 0(為了滿足統一的加法格式而且不改變操作數的值),将輸入傳遞到輸出。
同時也會在這一階段根據 ALU 計算得到的結果來設定條件碼寄存器的值,然後可以計算得到分支跳轉信号 Cnd(如果需要跳轉的話)。
訪存 Memory
通過上一階段得到的計算結果,或者是直接用指令中解析出來的 valC 位址,通路記憶體,讀出或者寫入一個記憶體字,指令記憶體和資料記憶體通路的是相同的位置,但是用于不同的目的。
寫回 Write Back
這一階段的寫回指定的是寫入寄存器檔案,這與之前的譯碼階段相對應,在譯碼階段隻可以進行讀取操作,而寫入寄存器的操作隻可以在這一階段執行。
寄存器檔案有兩個寫端口,其中端口 E 用來寫 ALU 計算出來的值,而端口 M 用來寫入從資料記憶體中讀取出的值。這兩個端口都是傳輸要寫入的資料!而不是要寫入的位址。
PC 更新 Program Counter
程式計數器的下一個值有多個可能,有可能是 PC 增加器(一般指令)計算得到的值,也可能是在目前指令中指定的那個值 valC(對應 jXX 跳轉指令),也可以是從記憶體中讀取出來的值 valM(對應 ret 指令)。
而對于考試而言,我看到的一些有關這一章的題目是考的指令分階段實作。
設計流水線處理器
在流水線化的系統中,待執行的任務被劃分成了若幹個獨立的階段,這些階段通常會允許多個任務同時執行,而不是需要等到一個任務完成了所有階段的任務才會開啟下一個任務(這樣的處理叫做串行處理,并行流水線處理要比串行處理快得多),不過在這樣的流水線系統中,任務難免要經過那 些并不需要的環節,例如上面的 OP 操作就根本沒有訪存階段,而它還是要有這個過程,并浪費這麼多的時間。
總的來說,流水線化的系統大大提高了系統的吞吐量,也就是機關時間内處理的指令的數量,不過帶來的弊端是會輕微增加任務的總體延遲(Latency)也就是處理單條指令的時間。
通過這兩張圖就能很友善的比較順序與流水線化的差別。
流水線的局限性
不一緻的劃分
我們的劃分往往是假設每個階段耗時都是一樣的,這樣才能充分利用給出的時鐘周期,但實際情況中并不那麼完美,就像有的指令根本沒有訪存階段一樣,不可能所有劃分出來的階段耗時都是一樣的,是以就會造成浪費,但是如果不按照最長耗時的階段來給定時鐘周期,那麼有的階段就會完不成,導緻流水線出錯。
流水線過深,收益下降
我們将計算劃分成 6 個階段,每個階段需要 50ps,再在每對階段之間插入流水線寄存器,我們就得到了新的六階段的流水線,這個系統的最小時鐘周期達到了 70ps,吞吐量為 14.29GIS,吞吐量也就是最小時鐘周期的倒數。
雖然我們把三階段的流水線提升為六階段的流水線,但是我們的吞吐量并沒有翻倍,是中間插入的寄存器,也就是流水線延遲過多,導緻延遲在整個時鐘周期的占比提高(20/70=0.286),最後的吞吐量沒有得到兩倍提升。
流水線階段及流程設計
這一階段的流水線設計增加了流水線寄存器(F,D,E,M,W),然後将 PC 的更新移動到了取指階段,也就是在開始的時候計算目前要執行的指令的位址。
回報與冒險
回報是一種依賴,是後執行的指令依賴先前執行指令的結果,因為代碼是人寫的,人的慣性思維往往是線性的,後來者依賴先行者是理所當然的事情,但由于流水線的劃分和流水線并發的特性,導緻後執行的指令在譯碼階段等讀取資料的時候往往前面的指令還沒有完成寫回操作去更改寄存器檔案,也就出現了冒險。
控制冒險
- 結果沒有被及時的回報給下一個操作。
- 流水線改變了系統的行為。
資料冒險
- 一條指令的結果作為另一條指令的操作數(一般是讀後寫資料相關)。
- 我們需要處理這類問題,目标是得到正确的結果,并最小化對流水線性能的影響。
冒險處理手段
- 添加氣泡 bubble
- 暫停 stalling
- 資料轉發 forward,增加旁路路徑來把後續指令需要的資料從前置指令中轉發出來,而不需要暫停等待前置指令更新寄存器。
- 轉發源:e_valE, m_valM, M_valE, W_valM, W_valE
- 轉發目的地:val_A, val_B
- 其中開頭大寫的是流水線寄存器中的值,小寫的是流水線階段中産生的信号。val_A, val_B 是 ALU 的操作數。
冒險具體類型
加載 / 使用冒險 Load/Use Hazard
檢測手段:在執行階段判斷目前執行的指令是不是 mrmovq 或者 popq 指令,以及指令要寫入的目标位址是不是譯碼階段給的源位址。
解決方法
- 将指令暫停在取指和譯碼階段
- 在執行階段的那條指令加入氣泡,等待資料加載完成。
分支預測錯誤 Mispredicted Branch
對于分支預測有很多的政策,例如永遠選擇 Always Taken,也有永不選擇 Never Taken 或是其他的更加複雜的政策,前者的預測正确率大概為 60%,後者為 40% 左右,Y86-64 中使用的是前者,但不管哪個政策都會往下執行兩條指令,因為隻有分支跳轉指令完成執行階段(Execute)才能算出 Cnd 的準确值,是以肯定有兩個新的指令已經加入流水線了,如果那兩條指令不是正确的指令,那麼就要取消他們的執行,不過根據我們設計的六階段流水線處理器,他們并不會改變寄存器和狀态,是以隻要單純地取消即可。
檢測手段
- 在執行階段檢測到未選擇該分支
- 在緊跟着的指令周期中,将處于執行和譯碼階段的指令用氣泡替換掉,氣泡指令實際上就是 nop 指令。
- 此處不會出現預測錯誤的副作用,是以不需要接着處理。
ret 指令
因為 ret 指令需要到達寫回階段才算結束,而在它之後執行的三條指令需要暫停,也就是插入氣泡,讓接下來的三條指令都暫停在取指階段,前面的指令不受影響,繼續正常執行。
解決手段
- 當 ret 經過的時候,接下來的指令都暫停在取指階段。
- 在後三條指令的譯碼階段插入氣泡。
- 當 ret 指令執行到寫回階段的時候釋放暫停。
冒險控制小結
組合情況的處理
在一個時鐘周期内多種不同的流水線冒險同時出現
- 組合 A
- 不選擇分支
- 位于分支中的 ret 指令
- 組合 B
- 指令從記憶體讀取到 % rsp
- 緊跟着 ret 指令
異常處理
我們需要遵循的異常處理原則是出現異常的指令的後續指令不能改變處理器的狀态,是以我們應該禁用對條件碼寄存器的修改以及資料記憶體的修改。