五月天青色头像情侣网名,国产亚洲av片在线观看18女人,黑人巨茎大战俄罗斯美女,扒下她的小内裤打屁股

歡迎光臨散文網(wǎng) 會員登陸 & 注冊

【轉】 從零開始制作自己的指令集架構

2023-07-01 15:25 作者:Bili_394329148  | 我要投稿

?從零開始制作自己的指令集架構

王金戈

微軟中國 軟件研發(fā)

本文,我們要做一件大膽的事情,從零開始實現(xiàn)一個全新的指令集架構,以此深入理解處理器的工作原理。

指令集發(fā)展歷史概況

開始我們的創(chuàng)造之旅前,先了解一下歷史上的指令集架構都有哪些。

一個處理器支持的指令和指令的字節(jié)級編碼稱為它的指令集架構(Instruction Set Architecture, ISA)。

最為我們熟知的就是x86架構,因為我們日常所用的個人電腦就采用了x86架構的處理器。目前世界上最大的兩個處理器制造商Intel和AMD都有基于x86架構的一系列產(chǎn)品。從Intel i386處理器開始,x86架構進入32位時代,稱為IA32架構(Intel Architecture 32bit)。后來,32位也不能滿足我們的需求了,Intel開始進軍64位處理器領域,提出IA64架構。但是,這個架構并不是我們現(xiàn)在在用的64位處理器,而是一個與x86完全無關的新的處理器架構,不保持向后兼容。雖然可以實現(xiàn)很高的性能,但是由于兼容性不好,市場反應冷淡。于此同時,AMD公司抓住機會,率先提出了x86-64處理器架構,支持64位的同時保持向后兼容,一舉在與Intel的市場競爭中占據(jù)了主動權。當然,Intel也不會執(zhí)迷不悟,他們果斷放棄了IA64,開始轉向x86-64架構,并逐步收回喪失的市場份額。后來,雖然AMD將自己的架構命名為AMD64,Intel將自己的架構命名為Intel64,但人們仍然習慣性地將它們統(tǒng)稱為x86-64。

Y86指令集

為了致敬偉大的x86指令集架構,我們將自己的指令集架構命名為Y86。其實呢,Y86的設計理念完全借鑒x86,相當于一個簡化的x86架構。

要想從頭設計一個指令集架構,需要先規(guī)定指令集和指令集編碼,然后將每個指令劃分為幾個階段分步執(zhí)行,每個階段只需要做簡單的一兩項工作,之后,將硬件設備結合適當?shù)倪壿嬰娐穼崿F(xiàn)指令每個階段的工作。下面我們詳細講解具體的實現(xiàn)過程。

指令集及其編碼

對于一個簡易的指令集來說,不需要太多的指令,能實現(xiàn)基本的數(shù)據(jù)轉移和流程控制就夠了。下圖列出了Y86指令集中包含的所有指令,以及每個指令的編碼。

這些都是非?;镜闹噶?,不過看起來有些奇怪,這是因為我們把x86中的movl指令替換成了四個獨立的指令rrmovlirmovl、rmmovlmrmovl,每個指令指明了操作數(shù)的來源,這樣就避免了各種尋址方式的麻煩。

可以看到,各個指令的長度從1字節(jié)到6字節(jié)不等,這樣編碼可以減少程序代碼占用的空間。第1個字節(jié)的高4位作為指令編碼,用來區(qū)分不同的指令,低4位要么是0,要么是fn。fn稱為功能代碼,用來區(qū)分不同的操作。如下圖所示,不同的功能碼在不同的指令中有不同的含義。在運算指令中,分別代表加、減、與和異或;在分支跳轉指令中,分別代表不同的跳轉條件;在條件轉移指令中,分別代表不同的轉移條件。

第2個字節(jié),對于大部分指令來說存放的是寄存器標識符,請看下圖:

每個寄存器與一個數(shù)字一一對應,F(xiàn)代表無寄存器操作數(shù)。

最后,有些指令還包含四個字節(jié)的立即數(shù)。

舉一個例子來幫助我們更好地理解指令編碼。例如對于如下指令

rmmovl %esp, 0x12345(%edx)

對應的編碼為

40 42 45 23 01 00

其中,從左到右,40是指令編碼,42分別是寄存器%esp對應的4和寄存器%edx對應的2,45230100是偏移量0x12345在小端機器上的表示。

