深入理解計算機系統 --- 處理器體系結構

本章目的:簡要介紹處理器硬件的設計,研究一個硬件系統執行某種ISA指令的方式

到目前爲止,我們看到的計算機系統只限於機器語言程序級
我們知道處理器必須執行一系列指令,每條指令執行某個簡單操作
指令被編碼爲一個或多個字節序列組成的二進制格式

一個處理器支持的指令和指令的字節級編碼稱爲它的指令集結構(ISA)
不同處理器家族,都有不同的ISA,一個程序編譯成一種機器上運行,就不能在另一種機器上運行

ISA在編譯器編寫者和處理器設計人員之間提供了一個概念抽象層
編譯器編寫者只需要知道允許哪些指令
而處理器設計者必須構建出執行這些指令的處理器

理解處理器如何工作能幫助理解整個計算機系統如何工作

在這裏插入圖片描述
在這裏插入圖片描述
Y86-64是一個指令體系結構(ISA),它是寫這本書的作者出的指令集
目的是爲了讓我們更加清晰的瞭解ISA
就像在讀編譯原理一樣,作者會教你做個編譯器是一樣的道理

4.處理器體系結構

4.1 Y86-64指令集體系結構
定義一個指令集體系結構,包括定義各種狀態單元、指令集和他們的編碼、一組編程規範和異常事件處理

4.1.1程序員可見的狀態
在這裏插入圖片描述
Y86-64程序中的每條指令都會讀取或修改處理器狀態的某些部分
這稱爲程序員可見狀態
這裏的”程序員“既可以是彙編代碼寫程序的人,也可以是產生機器代碼的編譯器

在處理器實現中,只要我們保證機器級程序能夠訪問程序員可見狀態
就不需要完全按照ISA暗示的方式來表示和組織這個處理器狀態

上圖,程序計數器(PC)存放當前正在執行指令的地址

內存從概念上來說就是一個很大的字節數組,保存着程序和數據

Y86-64程序用虛擬地址來引用內存位置
硬件和操作系統軟件聯合起來將虛擬地址翻譯成實際或物理地址
指明數據實際存在內存中哪個地方

狀態碼Stat,表明程序執行的總體狀態,它會指示是正常運行,還是出現了某種異常
例如一條指令試圖去讀非法的內存地址

4.1.2 Y86-64指令
ATT格式,第一個操作數是目標操作數,第二個操作數是源操作數
在這裏插入圖片描述
上圖給出了Y86-64 ISA 中各個指令的簡單描述
這個指令集就是我們處理器實現的目標

Y86-64指令集基本上是x86-64指令集的一個子集
它只包括8字節整數操作,尋址方式較少,操作也較少
左邊是指令的彙編碼錶示,右邊是字節編碼

下面是Y86-64指令的一些細節
在這裏插入圖片描述
在這裏插入圖片描述

4.1.3 指令編碼
在這裏插入圖片描述
每條指令需要1-10個字節不等,這取決於需要哪些字段

每條指令的第一個字節表面指令的類型,這個字節分爲兩個部分
每部分4位:高4位是代碼部分,低4位是功能部分

在這裏插入圖片描述
15個程序寄存器中每個都有一個對應的範圍在0到0xE之間的寄存器標識符(register ID)
編號跟x860-64中的相同

例如用16進制來表示指令: rmmovq %rsp,0x123456789abcd(%rdx) 的字節編碼
從圖4-2中可以看到,rmmovq 第一個字節爲40,源寄存器%rsp應該編碼放在RA字段
而基質寄存器%rdx應該編碼放在Rb字段,據圖4-4中的寄存器編號,得到寄存器指示符字節42,最後偏移量編碼放在8字節常數字中,0x123456789abcd前面填充上0變成8字節
將它們都連接起來得到的指令編碼爲: 40 42 cd ab 89 67 45 23 01 00

指令集一個重要性質就是字節編碼必須有唯一的解釋
任意一個字節序列要麼是一個唯一的指令序列的編碼
要麼就不是一個合法的字節序列

如果不知道一段代碼序列的起始位置,我們就不能準確的確定怎樣將序列劃分成單獨的指令

