Xtensa處理器窗寄存器函數(shù)調(diào)用機(jī)制與應(yīng)用
現(xiàn)代處理器為了更好的支持高級編程語言的高效編譯,通常處理器所擁有的通用寄存器的數(shù)目都有16個(gè)甚至32個(gè)之多,如此多的寄存器在比較復(fù)雜的應(yīng)用程序上實(shí)現(xiàn)深度嵌套調(diào)用的時(shí)候,為了保證程序的正確執(zhí)行,寄存器要頻繁的進(jìn)行入棧和出棧操作,這樣頻繁的堆棧存儲(chǔ)器訪問將明顯降低應(yīng)用程序的性能,為有效解決這一問題,tensilica的Xtensa架構(gòu)設(shè)計(jì)了一種Windows旋轉(zhuǎn)方式的寄存器管理機(jī)制,將邏輯寄存器和物理寄存器分開,在函數(shù)調(diào)用的時(shí)候通過windows滑動(dòng)切換邏輯寄存器,從而避免寄存器覆蓋,減少壓棧和出棧的操作,更大限度的提高性能。
以一個(gè)MP3解碼器為例(如表1),假設(shè)外部存儲(chǔ)器的訪問的R/W等待cycles分別為100和20,可以看到采用Call8的windows旋轉(zhuǎn)大幅減少M(fèi)CPS到9%之多。
表1:MP3解碼器。
那么Windows寄存器機(jī)制是如何工作的,它又有那些典型應(yīng)用呢? 本文將詳細(xì)闡述這一主題。
寄存器Windows函數(shù)調(diào)用機(jī)制原理
1.AR物理寄存器環(huán)形Buffer
該方法的基本實(shí)現(xiàn)原理是用更多的物理AR寄存器組成一個(gè)環(huán)形的buffer,這些物理寄存器每4個(gè)為一組(pane),用一個(gè)WindowStart的每個(gè)比特依次表示是否該組作為邏輯寄存器窗口的起始位置或者占用,當(dāng)前的邏輯寄存器的起始位置則用WindowBase狀態(tài)寄存器來表示。如圖1,在發(fā)生函數(shù)調(diào)用的時(shí)候則通過修改WindowBase寄存器,滑動(dòng)邏輯寄存器窗口,設(shè)置相應(yīng)的WindowStart比特標(biāo)識當(dāng)前邏輯窗口在環(huán)形物理AR寄存器buffer中的位置。這樣父子函數(shù)看到的是不同的物理寄存器,避免了寄存器的壓棧和出棧。要說明的是,如果AR物理寄存器的數(shù)目為NAREG,則WindowStart的比特?cái)?shù)則為NAREG/4,而WindowBase的比特位數(shù)則為log(NAREG/4),如圖1所示,物理寄存器數(shù)為32,則WindowStart比特?cái)?shù)為8,WindowBase比特?cái)?shù)則為3.
圖1:Windows AR寄存器環(huán)形buffer.
2.Windows ABI函數(shù)調(diào)用規(guī)范
以每4個(gè)寄存器(pane)為單位,函數(shù)調(diào)用的時(shí)候窗口可以滑動(dòng)4個(gè)、8個(gè)、或者12個(gè)物理寄存器,分別可以用call4、call8、call12指令來實(shí)現(xiàn),而最典型的應(yīng)用則為call8,在c語言層面,編譯器通過XPG的core配置,可以為函數(shù)調(diào)用分別產(chǎn)生非windows機(jī)制的call0和call8,那么call8的Windows ABI函數(shù)調(diào)用規(guī)范是怎樣的呢? 參考圖2,左上角說明的是子函數(shù)調(diào)用約用規(guī)范,a0被用來保存返回地址,a1則為sp堆棧指針,a2~a7則用來傳遞函數(shù)入?yún)?,參?shù)超過6個(gè)的時(shí)候則需要使用堆棧了,以對調(diào)用者函數(shù)和被調(diào)用函數(shù)來說,a0~a7為獨(dú)立的寄存器,可以自由使用,而a8~a15則為scratch寄存器,隨時(shí)會(huì)被子函數(shù)使用,調(diào)用者函數(shù)如果要使用,則在調(diào)用子函數(shù)前要壓棧保存。
圖2:Window ABI調(diào)用規(guī)范。
為方便寄存器正常的保存與恢復(fù),以及調(diào)用棧的高效回溯,有必要對函數(shù)的Frame??臻g做統(tǒng)一的安排,在call8的Windows ABI規(guī)范下,Tensilica進(jìn)行了如下設(shè)計(jì)(如圖3)。
圖3:Windows ABI堆棧布局。
每級函數(shù)FrAME下包含有Base Area用于存儲(chǔ)其父函數(shù)的基本寄存器a0~a3,可能的extra area保存其子函數(shù)的擴(kuò)展寄存器a4~a7(call8),或者a4~a11(call12),函數(shù)局部變量(非寄存器變量)和alloc分配空間,及用于傳子函數(shù)所需要的??臻g等等。
當(dāng)較新的深度函數(shù)Fun(i)的寄存器窗口覆蓋到過去的函數(shù)Fun(p)時(shí),基本寄存器a0~a3保存到Fun(p+1)的basic area,額外的寄存器則存入Fun(p)的extra area,當(dāng)函數(shù)Fun(p+1)返回時(shí),如果檢測到underflow則相應(yīng)地將base area和extra area的寄存器恢復(fù)到Fun(p)的活動(dòng)窗口,讀者可以參考Tensilica的代碼體會(huì)一下,這樣的布局在壓棧和恢復(fù)的時(shí)候代碼是最高效和節(jié)省空間的。
3.Windows寄存器覆蓋問題
物理AR寄存器的數(shù)目是有限的,典型情況下,32個(gè)物理寄存器發(fā)生深度為3次,64個(gè)AR發(fā)生7次的函數(shù)調(diào)用后將會(huì)覆蓋到原來的函數(shù)寄存器窗,那么如何有效檢測和處理寄存器overflow問題呢?
寄存器的覆蓋檢測只發(fā)生在如下兩種情況:
函數(shù)調(diào)用時(shí),參考如下硬件semanTIcs:
CALLn/CALLXn
PS.CALLINC ← n32
AR[n||2'b00] ← n || (PC + 3)290
ENTRY s,imm12
WindowCheck (00,PS.CALLINC,00)
if as > 3 | PS.WOE = 0 | PS.EXCM = 1 then
-- undefined operatiON
-- may raise illegal instruction exception
else
AR[PS.CALLINC||s10] ← AR[s]- (017||imm12||03)
WindowBase ← WindowBase + (02||PS.CALLINC)
WindowStartWindowBase ← 1
endif
在發(fā)生函數(shù)調(diào)用,執(zhí)行call指令的時(shí)候,窗遞增值(call4、call8、call12分別對應(yīng)1、2、3)存入PS處理器狀態(tài)寄存器的CALLINC域,在進(jìn)入函數(shù)的入口處ENTRY指令將首先進(jìn)行Window重疊檢測,條件滿足的時(shí)候?qū)⒂|發(fā)相應(yīng)的windows overflow異常,引導(dǎo)程序進(jìn)行覆蓋寄存器的入棧保護(hù)。
正常模式下函數(shù)內(nèi)部指令的寄存器引用,如xxx ar,as,at,處理器在非異常模式下將進(jìn)行正常的window檢測,否則產(chǎn)生非法指令異常。
4.Windows寄存器檢測方法
寄存器覆蓋檢測通過如下硬件semantic實(shí)現(xiàn):
WindowCheck
n ← if (wr ≠ 2'b00 or ws ≠ 2'b00 or wt ≠ 2'b00) and WindowStartWindowBase+1 then 2'b01
else if (wr1 or ws1 or wt1) and WindowStartWindowBase+2 then 2'b10
else if (wr = 2'b11 or ws = 2'b11 or wt = 2'b11) and WindowStartWindowBase+3 then 2'b11
else 2'b00
if CWOE = 1 and n ≠ 2'b00 then
PS.OWB ← WindowBase
m ← WindowBase + (2'b00||n)
PS.EXCM ← 1
EPC[1] ← PC
nextPC ← if WindowStartm+1 then WindowOverflow4
else if WindowStartm+2 then WindowOverflow8
else WindowOverflow12
WindowBase ← m(注:和Overflow跳轉(zhuǎn)并行)
endif
通過深入解析如上原語,有如下注意要點(diǎn):任何地方引用a0~a3不會(huì)產(chǎn)生windows異常,因此在用戶的c或者匯編代碼里可以任意使用,為什么呢? 因?yàn)樵赼0~a3引用的任意環(huán)境里,當(dāng)前函數(shù)的邏輯窗里的物理寄存器,要么是無覆蓋安全到達(dá),要么是經(jīng)過了函數(shù)調(diào)用entry指令觸發(fā)windows overflow異常,在異常里,a0~a3的所在物理AR寄存器已經(jīng)安全地壓棧保存了。
a15~a4之間的高位寄存器(比如a15)引用會(huì)觸發(fā)低位寄存器(如a4)的寄存器覆蓋檢測,哪怕沒有指令顯式的應(yīng)用低位寄存器,觸發(fā)的順序?qū)⑹窍冗M(jìn)行overflow4,overflow8,至overflow12,從而最有效和最安全地保存活動(dòng)寄存器。通過了解以上兩點(diǎn),讀者可以深入理解Tensilica提供的高效XTOS代碼,透徹體會(huì)相關(guān)代碼的精妙之處。
5. Windows寄存器下溢(underflow)問題
當(dāng)子函數(shù)返回時(shí),RETW或者RETW.N指令執(zhí)行,此時(shí)也僅此時(shí)處理器將進(jìn)行上溢檢查。如果當(dāng)Windowbase所在位置的前3個(gè)window pane(4 registers組)的WindowStart比特都為零,則意味著返回后的父函數(shù)發(fā)生過 WindowOverflow,父函數(shù)的窗口寄存器曾經(jīng)被壓入棧,要先行通過相應(yīng)的underflow彈出。
如果不是全為零,則應(yīng)該不為零的點(diǎn)和正常window返回的點(diǎn)對應(yīng),要正常返回,如果不同,則說明發(fā)生了不正常的調(diào)用,a0被破壞掉,要產(chǎn)生非法指令錯(cuò)誤。關(guān)于這個(gè)方面的具體硬件原語,讀者可以參考Xtensa的ISA手冊,這里不再贅述。
Alloca異常問題
C語言中函數(shù)中經(jīng)常會(huì)發(fā)生從堆棧中分配臨時(shí)空間的情況,在正常的不發(fā)生窗寄存器溢出的時(shí)候沒有任何問題。但是,如果該函數(shù)的下級函數(shù)的嵌套調(diào)用曾導(dǎo)致過寄存器溢出,由于該函數(shù)的堆棧Frame底部存有溢出的basic area的寄存器,如果簡單的偏移堆棧指針來分配臨時(shí)空間,則這些保存過basic寄存器會(huì)被完全破壞掉。為有效解決這一問題,Xtensa架構(gòu)引入一個(gè)特殊的Alloca異常來管理basic area寄存器的搬移和臨時(shí)空間的分配。
當(dāng)函數(shù)內(nèi)部進(jìn)行局部stack的內(nèi)存分配時(shí),Xtensa編譯器會(huì)生成一個(gè)MOVSP at,as指令,異常的檢測通過這一指令來完成,該指令有如下原語:
if WindowStartWindowBase-0011WindowBase-0001 = 03 then
Exception (AllocaCause)
elseAR[t] ← AR[s]
endif
類似于underflow,如果當(dāng)前寄存器窗口前3個(gè)register pane的占用狀態(tài)全為0(全部為自由使用狀態(tài)),則說明其上一級函數(shù)一定發(fā)生過窗口溢出,當(dāng)前函數(shù)棧下方一定保存有溢出的寄存器,簡單的修改SP指針不再安全,需要觸發(fā)Alloca異常來進(jìn)行正確處理。需要說明的是,發(fā)生alloc異常的時(shí)候,過去的寄存器窗口調(diào)用已經(jīng)循環(huán)一周,且發(fā)生溢出,溢出的充分必要條件必然是當(dāng)前寄存器窗口的前3個(gè)register pane占用狀態(tài)全為0(WindowStartWindowBase-0011WindowBase-0001 = 000),其次當(dāng)前函數(shù)不可能是調(diào)用樹的葉子節(jié)點(diǎn),當(dāng)前函數(shù)的前半部分曾經(jīng)進(jìn)入過,且過去進(jìn)入的路徑上發(fā)生過溢出,否則就沒有產(chǎn)生異常的必要。alloca異常是為解決sp覆蓋而引入的硬件機(jī)制。
這里解釋了alloc異常產(chǎn)生的基本原理,那么,什么樣的代碼會(huì)產(chǎn)生MOVsp指令,從而可能觸發(fā)alloc異常呢? 有如下幾種情況:
調(diào)用alloc函數(shù),如
void foo(int array_size) {
char * bar = alloca(array_size);
…
使用變長數(shù)組(GNU C 擴(kuò)展語言),如
void foo(int array_size) {
char bar[array_size];
…
使用嵌套函數(shù)定義(GNU C 擴(kuò)展語言),如
void afunction(void) {
…
int anotherfunction(void) {
}
使用特別長的局部數(shù)組,如
void foo(void) {
int an_array[8192]; // 32,768 bytes
int another_array[100]; // 400 bytes
…
精確的size限制是32,760,包含16~48字節(jié)的Frame開銷。
當(dāng)然,這里列出的不是全部的可能情況,僅僅列出幾個(gè)常見用例,讀者不能認(rèn)為自己的代碼沒有以上情況,編譯器就不會(huì)產(chǎn)生movsp指令,從而不會(huì)產(chǎn)生alloc異常。
由于alloc是一種不容易避免的正常的異常,應(yīng)用軟件需要積極的處理。處理的思路有兩種,其一是用異常或者中斷棧作為臨時(shí)儲(chǔ)存來搬移,這里要介紹另外一種比較巧妙的方法,如下述代碼:
rsr a2,PS
rsr a4,WINDOWBASE
extui a3,a2,XCHAL_PS_OWB_SHIFT,XCHAL_PS_OWB_BITS
xor a3,a3,a4
slli a3,a3,XCHAL_PS_OWB_SHIFT
xor a2,a2,a3
wsr a2,PS
l32i a4,a1,A4_SAVE
l32i a3,a1,A3_SAVE
l32i a2,a1,A2_SAVE
addi a1,a1,USER_EXCEPTION_FRAME_SIZE
rsync
// Now branch on the call increment
// We could branch earlier rotw instructions prior to the handler
// which would avoid executing two rotw instructions in the underflow
// eight case. However,the underflow 8 handler is right at a
// cache-line,so that would likely involve an extra cache miss better
// to just take the single cycle penalty here.
rotw -1
// what was a0 (that had the return address) is now a4
_bbci.l a4,31,_WindowUnderflow4
rotw -1
// what was a0 (that had the return address) is now a8
_bbci.l a8,30,_WindowUnderflow8
j _setup_WindowUnderflow12
這段代碼的巧妙之處在于首先通過將當(dāng)前的Windowbase保存到PS的OWB域中,然后通過反向旋轉(zhuǎn)窗,根據(jù)a0的高端2bit表示的調(diào)用類型,跳轉(zhuǎn)到相應(yīng)的下溢exception的位置進(jìn)行出?;謴?fù)save area的寄存器,當(dāng)sp指針正確偏移后,利用寄存器的引用觸發(fā)overflow異常自動(dòng)進(jìn)行再次入棧,從而實(shí)現(xiàn)搬移。
Context Switch下的寄存器保存與恢復(fù)
在windows窗口寄存器機(jī)制下,多任務(wù)RTOS的上下文環(huán)境切換問題變得非常有趣。 那么該如何保存這些通用寄存器呢,是不是所有的物理寄存器都要保存與恢復(fù)呢? 答案是否定的,除了當(dāng)前窗口的邏輯寄存器要保存外,只需要保存當(dāng)前任務(wù)進(jìn)程里的live寄存器(置1的寄存器pane),而恢復(fù)則只需要恢復(fù)邏輯寄存器。
對于任務(wù)軟件來說,寄存器的實(shí)現(xiàn)機(jī)制是透明的,因此特別要說明的是WindowsStart和WindowsBase寄存器則完全不需要保存與恢復(fù)。參考如下進(jìn)程切換的環(huán)境保護(hù)核心代碼:
.Lspill_loop:
// Top of save loop.
// Find the size of this call and branch to the appropriate save routine.
beqz a2,.Ldone // if no start bit remaining,we're done
bbsi.l a2,0,.Lspill4 // if next start bit is set,it's a call4
bbsi.l a2,1,.Lspill8 // if 2nd next bit set,it's a call8
bbsi.l a2,2,.Lspill12 // if 3rd next bit set,it's a call12
j .Linvalid_window // else it's an invalid window!
// SAVE A CALL4
.Lspill4:
addi a3,a9,-16 // a3 gets call[i+1]'s sp - 16
s32i a4,a3,0 // store call[i]'s a0
s32i a5,a3,4 // store call[i]'s a1
s32i a6,a3,8 // store call[i]'s a2
s32i a7,a3,12 // store call[i]'s a3
srli a6,a2,1 // move and shift the start bits
rotw 1 // rotate the window
j .Lspill_loop
// SAVE A CALL8
.Lspill8:
……
該代碼的基本思路是通過處理WindowStart寄存器,解析各live窗口的相對偏移,基于調(diào)用棧布局規(guī)范進(jìn)行入棧處理。 當(dāng)任務(wù)切換到其他的進(jìn)程后,這些live窗口可能會(huì)被破壞(相應(yīng)的WindowStart比特清零),這沒有關(guān)系, 當(dāng)進(jìn)程重新切換回來時(shí),如果這些live窗口已經(jīng)在切換過的進(jìn)程恢復(fù)(WindowStart比特置1),則切換回來后無需出棧,程序可正常繼續(xù)執(zhí)行,如果沒有恢復(fù)(相應(yīng)的WindowStart比特繼續(xù)為零),那么該進(jìn)程就可以根據(jù)這個(gè)清零的狀態(tài)位將原先入棧的live寄存器正確恢復(fù)。
本文小結(jié)
Xtensa處理器的寄存器窗口旋轉(zhuǎn)函數(shù)調(diào)用是一種非常巧妙的實(shí)現(xiàn)機(jī)制,通過這一機(jī)制嵌入式軟件可明顯提高性能,并且其alloc異常,多任務(wù)上下文切換等等衍生和應(yīng)用問題也可高效而經(jīng)濟(jì)的解決,其和TIE(Tensilica Instruction Extension),其他諸多可配置選項(xiàng)等等正充分說明了Xtensa架構(gòu)經(jīng)久不衰,廣泛應(yīng)用,煥發(fā)持久生命力的原因所在。