硬件控制語言HCL

處理器的各個硬件設備(比如ALU、程序計數(shù)器)之間通常需要特定功能的邏輯電路來連接,在設計階段,我們使用一種結構化的語言來描述這些邏輯關系。

HCL(Hardware Control Language)是一種類似C的硬件控制語言,用于描述處理器的控制邏輯。

舉一個簡單的例子,對于如下所示的組合邏輯電路:

可以用HCL語言表示為

e = (a && !(b||c)) || (!d && !(b||c))

這句話描述了輸出和輸入的邏輯關系,無論多么復雜的組合電路,都可以用最基本的與或非門來實現(xiàn)。HCL在后面將會有大量的應用。

存儲器和時鐘

細心的讀者可能會注意到,上一段話講到“無論多么復雜的組合電路”。為什么特別強調組合電路呢,因為還有另一種電路——時序電路。

大家應該都有基本的電路知識,組合電路只是完成了一個函數(shù)的功能,不同的輸入導致不同的輸出,電路本身并不存儲任何信息。而時序電路就不一樣了,它可以存儲信息,而且在時鐘信號的控制下對輸入做出反應。

接下來,重點來了。在處理器中有兩種存儲設備:

  • 時鐘寄存器(簡稱寄存器) 存儲單個位或字。時鐘信號控制寄存器加載輸入值。

  • 隨機訪問存儲器(簡稱存儲器) 存儲多個字,由地址選擇讀寫哪個字。這里所說的存儲器可以分為兩種:處理器的虛擬存儲器系統(tǒng)和寄存器文件。前者是通常意義上的內存系統(tǒng),后者才是我們指令集中8個寄存器標識符對應的通用寄存器。

下圖為寄存器的工作原理,寄存器輸出一直保持在當前狀態(tài),直到時鐘上升沿,新的輸入將成為當前的寄存器狀態(tài)。

寄存器文件可以看成這樣一個功能塊:

它有兩個讀端口和一個寫端口,支持讀寫同時操作。值得注意的是,寄存器文件的讀操作是即時的,而寫操作是基于時鐘的。也就是說,讀出的值valA和valB隨時根據(jù)srcA和srcB的變化而變化,而要寫入的值valW只在clock的上升沿才能寫入。仔細想想,寄存器文件的讀寫特性好像和寄存器是完全一樣的,只不過是多了一個選址操作。

指令的分階段執(zhí)行

雖然宏觀上來看,指令已經(jīng)是程序不可分割的基本元素。但在處理器中,一條指令的執(zhí)行還是要分多個階段,這樣才可以提高硬件的處理效率。在Y86架構中,我們將每個指令的執(zhí)行分為6個階段。

  1. 取指:從PC中取出當前要執(zhí)行的指令,并按照指令編碼對其分解,得到icode、ifun、rA、rB、valC等值。

  2. 譯碼:根據(jù)rA、rB取出對應寄存器的值valA、valB。

  3. 執(zhí)行:ALU在不同指令下執(zhí)行不同的操作,包括簡單運算、地址加減等等,運算結果為valE,運算時會對條件碼產(chǎn)生影響。

  4. 訪存:從存儲器讀取數(shù)據(jù)或向存儲器寫入數(shù)據(jù)。讀出的值為valM。

  5. 寫回:將前面生成的結果寫回寄存器文件。

  6. 更新PC:將PC設置成下一條指令的地址。

這些步驟現(xiàn)在看起來雜亂無章,不知有何用處。但仔細分析,可以看到,每個階段只做與一兩個硬件相關的事情,由輸入決定輸出,完全可以在一個時鐘周期內做完。而各個階段之間的聯(lián)系就是各種信號的輸入和輸出,比如,譯碼階段的輸出valA可以作為執(zhí)行階段的輸入,而執(zhí)行階段的輸出又可以作為寫回階段的輸入,這樣就可以用簡單的組合電路把這些硬件單元連接起來,實現(xiàn)我們需要的功能。

為了大家更清楚地理解各個階段的作用,我們用一個例子來詳細說明。