在這裏插入圖片描述
在這裏插入圖片描述

4.1.4 Y86-64 異常
在這裏插入圖片描述
對Y86-64來說,程序員可見狀態包括狀態碼Stat,它描述程序運行的總體狀態
對於Y86-64,當遇到這些異常的時候,就簡單地讓處理器停止執行指令

在更完整的設計中,處理器通常會調用一個異常處理程序(exception handler)
這個過程被指定用來處理遇到的某種類型的異常

4.1.5 Y86-64 程序
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
這個程序程序包括數據,指令
僞指令指明應該將代碼或數據放在什麼位置,以及如何對齊
這個程序詳細說明了棧的放置,數據初始化,程序初始化和程序結束等問題

“.”開頭的詞是彙編器僞指令,告訴彙編器調整地址,以便在那裏產生代碼或插入一些數據
僞指令 .pos 0 告訴彙編器應該從0地址處開始產生代碼,這個地址是所有Y86-64程序的起點
接下來的一條指令(第三行)初始化棧指針
結尾處聲明瞭標號 Stack,並用一個. Pos 僞指令指明地址200,因此棧會從這個地址開始
向低地址增長

我們必須保證棧不會增長得太大以至於覆蓋了代碼或其他程序數據

在這裏插入圖片描述
標號array表明了這個數組的起始,並在8字節邊界處對齊(用 .align 僞指令指定)
16-19行爲“main”過程,在過程中對那個4字數組調用了sun函數,然後停止

4.1.6 一些Y86-64 指令的詳情
大多數Y86-64 指令時以一種直接明瞭的方式修改程序狀態的
所以定義每條指令想要達到的結果並不困難
Pushq 指令會把棧指針減8,並且將一個寄存器值寫入內存中
因此當執行pushq %rsp 指令時,處理器行爲是不確定的
因爲入棧的寄存器會被同一條指令修改,通常有兩種不同的約定:
1.壓入%rsp的原始值
2.壓入減去8的%rsp的值

4.2 邏輯設計和硬件控制語言HCL
在硬件設計中,用電子電路來計算對位進行運算的函數
以及在各種存儲器單元中存儲位

大多數現代電路技術都是用信號線上的高電壓或低電壓來表示不同的位值

在當前技術中,邏輯1是用1.0伏特左右的高電壓表示,邏輯0使用0.0伏特左右的低電壓表示的
要實現一個數字系統需要三個主要的組成部分:
1.計算機對位進行操作的函數的組成邏輯
2.存儲位的存儲器單元
3.控制存儲器單元更新的時鐘信號

4.2.1 邏輯門
邏輯門是數字電路的基本計算單元,它們產生的輸出,等於它們輸入位值的某個布爾函數
在這裏插入圖片描述
後邊很多地方都用這個圖

C語言中運算符的邏輯門下面是對應的HCL表達式:
AND 用 && OR 用 || NOT 用 !

邏輯門總是活動的(active),一旦一門的輸入變化了,在很短的時間內,輸出就會相應地變化

4.2.2 組合電路和HCL布爾表達式
將很多的邏輯門組合成一個網,就能構建計算塊(computational block),稱爲組合電路
如何構建這些網有幾個限制:
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
圖4-11 多路複用器,根據輸入控制信號的值,從一組不同的數據信號選出一個:
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述

在這裏插入圖片描述
在這裏插入圖片描述
4.2.3 字級的組合電路和HCL整數表達式
通過將邏輯門組合成大的網,可以構造出能計算更加複雜函數的組合電路

我們的處理器設計將包含很多字,字的大小的範圍爲 4位到64位
代表 整數、地址、指令代碼和寄存器標識符

執行字級計算的組合電路根據輸入字的各個位,用邏輯門來計算出字的各個位

例如,測試兩個64位字A和字B是否相等,僅當A的每個位都和B的相應位相等時
輸出才爲1

在這裏插入圖片描述
在這裏插入圖片描述

4.2.5 存儲器和時鐘
組合電路從本質上講,不存儲任何信息,它只是簡單地響應輸入信號,產生等於輸入的某個函數的輸出

