本文上接《大道至簡——RISC-V架構(gòu)之魂(上)》
2.3 規(guī)整的指令編碼
在流水線中能夠盡早盡快的讀取通用寄存器組,往往是處理器流水線設(shè)計的期望之一,這樣可以提高處理器性能和優(yōu)化時序。這個看似簡單的道理在很多現(xiàn)存的商用RISC架構(gòu)中都難以實(shí)現(xiàn),因?yàn)榻?jīng)過多年反復(fù)修改不斷添加新指令后,其指令編碼中的寄存器索引位置變得非常的凌亂,給譯碼器造成了負(fù)擔(dān)。
得益于后發(fā)優(yōu)勢和總結(jié)了多年來處理器發(fā)展的教訓(xùn),RISC-V的指令集編碼非常的規(guī)整,指令所需的通用寄存器的索引(Index)都被放在固定的位置,如圖2所示。因此指令譯碼器(Instruction Decoder)可以非常便捷的譯碼出寄存器索引然后讀取通用寄存器組(Register File,Regfile)。
圖2 RV32I規(guī)整的指令編碼格式
這里寫圖片描述
2.4 簡潔的存儲器訪問指令
與所有的RISC處理器架構(gòu)一樣,RISC-V架構(gòu)使用專用的存儲器讀(Load)指令和存儲器寫(Store)指令訪問存儲器(Memory),其他的普通指令無法訪問存儲器,這種架構(gòu)是RISC架構(gòu)的常用的一個基本策略,這種策略使得處理器核的硬件設(shè)計變得簡單。
存儲器訪問的基本單位是字節(jié)(Byte)。RISC-V的存儲器讀和存儲器寫指令支持一個字節(jié)(8位),半字(16位),單字(32位)為單位的存儲器讀寫操作,如果是64位架構(gòu)還可以支持一個雙字(64位)為單位的存儲器讀寫操作。
RISC-V架構(gòu)的存儲器訪問指令還有如下顯著特點(diǎn):
為了提高存儲器讀寫的性能,RISC-V架構(gòu)推薦使用地址對齊的存儲器讀寫操作,但是地址非對齊的存儲器操作RISC-V架構(gòu)也支持,處理器可以選擇用硬件來支持,也可以選擇用軟件來支持。
由于現(xiàn)在的主流應(yīng)用是小端格式(Little-Endian),RISC-V架構(gòu)僅支持小端格式。有關(guān)小端格式和大端格式的定義和區(qū)別,本文在此不做過多介紹,若對此不甚了解的初學(xué)者可以自行查閱學(xué)習(xí)。
很多的RISC處理器都支持地址自增或者自減模式,這種自增或者自減的模式雖然能夠提高處理器訪問連續(xù)存儲器地址區(qū)間的性能,但是也增加了設(shè)計處理器的難度。RISC-V架構(gòu)的存儲器讀和存儲器寫指令不支持地址自增自減的模式。
RISC-V架構(gòu)采用松散存儲器模型(Relaxed Memory Model),松散存儲器模型對于訪問不同地址的存儲器讀寫指令的執(zhí)行順序不作要求,除非使用明確的存儲器屏障(Fence)指令加以屏蔽。
這些選擇都清楚地反映了RISC-V架構(gòu)力圖簡化基本指令集,從而簡化硬件設(shè)計的哲學(xué)。RISC-V架構(gòu)如此定義非常合理,能夠達(dá)到能屈能伸的效果。譬如:對于低功耗的簡單CPU,可以使用非常簡單的硬件電路即可完成設(shè)計;而對于追求高性能的超標(biāo)量處理器則可以通過復(fù)雜設(shè)計的動態(tài)硬件調(diào)度能力來提高性能。
2.5 高效的分支跳轉(zhuǎn)指令
RISC-V架構(gòu)有兩條無條件跳轉(zhuǎn)指令(Unconditional Jump),jal與jalr指令。跳轉(zhuǎn)鏈接(Jump and Link)指令jal可用于進(jìn)行子程序調(diào)用,同時將子程序返回地址存在鏈接寄存器(Link Register:由某一個通用整數(shù)寄存器擔(dān)任)中。跳轉(zhuǎn)鏈接寄存器(Jump and Link-Register)指令jalr指令能夠用于子程序返回指令,通過將jal指令(跳轉(zhuǎn)進(jìn)入子程序)保存的鏈接寄存器用于jalr指令的基地址寄存器,則可以從子程序返回。
RISC-V架構(gòu)有6條帶條件跳轉(zhuǎn)指令(Conditional Branch),這種帶條件的跳轉(zhuǎn)指令跟普通的運(yùn)算指令一樣直接使用2個整數(shù)操作數(shù),然后對其進(jìn)行比較,如果比較的條件滿足時,則進(jìn)行跳轉(zhuǎn)。因此,此類指令將比較與跳轉(zhuǎn)兩個操作放到了一條指令里完成。
作為比較,很多的其他RISC架構(gòu)的處理器需要使用兩條獨(dú)立的指令。第一條指令先使用比較指令,比較的結(jié)果被保存到狀態(tài)寄存器之中;第二條指令使用跳轉(zhuǎn)指令,判斷前一條指令保存在狀態(tài)寄存器當(dāng)中的比較結(jié)果為真時則進(jìn)行跳轉(zhuǎn)。相比而言RISC-V的這種帶條件跳轉(zhuǎn)指令不僅減少了指令的條數(shù),同時硬件設(shè)計上更加簡單。
對于沒有配備硬件分支預(yù)測器的低端CPU,為了保證其性能,RISC-V的架構(gòu)明確要求其采用默認(rèn)的靜態(tài)分支預(yù)測機(jī)制,即:如果是向后跳轉(zhuǎn)的條件跳轉(zhuǎn)指令,則預(yù)測為“跳”;如果是向前跳轉(zhuǎn)的條件跳轉(zhuǎn)指令,則預(yù)測為“不跳”,并且RISC-V架構(gòu)要求編譯器也按照這種默認(rèn)的靜態(tài)分支預(yù)測機(jī)制來編譯生成匯編代碼,從而讓低端的CPU也能得到不錯的性能。
為了使硬件設(shè)計盡量簡單,RISC-V架構(gòu)特地定義了所有的帶條件跳轉(zhuǎn)指令跳轉(zhuǎn)目標(biāo)的偏移量(相對于當(dāng)前指令的地址)都是有符號數(shù),并且其符號位被編碼在固定的位置。因此,這種靜態(tài)預(yù)測機(jī)制在硬件上非常容易實(shí)現(xiàn),硬件譯碼器可以輕松的找到這個固定的位置,并判斷其是0還是1來判斷其是正數(shù)還是負(fù)數(shù),如果是負(fù)數(shù)則表示跳轉(zhuǎn)的目標(biāo)地址為當(dāng)前地址減去偏移量,也就是向后跳轉(zhuǎn),則預(yù)測為“跳”。當(dāng)然對于配備有硬件分支預(yù)測器的高端CPU,則可以采用高級的動態(tài)分支預(yù)測機(jī)制來保證性能。
2.6 簡潔的子程序調(diào)用
為了理解此節(jié),需先對一般RISC架構(gòu)中程序調(diào)用子函數(shù)的過程予以介紹,其過程如下:
進(jìn)入子函數(shù)之后需要用存儲器寫(Store)指令來將當(dāng)前的上下文(通用寄存器等的值)保存到系統(tǒng)存儲器的堆棧區(qū)內(nèi),這個過程通常稱為“保存現(xiàn)場”。
在退出子程序之時,需要用存儲器讀(Load)指令來將之前保存的上下文(通用寄存器等的值)從系統(tǒng)存儲器的堆棧區(qū)讀出來,這個過程通常稱為“恢復(fù)現(xiàn)場”。
“保存現(xiàn)場”和“恢復(fù)現(xiàn)場”的過程通常由編譯器編譯生成的指令來完成,使用高層語言(譬如C或者C++)開發(fā)的開發(fā)者對此可以不用太關(guān)心。高層語言的程序中直接寫上一個子函數(shù)調(diào)用即可,但是這個底層發(fā)生的“保存現(xiàn)場”和“恢復(fù)現(xiàn)場”的過程卻是實(shí)實(shí)在在地發(fā)生著(可以從編譯出的匯編語言里面看到那些“保存現(xiàn)場”和“恢復(fù)現(xiàn)場”的匯編指令),并且還需要消耗若干的CPU執(zhí)行時間。
為了加速這個“保存現(xiàn)場”和“恢復(fù)現(xiàn)場”的過程,有的RISC架構(gòu)發(fā)明了一次寫多個寄存器到存儲器中(Store Multiple),或者一次從存儲器中讀多個寄存器出來(Load Multiple)的指令,此類指令的好處是一條指令就可以完成很多事情,從而減少匯編指令的代碼量,節(jié)省代碼的空間大小。但是此種“Load Multiple”和“Store Multiple”的弊端是會讓CPU的硬件設(shè)計變得復(fù)雜,增加硬件的開銷,也可能損傷時序使得CPU的主頻無法提高,筆者在曾經(jīng)設(shè)計此類處理器時便深受其苦。
RISC-V架構(gòu)則放棄使用這種“Load Multiple”和“Store Multiple”指令。并解釋,如果有的場合比較介意這種“保存現(xiàn)場”和“恢復(fù)現(xiàn)場”的指令條數(shù),那么可以使用公用的程序庫(專門用于保存和恢復(fù)現(xiàn)場)來進(jìn)行,這樣就可以省掉在每個子函數(shù)調(diào)用的過程中都放置數(shù)目不等的“保存現(xiàn)場”和“恢復(fù)現(xiàn)場”的指令。
此選擇再次印證了RISC-V追求硬件簡單的哲學(xué),因?yàn)榉艞墶癓oad Multiple”和“Store Multiple”指令可以大幅簡化CPU的硬件設(shè)計,對于低功耗小面積的CPU可以選擇非常簡單的電路進(jìn)行實(shí)現(xiàn),而高性能超標(biāo)量處理器由于硬件動態(tài)調(diào)度能力很強(qiáng),可以有強(qiáng)大的分支預(yù)測電路保證CPU能夠快速的跳轉(zhuǎn)執(zhí)行,從而可以選擇使用公用的程序庫(專門用于保存和恢復(fù)現(xiàn)場)的方式減少代碼量,但是同時達(dá)到高性能。
2.7 無條件碼執(zhí)行
很多早期的RISC架構(gòu)發(fā)明了帶條件碼的指令,譬如在指令編碼的頭幾位表示的是條件碼(Conditional Code),只有該條件碼對應(yīng)的條件為真時,該指令才被真正執(zhí)行。
這種將條件碼編碼到指令中的形式可以使得編譯器將短小的循環(huán)編譯成帶條件碼的指令,而不用編譯成分支跳轉(zhuǎn)指令。這樣便減少了分支跳轉(zhuǎn)的出現(xiàn),一方面減少了指令的數(shù)目;另一方面也避免了分支跳轉(zhuǎn)帶來的性能損失。然而,這種“條件碼”指令的弊端同樣會使得CPU的硬件設(shè)計變得復(fù)雜,增加硬件的開銷,也可能損傷時序使得CPU的主頻無法提高,筆者在曾經(jīng)設(shè)計此類處理器時便深受其苦。
RISC-V架構(gòu)則放棄使用這種帶“條件碼”指令的方式,對于任何的條件判斷都使用普通的帶條件分支跳轉(zhuǎn)指令。此選擇再次印證了RISC-V追求硬件簡單的哲學(xué),因?yàn)榉艞墡А皸l件碼”指令的方式可以大幅簡化CPU的硬件設(shè)計,對于低功耗小面積的CPU可以選擇非常簡單的電路進(jìn)行實(shí)現(xiàn),而高性能超標(biāo)量處理器由于硬件動態(tài)調(diào)度能力很強(qiáng),可以有強(qiáng)大的分支預(yù)測電路保證CPU能夠快速的跳轉(zhuǎn)執(zhí)行達(dá)到高性能。
2.8 無分支延遲槽
很多早期的RISC架構(gòu)均使用了“分支延遲槽(Delay Slot)”,最具有代表性的便是MIPS架構(gòu),在很多經(jīng)典的計算機(jī)體系結(jié)構(gòu)教材中,均使用MIPS對分支延遲槽進(jìn)行過介紹。分支延遲槽就是指在每一條分支指令后面緊跟的一條或者若干條指令不受分支跳轉(zhuǎn)的影響,不管分支是否跳轉(zhuǎn),這后面的幾條指令都一定會被執(zhí)行。
早期的RISC架構(gòu)很多采用了分支延遲槽誕生的原因主要是因?yàn)楫?dāng)時的處理器流水線比較簡單,沒有使用高級的硬件動態(tài)分支預(yù)測器,所以使用分支延遲槽能夠取得可觀的性能效果。然而,這種分支延遲槽使得CPU的硬件設(shè)計變得極為的別扭,CPU設(shè)計人員對此往往苦不堪言。
RISC-V架構(gòu)則放棄了分支延遲槽,再次印證了RISC-V力圖簡化硬件的哲學(xué),因?yàn)楝F(xiàn)代的高性能處理器的分支預(yù)測算法精度已經(jīng)非常高,可以有強(qiáng)大的分支預(yù)測電路保證CPU能夠準(zhǔn)確的預(yù)測跳轉(zhuǎn)執(zhí)行達(dá)到高性能。而對于低功耗小面積的CPU,由于無需支持分支延遲槽,硬件得到極大簡化,也能進(jìn)一步減少功耗和提高時序。
2.9 無零開銷硬件循環(huán)
很多RISC架構(gòu)還支持零開銷硬件循環(huán)(Zero Overhead Hardware Loop)指令,其思想是通過硬件的直接參與,通過設(shè)置某些循環(huán)次數(shù)寄存器(Loop Count),然后可以讓程序自動地進(jìn)行循環(huán),每一次循環(huán)則Loop Count自動減1,這樣持續(xù)循環(huán)直到Loop Count的值變成0,則退出循環(huán)。
之所以提出發(fā)明這種硬件協(xié)助的零開銷循環(huán)是因?yàn)樵谲浖a中的for 循環(huán)(for i=0; i<N; i++)極為常見,而這種軟件代碼通過編譯器編譯之后,往往會編譯成若干條加法指令和條件分支跳轉(zhuǎn)指令,從而達(dá)到循環(huán)的效果。一方面這些加法和條件跳轉(zhuǎn)指令占據(jù)了指令的條數(shù);另外一方面條件分支跳轉(zhuǎn)如存在著分支預(yù)測的性能問題。而硬件協(xié)助的零開銷循環(huán),則將這些工作由硬件直接完成,省掉了這些加法和條件跳轉(zhuǎn)指令,減少了指令條數(shù)且提高了性能。
然有得必有失,此類零開銷硬件循環(huán)指令大幅地增加了硬件設(shè)計的復(fù)雜度。因此,零開銷循環(huán)指令與RISC-V架構(gòu)簡化硬件的哲學(xué)是完全相反的,在RISC-V架構(gòu)中自然沒有使用此類零開銷硬件循環(huán)指令。
2.10 簡潔的運(yùn)算指令
在本章第2.1節(jié)中曾經(jīng)提到RISC-V架構(gòu)使用模塊化的方式組織不同的指令子集,最基本的整數(shù)指令子集(I字母表示)支持的運(yùn)算包括加法、減法、移位、按位邏輯操作和比較操作。這些基本的運(yùn)算操作能夠通過組合或者函數(shù)庫的方式完成更多的復(fù)雜操作(譬如乘除法和浮點(diǎn)操作),從而能夠完成大多數(shù)的軟件操作。
整數(shù)乘除法指令子集(M字母表示)支持的運(yùn)算包括,有符號或者無符號的乘法和除法操作。乘法操作能夠支持兩個32位的整數(shù)相乘得到一個64位的結(jié)果;除法操作能夠支持兩個32位的整數(shù)相除得到一個32位的商與32位的余數(shù)。
單精度浮點(diǎn)指令子集(F字母表示)與雙精度浮點(diǎn)指令子集(D字母表示)支持的運(yùn)算包括浮點(diǎn)加減法,乘除法,乘累加,開平方根和比較等操作,同時提供整數(shù)與浮點(diǎn),單精度與雙精度浮點(diǎn)彼此之間的格式轉(zhuǎn)換操作。
很多RISC架構(gòu)的處理器在運(yùn)算指令產(chǎn)生錯誤之時,譬如上溢(Overflow)、下溢(Underflow)、非規(guī)格化浮點(diǎn)數(shù)(Subnormal)和除零(Divide by Zero),都會產(chǎn)生軟件異常。RISC-V架構(gòu)的一個特殊之處是對任何的運(yùn)算指令錯誤(包括整數(shù)與浮點(diǎn)指令)均不產(chǎn)生異常,而是產(chǎn)生某個特殊的默認(rèn)值,同時,設(shè)置某些狀態(tài)寄存器的狀態(tài)位。RISC-V架構(gòu)推薦軟件通過其他方法來找到這些錯誤。再次清楚地反映了RISC-V架構(gòu)力圖簡化基本的指令集,從而簡化硬件設(shè)計的哲學(xué)。
2.11 優(yōu)雅的壓縮指令子集
基本的RISC-V基本整數(shù)指令子集(字母I表示 )規(guī)定的指令長度均為等長的32位,這種等長指令定義使得僅支持整數(shù)指令子集的基本RISC-V CPU非常容易設(shè)計。但是等長的32位編碼指令也會造成代碼體積(Code Size)相對較大的問題。
為了滿足某些對于代碼體積要求較高的場景(譬如嵌入式領(lǐng)域),RISC-V定義了一種可選的壓縮(Compressed)指令子集,由字母C表示,也可以由RVC表示。RISC-V具有后發(fā)優(yōu)勢,從一開始便規(guī)劃了壓縮指令,預(yù)留了足夠的編碼空間,16位長指令與普通的32位長指令可以無縫自由地交織在一起,處理器也沒有定義額外的狀態(tài)。
RISC-V壓縮指令的另外一個特別之處是,16位指令的壓縮策略是將一部分普通最常用的的32位指令中的信息進(jìn)行壓縮重排得到(譬如假設(shè)一條指令使用了兩個同樣的操作數(shù)索引,則可以省去其中一個索引的編碼空間),因此每一條16位長的指令都能一一找到其對應(yīng)的原始32位指令。因此,程序編譯成為壓縮指令僅在匯編器階段就可以完成,極大的簡化了編譯器工具鏈的負(fù)擔(dān)。
RISC-V架構(gòu)的研究者進(jìn)行了詳細(xì)的代碼體積分析,如圖3所示,通過分析結(jié)果可以看出,RV32C的代碼體積相比RV32的代碼體積減少了百分之四十,并且與ARM,MIPS和x86等架構(gòu)相比都有不錯的表現(xiàn)。
圖3 各指令集架構(gòu)的代碼密度比較(數(shù)據(jù)越小越好)
這里寫圖片描述