上圖分別為OPl rA, rB、rrmovl rA, rBirmovl V, rB這三個指令的分階段執(zhí)行過程。在取指階段中,M表示存儲器,M1[PC]表示以PC為基址從存儲器中取出1字節(jié)數(shù)據(jù)。由于各個指令長短不一,因此取指階段做的事情也不盡相同。在該階段最后,會計算出PC的新值valP。譯碼階段是從寄存器文件中取出寄存器的值,用R[rA]來表示寄存器rA的值。執(zhí)行階段對于OPl指令來說會設置狀態(tài)碼CC,而后兩個指令則不會對狀態(tài)碼產(chǎn)生影響。訪存階段在這三個指令中都沒有涉及。最后的更新PC階段將valP的值賦值給PC。

當我讀到這里的時候,我有很大的疑問:不是說每個階段只做一件簡單的事情嗎,但是不同的指令在同一個階段做的事情似乎各不相同。比如剛才的三個指令,在執(zhí)行階段只有OPl指令會設置狀態(tài)碼,而另外兩個不會,這是為什么?包括書中后面舉的其它例子,更新PC階段并不一定是把valP的值賦值給PC,有些指令比如call和ret,它們會將valC的值或valM的值賦值給PC,這又是怎么做到的?

大家是否也想到了這些問題呢?很顯然,每個階段對不同的指令有不同的響應是很自然的事情,不然怎么適應各個指令的不同功能呢。我們前面提到的HCL硬件控制語言,就是要完成這個任務,控制每個指令在每個階段要完成的任務。

好了,在詳細說明如何用HCL控制邏輯之前,先給出完整的硬件結構圖。

我們要注意圖中不同顏色的方塊和不同粗細的線條,它們代表著不同的意思。綠色塊代表基本的硬件單元,比如ALU、寄存器文件、PC,基本上我們都已經(jīng)接觸過?;疑綁K將是我們下一步研究的重點,它們是HCL描述的組合邏輯電路,用于連接綠色塊并實現(xiàn)特定的選擇或邏輯運算。白色圓圈并沒有特殊的含義,只是用來標識信號線的名稱。圖中還有三種線條,粗實線表示寬度為字長的信號線,細實線表示寬度為1個字節(jié)或更窄的信號線,而虛線表示單個位的信號線。

圖中從下到上分別是剛才介紹的取指、譯碼(寫回)、執(zhí)行、訪存和更新PC階段。由于譯碼和寫回階段都是對寄存器文件的操作,因此它們在圖中畫在了同一個位置。用圓圈標出的信號就是前文提到的各個階段產(chǎn)生的中間值,這些值通常在不同指令中擔任著不同的角色,因此會出現(xiàn)一個信號分叉為兩個信號的情況。例如圖中valA產(chǎn)生之后分為兩條線,一條通向ALUB控制邏輯,另一條通向Data控制邏輯。再例如圖中valM產(chǎn)生之后分為兩條線,一條通向New PC控制邏輯,另一條通向寄存器文件的輸入端。我們需要明白的是,一個信號分為兩個信號,意味著兩個接收端都可以讀取到該信號的值,但讀取到該值并不意味著使用該值,接收端的控制邏輯決定是否使用該值,下文將會詳細敘述。

SEQ的狀態(tài)改變周期

上一張圖的標題我沒做解釋,其實是留了個疑問。SEQ的意思是Sequential(順序的),“SEQ硬件結構”就是說“順序的硬件結構”或者“硬件結構的順序實現(xiàn)”。什么??!難道還有其它方式的實現(xiàn)?答案是當然的,我們留到后面再揭開謎底。SEQ的硬件結構使得指令必須按順序一個接一個地執(zhí)行,下一條指令的開始必須晚于上一條指令的結束。這就導致處理器效率極其低下,因為一個指令必須在一個時鐘周期內通過所有階段,而由于電路延遲的固有因素,通過所有階段需要的時間存在下限,也就使得時鐘周期不能無限縮短。然而,為什么一個指令必須在一個時鐘周期內通過所有階段呢?

因為對于時序邏輯電路,比如SEQ中的存儲器、寄存器文件、CC和程序計數(shù)器,它們只在時鐘信號的上升沿寫入數(shù)據(jù)。當前個指令結束,下個指令開始的時候,時鐘信號上升沿觸發(fā)這幾個硬件單元的更新。如果在下一個時鐘周期上升沿到來之前,需要更新的新值還沒有產(chǎn)生,這個指令就相當于沒執(zhí)行或執(zhí)行了一半。因此時鐘周期不能降得太低,否則將造成指令執(zhí)行紊亂。

下圖展示了兩個指令周期的過程中,由時鐘控制的各個硬件單元的狀態(tài)改變。