爲了產生 時序電路 ,也就是有狀態並且在這個狀態上進行計算的系統
我們必須引入按位存儲信息的設備

存儲設備都是由同一個時鐘控制的,時鐘是一個週期性信息,決定什麼時候要把新值加載到設備中,考慮兩類存儲器設備:
在這裏插入圖片描述
在硬件中,寄存器直接將它的輸入和輸出線連接到電路的其他部分
在機器級編程中,寄存器代表的是CPU中爲數不多的可尋址的字,這裏地址是寄存器ID
這些字通常都存在寄存器文件中
在這裏插入圖片描述
上圖說明了一個硬件寄存器以及它是如何工作的
大多數時候,寄存器都保持在穩定狀態(x表示),產生的輸出等於它的當前狀態

信號沿着前面的組合邏輯傳播,這時,產生了一個新的寄存器輸入(y表示),但只要時鐘是低電位的,寄存器的輸出就仍然保持不變
當時鍾變成高電位的時候,輸入信號就加載到寄存器中,成爲了下一個狀態y
直到下一個時鐘上升沿,這個狀態就一直是寄存器的新輸出
每當每個時鐘到達上升沿時,值纔會從寄存器的輸入傳送到輸出

在這裏插入圖片描述
寄存器文件有兩個讀端口(A和B),還有一個寫端口(W)
這樣一個多端口隨機訪問存儲器允許同時進行多個讀和寫操作
圖所示的寄存器文件中,電路可以讀兩個程序寄存器的值,同時更新第三個寄存器的狀態
每個端口都有一個地址輸入,表明選擇哪個程序寄存器
還有一個數據輸出或對應該程序寄存器的輸入值

雖然寄存器文件不是組合電路,因爲它有內部存儲,不過在我們的實現中,
從寄存器文件讀數據就好像它是一個以地址爲輸入、數據爲輸出的一個組合邏輯塊
例如,將srcA設爲3,就會讀出程序寄存器%rbx的值,然後這個值就會出現在輸出valA上

4.3 Y86-64指令的順序實現
首先,我們描述一個成爲SEQ(“sequential”順序的)的處理器
每個時鐘週期上,SEQ執行處理一條完整指令所需的所有步驟
不過,這需要一個很長的時鐘週期時間,因此時鐘週期頻率會低到不可接受
我們開發SEQ的目的就是提供實現最終目的的第一步,實現一個高效的、流水線化的處理器

4.3.1 將處理組織成階段
通常,處理一條指令包括很多操作,將他們組織成某個特殊的階段序列
即使指令動作差異很大,但所有的指令都遵循統一的序列
取指(fetch): 取指階段從內存讀取指令字節,地址爲程序計數器(PC)的值
從指令中抽取出指令指示符字節的兩個四位部分,稱爲icode(指令代碼),ifun(指令功能)
它可能取出一個寄存器指示符字節,指明一個或兩個寄存器操作數指示符Ra和Rb
它還可能取出一個四字節常數字valC,它按順序方式計算當前指令的下一條指令的地址valP
也就是說,valP等於PC的值加上已取出指令的長度

譯碼(decode): 譯碼階段從寄存器文件讀入最多兩個操作數,得到值valA 和或/valB
通常,它讀入指令Ra和Rb字段指明的寄存器,不過有些指令是讀寄存器 %rsp

執行(execute): 在執行階段,算術/邏輯單元(ALU)要麼執行指令指明的操作(根據ifun的值)
計算內存引用的有效地址,要麼增加或減少棧指針,得到我們成爲valE,在此,有可能設置條件碼,對一條條件傳送指令來說,這個階段會檢驗條件碼和傳送條件(由ifun給出),如果條件成立,則更新目標寄存器
同樣,對一條跳轉指令來說,這個階段會決定是不是應該選擇分支

訪存(memory): 訪存階段可以將數據寫入內存,或者從內存讀出數據,讀出的值爲valM

寫回(write back): 在寫回階段最多可以寫兩個結果到寄存器文件

更新PC(PC update): 將PC設置成下一條指令的地址