可以看到,圖中將四個時序邏輯電路之外的其它部分作為一個組合邏輯電路的整體來看待。當周期3開始時,組合邏輯電路開始運行,直到周期3結束前,所有結果都已得出,準備寫入存儲器等設備。當周期4開始時,存儲器、寄存器文件、CC和程序計數(shù)器的值被更新,同時,這些新值被組合邏輯電路讀取并開始計算結果,如此循環(huán)往復。因此,每個時鐘周期SEQ的狀態(tài)改變一次。

SEQ的各階段實現(xiàn)

前文給出的SEQ硬件結構圖只是一個大概的實現(xiàn),有些細節(jié)并沒有給出?,F(xiàn)在,我們一個階段一個階段地分析SEQ的具體實現(xiàn)。

取指階段

指令從內存中取出后按字節(jié)分為了兩部分:Split和Align。Split又分為icode和ifun。Align分為rA、rB和valC,這些都很容易理解。重點在于PC增加的邏輯。PC增加多少要根據(jù)本條指令的長短來決定,而本條指令的長短又在于指令中是否包含寄存器標識,以及是否包含常數(shù)valC,圖中的兩個組合電路Need valC和Need rigids就是用來做這個判斷。

以Need rigids為例,它的HCL語言描述如下:

bool need_rigids = ? ?icode in { IRRMOVL, IOPL, IPUSHL, IPOPL, IIRMOVL, IRMMOVL, IMRMOVL };

意即,當icode等于括號中7種指令碼之一時,need_rigids為真。也就是說這7種指令中包含寄存器標識。同理,need_valC也可以用這個枚舉的方法確定,只需要查前面的指令集編碼表,找到包含valC的指令,放在括號里面就行了。

當need_rigids和need_valC都確定了之后,PC increment將按如下公式計算新的PC值,其實就是加上了該條指令的長度:

newPC = oldPC + 1 + need_rigids + 4*need_valC

現(xiàn)在我們明白了,灰色方框代表的組合電路可以用HCL語言來描述。而實際電路中這些HCL語句將通過綜合成為真正的組合邏輯電路。在這里,HCL是一種很好的抽象,將原理與具體的實現(xiàn)相分離,方便我們的設計。

譯碼和寫回階段

這兩個階段都與寄存器文件的讀寫相關。從取指階段得到的信號icode、rA和rB在這里作為輸入信號,經(jīng)過一些組合電路生成寄存器文件的輸入。我們的目的是,在譯碼階段,對于那些需要使用特定寄存器的命令,從寄存器文件中取出這些寄存器的值,地址由srcA和srcB來決定,結果輸出為valA和valB;在寫回階段,將執(zhí)行階段的結果valE或訪存階段的結果valM寫回特定的寄存器,寄存器的地址由dstE和dstM來決定。以組合電路srcA為例,它的HCL表述為:

int srcA = [ ? ?icode in { IRRMOVL, IRMMOVL, IOPL, IPUSHL } : rA; ? ?icode in { IPOPL, IRET } : RESP; ? ?1 : RNONE; ? ?#Don't need register];

方括號類似C語言中的switch語句,當?shù)谝粋€分號前的條件滿足時返回rA,后面的兩個條件不再考慮;否則再判斷第二個條件是否滿足,滿足則返回RESP;否則返回RNONE,表示不需要讀取寄存器文件。從中可以看出,在譯碼階段,當指令為第一個分號前的四種時,將讀取rA寄存器的值并放入結果valA;當指令為第二個分號前的兩種時,將讀取RESP寄存器的值并放入結果valA;否則,不必讀取任何寄存器。

與srcA類似的還有srcB、dstE和dstM三個組合邏輯電路,它們的HCL表述可以從SEQ的硬件結構和指令集編碼中分析得出,不再一一敘述。

執(zhí)行階段

ALU需要兩個操作數(shù)和一個alufun信號,alufun信號用于指明ALU對兩個操作符執(zhí)行怎樣的邏輯運算(加、減、與、異或)。

以第一個操作數(shù)aluA為例,它的HCL描述如下:

int aluA = [ ? ?icode in { IRRMOVL, IOPL } : valA; ? ?icode in { IIRMOVL, IRMMOVL, IMRMOVL } : valC; ? ?icode in { ICALL, IPUSH } : -4; ? ?icode in { IRET, IPOPL } : 4; ? ?# Other instructions don't need ALU ];

可以看出,操作數(shù)aluA有時取valA,有時取valC,有時取-4或4,完全決定于指令類型。

alufun信號的HCL描述如下:

int alufun = [ ? ?icode == IOPL : ifun; ? ?1 : ALUADD; ];

僅當指令為IOPL指令(即運算指令)時,alufun由ifun決定,其它情況下ALU全部當做加法器來使用。這也就不難理解為什么剛才aluA會取-4或4,因此此時aluA作為加法器的一個加數(shù),而另一個加數(shù)從圖中可以看到只能來自于valB,雖然valB在譯碼階段的HCL我們并沒有給出,不過可以告訴大家valB在這四種情況下的輸出都是RESP。因此對于ICALL和IPUSH來說是為了讓棧指針esp-4,對于IRET和IPOPL來說是為了讓棧指針esp+4。

訪存階段

Mem read和Mem write決定當前指令對存儲器是讀操作還是寫操作。Mem addr和Mem data決定讀寫操作的地址和數(shù)據(jù)。以Mem addr為例,HCL描述如下:

int mem_addr = [ ? ?icode in { IRMMOVL, IPUSHL, ICALL, IMRMOVL } : valE; ? ?icode in { IPOPL, IRET } : valA; ? ?# Other instructions don't need address ];

更新PC階段

新的PC值來源可以從valC、valM和valP中選擇,New PC的HCL描述如下:

int new_pc = [ ? ?# Call. Use instruction constant ? ?icode == ICALL : valC; ? ?# Taken branch. Use instruction constant ? ?icode == IJXX && Cnd : valC; ? ?# Completion of RET instruction. Use value from stack ? ?icode == IRET : valM; ? ?# Default. Use incremented PC ? ?1 : valP; ];

流水線的一般原則

到此為止,我們的前奏剛剛落幕,終于要步入正題了。(這個前奏的確有點長,哈哈。)

在“SEQ的狀態(tài)改變周期”中埋下了一個伏筆,現(xiàn)在我們來揭開謎底。由于SEQ的時鐘頻率太低,我們需要想些辦法來提高時鐘頻率。通??梢韵氲絻煞N途徑,一是縮短每條指令的執(zhí)行時間,二是讓多條指令同時執(zhí)行。方法一不可行,因為每條指令的執(zhí)行時間很難壓縮,這是由電路的固有性質決定的。因此只能采用方法二,即流水線技術。

先來用一個形象的比喻來形容流水線技術。有一種帶傳送帶的自助餐廳,食物擺在傳送帶上經(jīng)過顧客,顧客可以隨意取走自己喜歡的食品。如果我們把一盤食物當做一條指令,而傳送帶兩旁的顧客當做指令執(zhí)行的各個階段,那么SEQ的實現(xiàn)就相當于每次只往傳送帶上放一盤食物,當這盤食物走到傳送帶盡頭后再放下一盤食物,如果餐館真這么做的話顧客恐怕都要餓死了。實際情況是,食物一盤接一盤地放在傳送帶上,每個顧客送走這一盤食物馬上迎來下一盤食物,效率大大提高。

處理器架構的流水線技術也是這樣,每個階段都有一條指令正在執(zhí)行,6個階段就會有6條指令同時執(zhí)行,將吞吐量提高為SEQ時的6倍。這樣是不是感覺非常給力呢,不過,事情遠沒有想象中那么簡單,最直接的問題是多個指令間會不會相互干擾?

我們回顧一下SEQ的硬件結構圖,不同階段間經(jīng)常有跨階段的連線,比如取指階段得到的valC直接連接到了更新PC階段的New PC。這在流水線情況下會出問題,因為后面的指令會覆蓋前面指令產(chǎn)生的valC,因此,當先前的指令到達更新PC階段再回頭取valC的值時,已經(jīng)不是當初自己在譯碼階段生成的值了。怎么辦呢?

解決方案也很容易想到,把每條指令后面有可能用到的值都保存下來不就行了。相當于每個階段多加一套寄存器,在階段開始時將這些寄存器的值更新為當前指令配套的值。在流水線技術中,這些插入到各個階段間的寄存器稱為流水線寄存器。

現(xiàn)在我們的處理器架構更新為PIPE-(Pipeline-,減號表示非最終版本),如下圖所示。

與SEQ相比有兩處變化,一是將更新PC階段和取指階段放在了一起,在取指之前更新PC;二是每兩個階段間插入了流水線寄存器。這些流水線寄存器是基于時鐘更新的,每個時鐘周期的開始將會更新這些寄存器中的數(shù)據(jù),相當于把當前指令的狀態(tài)傳遞到了下一個階段。

流水線冒險

現(xiàn)在大功告成了嗎?還沒有。當我們仔細分析PIPE-的時候我們會發(fā)現(xiàn)仍然存在一些問題。雖然流水線寄存器隔離了各個指令之間的數(shù)據(jù)共享,但是多個指令之間仍然存在依賴,包括兩個方面:

數(shù)據(jù)依賴:前一條指令寫入的寄存器或存儲器正好是后一條指令需要讀取的寄存器或存儲器。在PIPE-中,當后一條指令在譯碼階段讀寄存器的時候,前一條指令才剛剛到執(zhí)行階段,因此新值還沒有寫入寄存器,如果此時后一條指令直接讀寄存器的話,讀到的是舊值,這就違反了代碼順序執(zhí)行的規(guī)則。

控制依賴:當一條指令是jump、call或return時,下一條指令的地址是無法提前確定的,它依賴于當前指令的執(zhí)行結果。因此流水線很可能需要中斷。

這些依賴可能導致流水線產(chǎn)生計算錯誤,這種現(xiàn)象稱為流水線冒險。我們先來考慮數(shù)據(jù)冒險。下圖畫出了一段代碼的分階段執(zhí)行過程。

irmovl $3, %eaxaddl %edx, %eax之間插入了三個空指令。這樣的話,前者執(zhí)行完寫回階段,后者才開始執(zhí)行譯碼階段,保證了讀取寄存器前已經(jīng)寫入完畢。不發(fā)生數(shù)據(jù)冒險。

再看下圖。

現(xiàn)在去掉了一個空指令,情況立馬惡化。指令0x006的寫回階段和指令0x00e的譯碼階段同時發(fā)生,但由于寫回寄存器的操作直到第7周期的開始才會生效,因此譯碼階段讀出的值仍然是舊值,出現(xiàn)數(shù)據(jù)冒險現(xiàn)象。

如果把剩下的兩個空指令也去掉,結果可想而知,肯定會發(fā)生更嚴重的數(shù)據(jù)冒險,我們在此不再驗證。接下來考慮如何避免數(shù)據(jù)冒險。

仍然有兩種解決方案:

暫停:與插入nop空指令類似,處理器自動向可能發(fā)生數(shù)據(jù)冒險的代碼間插入bubble,使當前正在執(zhí)行的指令暫停一個時鐘周期。

如上圖所示,當addl指令執(zhí)行到譯碼階段時,檢測到將會發(fā)生數(shù)據(jù)冒險,于是插入一個bubble,addl指令在譯碼階段重復一個時鐘周期。

如果把所有nop都去掉,仍然可以用插入bubble的方法解決數(shù)據(jù)冒險,只不過需要插入多個bubble而已,如下圖所示。

轉發(fā):暫停有一點很不好,它會降低程序執(zhí)行效率,因為加入了很多無用的指令,純粹在浪費時間。而轉發(fā)可以更充分地利用每一個周期的時間。

仍然以剛才的代碼段為例講解轉發(fā)如何起作用。

如圖,當addl到譯碼階段的時候,irmovl到寫回階段,由于還沒有寫入寄存器,因此讀取數(shù)據(jù)時發(fā)生數(shù)據(jù)冒險。不過,我們可以用一個巧妙的方法避免這個冒險。既然寫回階段需要等到下個周期開始才能寫入寄存器,那不如直接把要寫入的值轉發(fā)給譯碼階段,這樣的話譯碼階段也不需要再從寄存器讀了,直接拿轉發(fā)來的值用就行了。

接下來,如果是prog3代碼段呢?

prog3和prog2的區(qū)別在于少了一個nop指令,這就導致當addl到譯碼階段的時候irmovl指令才到訪存階段。不過似乎對轉發(fā)并沒有影響,因為irmovl指令并不操作內存,在下一個階段將要寫入寄存器的值現(xiàn)在已經(jīng)產(chǎn)生了,就是M_valE(需要注解一下,M_valE的意思是M階段的流水線寄存器中保存的valE的值,請查看前面的PIPE-硬件結構圖),所以直接把M_valE轉發(fā)給譯碼階段就行了。

再接下來,如果是prog4代碼段呢?

現(xiàn)在,一個nop指令都沒有了,irmovl后面緊跟著addl,當addl到譯碼階段的時候irmovl才到執(zhí)行階段。可是令人驚訝的是,仍然可以轉發(fā)。首先,我們可以發(fā)現(xiàn)最后需要的寄存器的值就是在執(zhí)行階段經(jīng)過計算得出的。其次,我們要考慮到執(zhí)行階段得出結果需要一定時間,這個時間會不會導致不能按時轉發(fā)到譯碼階段呢?答案是否定的。因為譯碼階段即使很早拿到這個值,也會等到下一個周期開始才把它寫入執(zhí)行階段的流水線寄存器。因此只要在下個周期開始之前計算出這個值就可以了,而這個條件是永遠都能得到滿足的。

有沒有感覺到很神奇呢?竟然可以用轉發(fā)在不降低程序效率的條件下解決數(shù)據(jù)冒險問題,簡直太棒了??墒侨魏问虑槎疾皇峭昝赖模瑒偛诺睦又皇莍rmovl后面跟addl且兩者使用同一個寄存器,而實際程序有非常多種可能的組合,是不是轉發(fā)可以解決所有的問題?我們看下面這個例子。

prog5代碼段的0x018和0x01e兩行代碼稱為加載/使用數(shù)據(jù)冒險,mrmovl將數(shù)據(jù)從存儲器加載到寄存器%eax,然后緊接著addl使用寄存器%eax的值。仍然用轉發(fā),將mrmovl執(zhí)行階段的值轉發(fā)給addl,卻得到了錯誤的結果。其實原因很容易想到,因為mrmovl指令需要到訪存階段才能獲取到正確的值并賦值給%eax,因此再從執(zhí)行階段轉發(fā)到譯碼階段已經(jīng)完全不可行了。如何解決這個問題呢?我們可以把暫停和轉發(fā)兩種方式結合起來,先暫停一個周期,然后mrmovl到了訪存階段就可以把值正確地轉發(fā)給addl了。

好了,解決了這么多問題,終于可以給出我們的最終版硬件結構圖了。

比PIPE-增加的內容就是為了解決數(shù)據(jù)冒險問題而增加的轉發(fā)電路,轉發(fā)的接收方基本都在譯碼階段。

更完善的設計

任何事情都講究完美,我們現(xiàn)在得到的PIPE其實還不夠完美,有些關鍵細節(jié)沒有考慮到。

異常處理:處理器非常重要的一個方面就是異常處理。很多指令執(zhí)行過程中都可能發(fā)生各種各樣的異常,比如訪問存儲器時無效的地址、無效的指令的編碼等等。當程序發(fā)生異常時,應該立即中止程序,從外面來看的效果應該是正好停在異常發(fā)生的位置:即前面的代碼已經(jīng)完全執(zhí)行,而后面的代碼完全沒有執(zhí)行??雌饋砗芎唵蔚氖虑樵赑IPE中并不那么容易實現(xiàn),因為流水線中有多個指令同時執(zhí)行,如果某個指令在某個階段發(fā)生了異常,此時很可能后面的代碼已經(jīng)執(zhí)行了一部分,要想得到完全沒執(zhí)行的效果,就要消除掉已經(jīng)產(chǎn)生的影響,這需要加強控制邏輯的功能。

控制冒險:上一節(jié)流水線冒險中我們提到了控制依賴,它會導致控制冒險。當執(zhí)行到條件跳轉指令時,需要做分支預測,一旦預測錯誤,就需要消除已經(jīng)執(zhí)行的若干條指令,重新執(zhí)行正確分支的指令。當執(zhí)行到子函數(shù)返回指令時,需要從存儲器中取出返回地址,因此下一條指令直到訪存階段才能開始執(zhí)行。這些特殊情況都需要我們特殊考慮,并在控制邏輯中實現(xiàn)。

如果詳細講解這兩部分的具體實現(xiàn),又會花很多篇幅,有興趣的朋友可以訪問這本書的官網(wǎng)進一步了解。

與真實指令集架構的差距

本文講述了Y86指令集架構的設計過程,雖然敘述已經(jīng)足夠粗略,可還是寫了這么長的篇幅。然而如果與真實的指令集架構(比如x86)的復雜度相比那又真是小巫見大巫了。我們只規(guī)定了一個非常簡單的指令集,并完成了一個簡易的實現(xiàn)。而真實的指令集會包含非常多的指令,包括一些多周期的指令,比如浮點數(shù)運算指令,這些指令無法在一個周期內完成,因此需要一些額外的硬件單元的支持。Y86中的存儲器被我們看做是理想的存儲單元,我們認為數(shù)據(jù)的存取操作都可以在一個時鐘周期內完成。然而CPU速率與內存速率其實相差上千倍,通常需要多級緩存構成一個復雜的存儲器層次結構才能加快存取效率?,F(xiàn)代處理器還采用了多發(fā)射和亂序執(zhí)行技術,已經(jīng)不是Y86中所描述的一個階段一個階段地執(zhí)行了,而是多條指令同時執(zhí)行,而且與它們在代碼中的先后順序無關。近些年,處理器向多核方向發(fā)展,多個核具有更強的處理能力,也使指令在代碼級別的并行執(zhí)行成為潮流。今后,處理器會采用哪些新技術我們無從得知,但一定會變得越來越復雜。不過萬變不離其宗,理解了處理器和指令集的基本原理,我們可以看透一切,再復雜的系統(tǒng)也是從基本形式一步步擴展得到的,把握核心才是最關鍵的。

編輯于 2021-05-13 21:07

評論千萬條,友善第一條


24 條評論

默認

最新

飛天小石頭

這個是深入理解計算機系統(tǒng)書上的內容吧?

2021-10-02

王金戈

作者

是的

2021-10-02

mushroom

全文照抄

2021-11-03

黃猩猩

點贊收藏退出 一氣呵成

2020-03-11

Lim Shaw

師兄太牛了,寒武紀架構部需要你

2020-03-16

王金戈

作者

2020-03-16

Micro

大佬您好,請問“取指、譯碼、執(zhí)行、訪存、寫回”這五個過程和“發(fā)射”這個過程是什么樣的關系呢?是先發(fā)射,然后取指、譯碼、執(zhí)行、訪存、寫回嗎?跪謝大佬?。?/p>

02-06

Micro

王金戈

奧奧,我打字打錯了,是被發(fā)送到不同的運算器上。

02-07

Micro

王金戈

奧奧好的,謝謝大佬,我剛才查了下,說是 指令發(fā)射,指的是指令從譯碼階段被派發(fā)到不用運算器上進入執(zhí)行階段。大佬你覺得這么說對嘛

02-06

尚戈繼

cs.utexas.edu/users/wit?這篇ppt和博主的博文可以互相補充。

2021-06-20

fine就fine在他喵

因此時鐘周期不能提得太高,否則將造成指令執(zhí)行紊亂。
原文這一句不太懂

,周期高是指一個周期時間長嗎?時間長的話不是應該不容易出問題嘛,

2021-05-13

fine就fine在他喵

王金戈

2021-05-13

王金戈

作者

感謝提醒!原文這里的確寫錯了,現(xiàn)在已經(jīng)改正,應該是“不能降得太低”。

2021-05-13

待星殘羽

收藏 & 吃灰

2020-05-05

馮曉蔥

這必須贊?。。?!

2020-04-21

笨拙的泥匠

永遠也沒有看完的的csapp

2020-04-10

深度人工dazed

請問你的csapp是第三版英文的嘛?

2020-03-16

王金戈

作者

深度人工dazed

電子版嗎?沒有

2020-03-17

深度人工dazed

王金戈

請問你有第3版的英文版嘛?

2020-03-16

cache

我還以為

2020-03-11


【轉】 從零開始制作自己的指令集架構的評論 (共 條)

分享到微博請遵守國家法律
信阳市| 随州市| 佛山市| 榆林市| 台湾省| 夹江县| 阳新县| 长兴县| 阿城市| 苗栗县| 紫金县| 平和县| 清河县| 察雅县| 双牌县| 林甸县| 鹤庆县| 嘉兴市| 余干县| 彰化市| 临沭县| 阿拉尔市| 竹溪县| 新干县| 乾安县| 宁武县| 怀柔区| 顺平县| 农安县| 无棣县| 永嘉县| 宜兰县| 高邑县| 盐池县| 凌海市| 开平市| 梁河县| 威宁| 汾阳市| 肇东市| 晋江市|