處理器無限循環,執行這些階段 ,在我們簡化的實現中,發生任何異常,處理器就會停止
在完整的設計中,處理器會進入異常模式,開始執行由異常的類型決定的特殊代碼

執行一條指令是需要進行很多處理的,不僅必須執行指令表明的操作,還必須計算地址、更新棧指針,以及確定下一條地址
在這裏插入圖片描述
在這裏插入圖片描述
整數操作指令的處理遵循上面列出的通用模式
在取指階段,不需要常數字,所以 valP 就計算爲 PC+2
在譯碼階段,需要讀兩個操作數
在執行階段,它們和功能指示符ifun一起再提供給ALU,這樣一來valE就成爲了指令結果
這個計算是用表達式valB OP valA 來表達的,這裏OP代表ifun指令的操作
在訪存階段,什麼也不做
在寫回階段,valE被寫入寄存器Rb,然後PC設爲valP,整個指令的執行就結束了

看一下subq指令執行過程
據圖4-17,第三行,指令位於0x014 地址,佔兩個字節,值爲 0x61 和 0x23
在這裏插入圖片描述
4.3.2 SEQ硬件結構
實現所有Y86-64指令所需要的計算可以被組織成6個基本階段:
取指,譯碼,執行、訪存、寫回、更新PC

在這裏插入圖片描述
從程序計數器PC(左下角),信息沿着線流動,先向上再向右
同各個階段相關的硬件單元(hardwareunits)負責執行這些處理
在右邊,反饋線路向下,包括要寫到寄存器文件的更新值,以及更新的程序計數器值

硬件單元與各個處理階段相關聯:
取指: 將程序計數器寄存器作爲地址,指令內存讀取指令的字節,PC增加器計算valP
即增加了的程序計數器

譯碼: 寄存器文件有兩個讀端口A和B,從這兩個端口同時讀寄存器值valA和valB

執行: 執行階段會根據指令的類型,將算術/邏輯單元(ALU)用於不同的目的
對整數操作,它要執行指令所指定的運算,對其他指令,它會作爲一個加法器來計算增加或減少棧指針,或者計算有效地址,或者只是簡單地加0,將一個輸入傳遞到輸出
條件碼寄存器(CC)有三個條件碼位,ALU負責計算條件碼的新值
當執行條件傳送指令時,根據條件碼和傳送條件來計算決定是否更新目標寄存器
同樣,當執行一條跳轉指令時,會根據條件碼和跳轉類型來計算分支信號Cnd

訪存: 在執行訪存操作時,數據內存讀出或寫入一個內存字
指令和數據內存訪問的是相同的內存地址,但是用於不同的目的

寫回: 寄存器文件有兩個寫端口,端口E用來寫ALU計算出來的值
而端口M用來寫從數據內存中讀出的值

PC更新: 程序計數器的新值選擇自:valP,下一條指令的地址
ValC,調用指令或跳轉指令指定的目標地址
ValM,從內存讀取的返回地址

在這裏插入圖片描述
在這裏插入圖片描述

4.3.3 SEQ的時序
SEQ的實現包括組合邏輯和兩種存儲器設備:
時鐘寄存器(程序計數器和條件碼寄存器)
隨機訪問存儲器(寄存器文件、指令內存和數據內存)

組合邏輯不需要任何時序或控制,只要輸入變化了,值就通過邏輯門網絡傳播

我們也將 讀隨機訪問存儲器 看成和組合邏輯一樣的操作,根據地址輸入產生輸出字
由於指令內存只用來讀指令,因此我們可以將這個單元看成是組合邏輯

還剩四個邏輯單元需要對它們的時序進行明確的控制
程序計數器、條件碼寄存器、數據內存和寄存器文件
這些單元通過一個時鐘信號來控制,它觸發將新值裝載到寄存器以及將值寫到隨機訪問存儲器,每個時鐘週期,程序計數器都會裝載新的指令地址
只有在執行整數運算指令時,纔會裝載條件碼寄存器
只有在執行rmmovq、pushq或call指令時,纔會寫數據內存

要控制處理器中活動的時序,只需要寄存器和內存的時序控制
原則:從不回讀
處理器從來不需要爲了完成一條指令的執行而去讀由該指令更新了的狀態

在這裏插入圖片描述
在這裏插入圖片描述
不同顏色的代碼表面電路信號是如何與正在被執行的不同指令相聯繫的

實在太燒腦!

4.3.4 SEQ階段的實現
我們沒有講那部分SEQ的HCL描述,是不同整數和布爾信號的定義,它們可以作爲HCL操作的參數

在這裏插入圖片描述

1.取指階段
在這裏插入圖片描述
取指階段包括指令內存硬件單元,以PC作爲第一個字節(字節0)的地址
這個單元一次從內存讀出10個字節
第一個字節被解釋成指令字節,標號爲“Split”的單元,分爲兩個4位的數
然後標號爲“icode”和“ifun”的控制邏輯塊計算指令的功能碼
或者使之等於從內存讀出的值,或者當前指令地址不合法(由信號imem_error指明)
根據icode的值,我們可以計算三個一位的信號:
Instr_valid: 這個字節對應於一個合法的Y86-64指令嗎?這個信號用來發現不合法的指令
Need_regids: 這個指令包括一個寄存器指示符字節嗎?
Need_valc: 這個指令包括一個常數字嗎?

信號instr_valid 和 imem_error 在訪存階段被用來產生狀態碼

2.譯碼和寫回階段
在這裏插入圖片描述
把這兩個階段聯繫在一起是因爲他們都要訪問寄存器文件
寄存器文件有四個端口,支持同時進行兩個讀(A,B)和兩個寫(E,M)
每個端口都有一個地址連接和一個數據連接
地址連接是一個寄存器ID,數據連接是一組64根線路,既可以作爲寄存器文件的輸出字(讀端口)也可以作爲它的輸入字(寫端口)

根據指令代碼icode以及寄存器指示值Ra和Rb,可能還會根據執行階段計算出的Cnd條件信號,圖底部的四個塊產生出四個不同的寄存器文件的寄存器ID
寄存器ID srcA表明應該讀哪個寄存器以及產生valA

3.執行階段
在這裏插入圖片描述
執行階段包括算術/邏輯單元(ALU)
這個單元根據alufun信號的設置,對輸入aluB和aluB執行
ADD,SUBTRACT,AND,EXCLUSVEOR運算

4.訪存階段
在這裏插入圖片描述
訪存階段的任務就是讀或者寫程序數據
如圖所示,兩個控制塊產生內存地址和內存輸入數據(寫操作)的值
另外兩個塊產生表明應該執行讀操作還是寫操作的控制信號
當執行寫操作時,數據內存產生值valM

5.更新PC階段
在這裏插入圖片描述
SEQ中最後一個階段會產生程序計數器的新值
新的PC可能是valC、valM、valP

在這裏插入圖片描述

4.4 流水線的通用原理
在流水線化的系統中,待執行的任務被劃分成了若干個獨立的階段

流水線化的一個重要的特性就是提高了系統的 吞吐量(throughput)
不過它也會輕微地增加 延遲(latency)

4.4.1 計算流水線
每個階段完成指令執行的一部分,
下圖給出了一個很簡單的非流水線化的硬件系統例子
它是由一些執行計算的邏輯以及一個保存計算結果的寄存器組成
時鐘信號控制在每個特定的時間間隔加載寄存器
圖中的計算塊使用組合邏輯實現的,信號會穿過一系列邏輯門,在一定時間延遲後輸出就成爲了輸入的某個函數
在這裏插入圖片描述
假設組合邏輯需要300ps,而加載寄存器需要20ps
圖中的流水線圖時間從左向右流動,從上到下一組操作(I1-3),實心長方形表示指令執行時間
在這個實現中,開始下一條指令之前必須完成前一個

在這裏插入圖片描述
在這裏插入圖片描述
在各個階段之間放上流水線寄存器(pipeline register),這樣每條指令都會按照三步經過這個系統,從頭到尾需要三個完整的時間週期

上圖的流水線所示,只要I1從A進入B,就可以讓I2進入階段A了,依此類推
在穩定狀況下,三個階段都應該是活動的,每個始終週期,一條指令離開系統,一條新的進入

這樣,我們將系統吞吐量提高了兩倍多
代價是增加了一些硬件,以及延遲的少量增加,延遲變大是由於增加的流水線寄存器的時間開銷

4.5 流水線的實現
首先,對順序的SEQ處理器做點小改動,將PC計算挪到取指階段,然後再各個階段之間加上流水線寄存器

4.5.1 SEQ+: 重新安排計算階段
稍微調整一下SEQ中五個階段的順序,使得更新PC階段在一個時鐘週期開始時執行,而不是結束時執行
在這裏插入圖片描述
在這裏插入圖片描述

4.5.2 插入流水線寄存器
要在SEQ+的各個階段之間插入流水線寄存器,並對信號重新排列
得到PIPE- 處理器,PIPE-抽象結構 (圖4-40)
流水線寄存器在圖中用黑色方框表示,每個寄存器包括不同的字段,用白色方框表示
在這裏插入圖片描述
可以看到 PIPE-使用了與順序設計SEQ幾乎一樣的硬件單元,但是有流水線寄存器分隔開這些階段
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述

4.5.3 對信號進行重新排列和標號

順序是先SEQ和SEQ+在一個時刻只處理一條指令,因此諸如valC,srcA,valE這樣的信號值有唯一的值
在流水線化的設計中,與各個指令相關的這些值由多個版本,會隨着指令一起流過系統

採用命名機制,存儲在流水線寄存器中的信號可以唯一地被標識

4.5.4 預測下一個PC
流水線化設計的謎底就是每個時鐘週期都發射一條新指令,也就是說每個時鐘週期都有一條指令進入執行階段並最終完成

要達到這個目的也就意味着吞吐量是每個時鐘週期一條指令

要做到這一點必須在取出當前指令之後,馬上確定下一條指令的位置

不幸的是,如果取出的指令是條件分支指令,要到幾個週期後,也就是指令通過執行階段之後,才能知道是否要選擇分支,如果是ret,要到指令通過訪存階段,才能確定返回地址

因此,通過預測PC的下一個值,在大多數情況下,我們能達到每個時鐘週期發射一條新指令的目的

4.5.5 流水線冒險
PIPE-結構是創建一個流水線化的Y86-64處理器的好開端
當相鄰指令存在相關聯會導致出現問題
1.數據相關,下一條指令會用到這一條指令計算出的結果
2.控制相關,一條指令要確定下一條指令的位置

這些可能會導致流水線產生計算錯誤,稱爲:毛線
冒險也可以分爲兩類: 數據冒險,控制冒險
在這裏插入圖片描述
1.用暫停來避免數據冒險
暫停時避免毛線的一種常用數據,暫停時,處理器會停止流水線中一條或多條指令
知道冒險條件不再滿足
讓上一條指令停頓在譯碼階段,知道產生它的源操作數的指令通過了寫回階段
這樣就能避免數據冒險

2.用轉發來避免數據冒險
PIPE- 的設計是在譯碼階段從寄存器文件中讀入源操作數,但是對些源寄存器的寫有可能要在寫回階段才能進行
與其暫停直到寫完成,不如簡單地將要寫的值傳到流水線寄存器E作爲源操作數
這種將結果直接從一個流水線階段傳到較早階段的的技術稱爲:數據轉發

3.加載/使用數據冒險

4.避免控制冒險

4.5.6 異常處理
處理器中很多事情會導致異常控制流,此時,程序執行的正常流程被破壞掉
異常可以由程序執行從內部產生,也可以由某個外部信號從外部產生

我們的指令集體系結構包括三種不同的內部產生的異常:
1.halt指令
2.有非法指令和功能碼組合的指令
3.取指或數據讀寫試圖訪問一個非法地址

我們把導致異常的指令稱爲異常指令(excepting instruction)

一般地,通過在流水線結構中加入異常處理邏輯,我們既能夠從各個異常中做出正確的選擇,也能夠避免出現由分支預測錯誤取出的指令造成異常

PIPE- 各階段的實現

小結

在這裏插入圖片描述
在這裏插入圖片描述

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