【教學】在 Sony PlatStation3 Cell BE 處理器上編寫高性能的應用程式 - PART 2

這世界非紅即綠,有沒有和平共生的可能-_-!!(還有藍的跟白的怎麼辦)

版主: HBPB, VIML_NCHC

【教學】在 Sony PlatStation3 Cell BE 處理器上編寫高性能的應用程式 - PART 2

文章uno » 週二 3月 10日, 2009年 2:59 pm

在 Sony PLAYSTATION 3 的 SPE 上程式設計

SPE 概覽
Part 1介紹了 Cell Broadband Engine (Cell BE) 處理器及展示了如何在 PS3 上安裝 Linux® 並給出了一個簡短的示例程式。此回將就 Cell BE 晶片的 SPE 做深入的探討(有關程式設計實現 Power processing element (PPE) 的深入討論及對Cell Broadband Engine™ 處理器的 SPE 做深入的探討,研究一下這些 SPE 的底層工作原理。由於 SPE 使用了極為不同的架構,所以要想整體瞭解其原理,很有必要將其放到組合語言中進行審視。這之後,會展示如何在 C 中對它們進行程式設計,但比較而言,組合語言能夠更好地體現該處理器的獨特性。當轉而使用 C 時,您將會理解不同的編碼決定是如何影響性能的。文章重點放在基本的語法和 SPE 組合語言及 ABI(application binary interface 或平臺的函式呼叫約定)的用法上。

正如在PART 1提到的,Cell BE 晶片由 PPE 組成,而 PPE 又有幾個 SPE。PPE 負責運行作業系統、管理資源和輸入/輸出。SPE 負責資料處理任務。SPE 不能直接訪問主存,只能訪問很小一部分(PS3 上 256K)的本機存放區 (LS),LS 是一個獨立的 32 位位址空間。本機存放區位址空間中的位址稱為本機存放區位址 (LSA),而 PPE 上控制過程內的位址則稱為有效位址 (EA)。SPE 包括一個附加的記憶體流控制器 (MFC)。SPE 通過 MFC 在本機存放區、主存和其他 SPE 間傳輸資料。
SPU 是實際運行代碼的 SPE 的一部分。SPU 具有 128 個通用暫存器,每個暫存器都是 128 位寬。但是,SPU 的主要用處並不是進行 128 位值的操作,它是一個向量 處理器。這就是說,每個暫存器都被分為多個較小的值,指令作用於所有的值上。通常,暫存器被視為 4 個 32 位值(32 位被認為是 SPU 的字大小),但也可將暫存器視為 16 個 8 位元值(位元組)、8 個 16 位值(半字)、2 個 64 位值(雙字)或一個單一的 128 位值(四字)。本文中的代碼實際上是非向量的(即標量)代碼,這意味著一次只處理一個值。雖然代碼也使用了一些向量運算,但我們只關注一個暫存器內的一個值,而其他值則被簡單忽略。
本文並不要求您具有組合語言的經驗,當然,如果有相關的經驗會十分有幫助。本文會比較 SPE 和 PPE 的一些特性,但並不要求必須十分瞭解 PPE。

本文中的構建命令假設您已經根據 PART1 中的指導安裝了 Yellow Dog Linux。如果使用的是其他發佈版,一些命令名和標誌可能需要更改。比如,如果您使用的是 1.2 SDK (IBM 已經發佈了 2.0 SDK,但 1.2 SDK 是 YDL 所附帶的)的 IBM System Simulator,那麼您需要將所有的 gcc 引用更改為 ppu-gcc,將所有的 embedspu 引用更改為 ppu-embedspu。根據庫和標頭檔的安裝位置,可能還需要傳遞額外的標誌以查找它們。

一個簡單的示例程式
在開始介紹 SPU 組合語言之前,先來看一個通過遞迴演算法計算 32 位元數的階乘的簡單程式。該演算法的遞迴特性將十分有助於展示標準 ABI。
以下給出了實現相同功能的 C 代碼以便參考:

1. C 版本的階乘程式

int number = 4;
int main() {
printf("The factorial of %d is %d\n", number, factorial(number);
}

int factorial(int num) {
if(num == 0) {
return 1;
} else {
return num * factorial(num - 1);
}
}


現在,介紹一下該程式的組合語言版本,此外,還會對每一行代碼的含義做詳細的解釋。這段代碼看起來比較多,也無需望而卻步,因為其中大部分都是注釋和聲明(階乘函數本身只有 16 個指令)。將如下代碼作為 factorial.s 輸入。

2. 第一個 SPE 程式

###DATA SECTION###
.data

##GLOBAL VARIABLE##
#Alignment is _critical_ in SPU applications.
#This aligns to a 16-byte (128-bit) boundary
.align 4
#This is the number
number:
.long 4

.align 4
output:
.ascii "The factorial of %d is %d\n\0"

##STACK OFFSETS##
#Offset in the stack frame of the link register
.equ LR_OFFSET, 16
#Size of main's stack frame (back pointer + return address)
.equ MAIN_FRAME_SIZE, 32
#Size of factorial's stack frame (back pointer + return address + local variable)
.equ FACT_FRAME_SIZE, 48
#Offset in the factorial's stack frame of the local "num" variable
.equ LCL_NUM_VALUE, 32


###CODE SECTION###
.text

##MAIN ENTRY POINT
.global main
.type main,@function
main:
#PROLOGUE#
stqd $lr, LR_OFFSET($sp)
stqd $sp, -MAIN_FRAME_SIZE($sp)
ai $sp, $sp, -MAIN_FRAME_SIZE

#FUNCTION BODY#
#Load number as the first parameter (relative addressing)
lqr $3, number

#Call factorial
brsl $lr, factorial

#Display Factorial
#Result is in register 3 - move it to register 5 (third parameter)
lr $5, $3
#Load output string into register 3 (first parameter)
ila $3, output
#Put original number in register 4 (second parameter)
lqr $4, number
#Call printf (this actually runs on the PPE)
brsl $lr, printf

#Load register 3 with a return value of 0
il $3, 0

#EPILOGUE#
ai $sp, $sp, MAIN_FRAME_SIZE
lqd $lr, LR_OFFSET($sp)
bi $lr

##FACTORIAL FUNCTION
factorial:
#PROLOGUE#
#Before we set up our stack frame,
#store link register in caller's frame
stqd $lr, LR_OFFSET($sp)
#Store back pointer before reserving the stack space
stqd $sp, -FACT_FRAME_SIZE($sp)
#Move stack pointer to reserve stack space
ai $sp, $sp, -FACT_FRAME_SIZE
#END PROLOGUE#

#Save arg 1 in local variable space
stqd $3, LCL_NUM_VALUE($sp)
#Compare to 0, and store comparison in reg 4
ceqi $4, $3, 0
#Do we jump? (note that the "zero" we are comparing
#to is the result of the above comparison)
brnz $4, case_zero

case_not_zero:
#remove 1, and use it as the function argument
ai $3, $3, -1
#call factorial function (return value in reg 3)
brsl $lr, factorial
#Load in the value of the current number
lqd $5, LCL_NUM_VALUE($sp)
#multiply the last factorial answer with the current number
#store the answer in register 3 (the return value register)
mpyu $3, $3, $5

#EPILOGUE#
#Restore previous stack frame
ai $sp, $sp, FACT_FRAME_SIZE
#Restore link register
lqd $lr, LR_OFFSET($sp)
#Return
bi $lr

case_zero:
#Put 1 in reg 3 for the return value
il $3, 1
##EPILOGUE##
#Restore previous stack frame
ai $sp, $sp, FACT_FRAME_SIZE
#Return
bi $lr
要構建該程式,只需使用 C 編譯器,如下所示:

spu-gcc -o factorial factorial.s

現在,Cell BE 處理器並不直接運行 SPE 程式。實際上,它要求編寫主代碼以便由 PPE 管理資源。然而,如果傳遞給 Linux 的是其自身為 SPE 所編寫的程式而且 elfspe 包也已經被正確安裝,那麼 Linux 就會自動創建一個最簡單的 PPE 過程來為 SPE 管理資源並且會充當 SPE 過程的監管程式。因此,如果 elfspe 包已經安裝,就可以正常運行 SPE 程式,如下所示:

./factorial

如果不能正常運行,請確認 elfspe 包是否安裝正確。
現在來看一下每條指令和聲明的含義。
程式的開頭是一個典型的 .data 聲明。在組合語言中,靜態資料和全域變數與代碼在記憶體中是分開的。您可以在資料部分和代碼部分之間自由切換,但當程式被彙編時,會將所有部分彙編到一起,組成一個單元。.data 可用來切換至資料部分,而 .text 則可用來切換至代碼部分。

資料部分在標記為 number 的空間中存放有我們所要計算的階乘。如果一行的開始有一個字串,後跟一個冒號,就表明可以通過該標記在整個程式中引用隨後的聲明或指令的位址。因此,貫穿該段代碼,只要出現 number,它就引用下一個值的位址。.long 是一個聲明,表明在 32 位元空間存儲值。在本例中,存儲的是數值 4。
注意,在定義 number 之前,需要使用 .align 4 進行對齊。.align 操作告訴編譯器按特定的邊界對齊下一個指令或聲明。.align 4 會將下一個記憶體位置對齊到 16 位元組 (2^4) 的邊界。這十分關鍵,因為 SPU 一次只能載入 16 位元組,並對齊到 16 位元組的邊界。如果要載入自的位址不是一個 16 位元組的邊界,它就會將該位址的最後四位截斷然後再載入,以便使其能夠載入。因而,如果值沒有被正確對齊,它就會在寄存器的任意位置 被載入 —— 而且很可能是您不想載入的位置。通過將它對齊到 16 位邊界,就可以確保它將會被載入進寄存器的前四個位元組。這之後是另一個用來對齊輸出字串起始位置的對齊語句。.ascii 聲明告訴編譯器隨後出現的是一個 ASCII 字串,該字串以 \0 結束。

之後,為堆疊框架定義幾個常量。記住當程式進行函式呼叫(特別是針對遞迴函數)時,它必須在堆疊上存儲返回位址和本地變數。在 C 和其他高階語言中,語言本身會管理運行時堆疊。而在組合語言中,卻需要程式師來顯式處理。在程式開始時,由作業系統為您設置堆疊。堆疊的開始是一些高編號的位址,隨著堆疊框架的添加,逐漸向低編號的地址發展。您必須為每個堆疊分配空間並將合適的值移到此空間。在本程式中,需要兩個堆疊框架大小 —— 一個用於 main,一個用於 factorial。每個堆疊框架都有一個指向前一個堆疊框架的指標(稱為回鏈指標),還有一個當它調用其他函數時用於存放返回位址的空間。這些位址中的每一個的大小都只有一個字(4 位元組),但它們還是會被按 16 位元組對齊以便於載入和存儲(請記住 SPU 只能從 16 位元組對齊的位址載入)。剩餘的空間用來保存寄存器和存儲本地變數。main 的堆疊最小,為 32 位元組,而 factorial 的堆疊則為 48 位元組,原因是 factorial 需要存儲本地變數。要在程式內命名這些數量並使代碼更易於閱讀,可以通過 .equ 操作將符號賦予這些值。這就告訴編譯器將給定的符號和給定的值對等起來。兩個堆疊框架的大小分別賦給了符號 MAIN_FRAME_SIZE 和 FACT_FRAME_SIZE。LR_OFFSET 是返回位址的堆疊框架的偏移量。LCL_NUM_VALUE 是本地變數 num 的堆疊偏移量。上述這些做法目的是使在代碼的主體訪問堆疊框架變得更為清楚明瞭。

在代碼部分,定義了一個函數位址,方法同上述定義全域變數的相同 —— 只需在它們的名稱之後跟上一個冒號。這表明函數位址將是下一個指令的位址。.type 用來告訴連結器該值應該被用作函數,而 .global 則用來告訴連結器該符號在連結時可以在當前檔之外被引用。main 必須被聲明為全域的,因為它是程式的入口點。接下來,我將深入討論一下實際的彙編指令本身。
我會在探討 factorial 函數時再深入介紹此序言(prologue)的意義所在。就目前而言,只需知道它將設置堆疊框架就可以了。
您所看到的第一個實際的指令是 lqr $3, number,其含義是 “load quadword relative”。“quadword” 部分有些多餘,因為 SPU 只允許載入和存儲四字。該指令會把位址 number(編碼為相對於當前指令的位址)內的值載入到暫存器 3。與 PPE 組合語言不同,SPE 組合語言暫存器總是以美元符號開始。這就使得寄存器在代碼中更為醒目。因為所有 SPU 上的暫存器都是 16 位元組長,因而這會將整個 16 位元組的四字載入進暫存器,而我們所關心的只是它的前 4 個位元組。
在暫存器 3 中想要做的是計算此值的階乘。因而,需要將其作為第一個(也是惟一一個)參數傳遞給 factorial 函數。與 PPU ABI 一樣,SPU ABI 也使用暫存器來將值傳遞給函數。寄存器 3 應該保存第一個參數,寄存器 4 應該保存第二個參數,以此類推。所以,載入進暫存器 3 的值就是該函數要用的值。儘管暫存器可以存儲多個值(在本例中,4 個 32 位值),但當參數傳遞給函數時,每個參數值都會在其自己的暫存器內被傳遞。

這就帶來了一個問題:暫存器究竟是用來做什麼的?如果您以前從未用組合語言編過程,暫存器就是處理器為計算值所使用的臨時存儲。由於 SPU 有 128 個暫存器,所以它可以存儲大量臨時值和中間值,而無需像其他架構一樣,必須載入和向記憶體轉存。這就使得程式設計變得更為容易,執行起來也更為迅速。SPU 對暫存器如何使用沒有特殊規定,但 ABI 卻與之不同。以下是 ABI 對 SPU 內的暫存器使用的特殊規定:
SPU ABI 內的暫存器使用

暫存器範圍 類型 用途
0 專用 連結寄存器
1 專用 堆疊指標
2 可變 環境指標(針對有此需要的語言)
3-79 可變 函數參數、返回值和通用的一些使用
80-127 非可變 用於本地變數,必須跨函式呼叫保留


稍後,我會詳細介紹連結暫存器,總的來說,它用於臨時存儲返回位址。堆疊指標給出的是當前堆疊框架的結束位置。環境指標在大多數語言中並未使用。所有標有可變的暫存器都可以在函數內自由改變。但這意味著當函數進行函式呼叫時,所有在可變暫存器中的值都有可能會被重寫。所有標有非可變 的暫存器必須在使用前將它們之前的值保存起來並在從函式呼叫返回之前加以恢復。這就讓您有一組暫存器可跨函式呼叫保留。但,這類暫存器使用起來比較麻煩,因為需要用代碼實現先前值的保存和恢復。返回值返回至暫存器 3 。

由於需要對數值 4 進行階乘,它進入的是用來保存第一個參數的暫存器 3。然後需要使用 brsl $lr, factorial 對函數進行分支。brsl 代表 “branch relative and set link”,用來分支到函數的入口點並將連結暫存器 (LR) 設置為返回位址的下一個指令。注意在使用 brsl 時,需要為此暫存器指定 $lr。它是 $0 的別名。也請注意必須要顯式指定連結暫存器。SPU 沒有特殊的暫存器。連結暫存器只在約定的意義上有一些特殊 —— SPU 組合語言允許在您所選擇的任何暫存器內設置連結。但就大多數目的而言,這將是 $lr。

計算了階乘之後,現在需要用 printf 將其列印出來。printf 的第一個參數是輸出字串的位址。因而,首先需要將結果從暫存器 3 移到暫存器 5(暫存器 4 存有原始數值)。隨後需要將位址 output 移到暫存器 3。ila 是載入靜態位址的特殊載入指令,在本例中用來將輸出字串位址載入到 3。它載入的是 18 位元的無符號值,這是 PS3 上的本機存放區位址的首選大小。最後,原始值載入至暫存器 4。printf 函數通過 brsl $lr, printf 調用。請注意,printf 並不在 SPE 上執行,原因是 SPE 不能輸入和輸出。這實際上會轉入到一個 stub 函數,該函數可以停止 SPE 處理器、發信號給 PPE,而由 PPE 實際執行函式呼叫。這之後,控制權再交回給 SPE。
該段代碼的尾聲(epilogue)將在分析 factorial 代碼時再作討論,但總的來說,它的作用是結束堆疊框架並返回到先前的函數。
在討論 factorial 函數之前,先要瞭解堆疊框架的佈局。以下是 ABI 的堆疊框架佈局:

包含的部分 大小 開始堆疊偏移量
暫存器保存區 可變(16 位元組的整數倍) 可變
本地變數空間 可變(16 位元組的整數倍) 可變
參數列表 可變(16 位元組的整數倍) 32($sp)
連結暫存器保存區 16 位元組 16($sp)
回鏈指標 16 位元組 0($sp)


回鏈指標指向前一個堆疊框架的回鏈指標。連結暫存器保存區存有被調用函數(而非當前函數)的連結暫存器內容。參數清單中的參數是該函數發送給其他函式呼叫的參數,而非其自身的參數。但是,與 PPE 不同,這只用在參數的數量大於參數可用的暫存器數量的情況下(這種場景並不常見)。本地變數空間用作該函數的通用存儲空間,暫存器保存區用於保存函數所使用的非可變暫存器的值。

所以,在這個函數中,我們使用了回鏈指標、連結暫存器保存區和一個本地變數。這樣一來,框架的大小就為 16 * 3 = 48 位元組。正如我先前提到的,LR_OFFSET 是堆疊末端到連結暫存器保存區的偏移量。LCL_NUM_VALUE 是堆疊末端到本地變數 num 的偏移量。

序言 為函數設置堆疊框架。在序言中,所要做的第一件事情是保存連結暫存器。由於您尚未定義自己的堆疊框架,所以偏移量是由調用函數的堆疊框架的末端算起的。請記住連結暫存器存儲在調用函數的堆疊框架內,而非函數自身的堆疊框架。所以十分有必要在保留堆疊空間之前先保存它。這可以通過所謂的 D-Form 存儲(D-Form 是一種指令格式)加以實現。在Assembly language for Power Architecture, Part 2 中可以找到有關 PPU 指令格式的概覽(SPU 格式與 PPU 格式十分接近)。存儲指令的代碼是 stqd $lr, LR_OFFSET($sp)。 stqd 代表的是 “store quadword D-Form”;D-Form 指令將暫存器作為第一個運算元(它是所要存儲或載入到的暫存器),將一個常量和暫存器的組合作為第二個運算元。在暫存器加上一個常量是為了計算用於載入或存儲的位址。 其他常見的格式還有 X-Form(接受兩個加在一起的暫存器)和 A-Form(可保存一個常量或一個常量相對偏移量位址)。所以在這個指令中,$sp 是堆疊指標(它是 $1 的別名)。運算式 LR_OFFSET($sp) 計算 LR_OFFSET 的值與 $sp 的和,並使用它作為目的地址。所以該指令會將連結暫存器(存有返回位址)存儲到調用函數堆疊框架的恰當位置。

接下來,當前堆疊框架指標會被存儲為指向下一個堆疊框架的後向指標,雖然尚未建立堆疊框架(這是通過負的偏移量實現的)。與 PPU 不同,SPU 沒有原子存儲/更新指令,所以要確保後向指標是一致的,必須在移動堆疊指標之前存儲後向指標。最後,移動堆疊指標來通過指令 ai $sp, $sp, -FRAME_SIZE 保留所有所需的堆疊空間。ai 代表的是 “add immediate”,用來向寄存器添加一個立即方式值並將其存儲回寄存器。它將第二個運算元內的寄存器與第三個運算元內的常量加在一起,並將結果存儲在第一個運算元內指定的寄存器中。大多數指令都遵循類似的格式,結果存儲在第一個運算元內指定的暫存器中。

注意 ai 指令是個向量運算。SPU 暫存器均為 128 位寬,但我們的值只有 32 位長。暫存器邏輯上被視為多個值,這些值被同時操作。ai 指令實際上將暫存器視為 4 個 32 位值,每一個都添加了 -FRAME_SIZE,然後它們均被存儲回目的暫存器。SPU 的首選值大小為 32 位字,但也支持其他的大小,包括位元組、半字和雙字。如果運算元的大小未在指令中指定,這就意味著運算元的大小並不十分重要(例如邏輯指令)或者它的大小為 32 位。如果指令中包含字母 b 就表明是位元組,如果包含字母 h 就表明是半字,如果包含字母 d 就表明是雙字,雙字通常只用在浮點指令中(通常指令裡的 d 指的是 D-Form 格式的定址,而非雙字)。但在本例中,我們只關心暫存器裡的第一個字。其他值在 ABI 中的意義不大。

接下來,通過 stqd $3, LCL_NUM_VALUE($sp) 將第一個參數複製到本地變數。之所以需要這麼做是因為參數會在遞迴函數調用上被截斷,而以後卻還需要訪問它。

接下來,通過 ceqi $4, $3, 0 將暫存器 3 與數值 0 做立即模式對比並將結果存儲在暫存器 4 中。注意如果是 PPU(和與之相關的大多數處理器),一種特殊用途的暫存器可用來保存條件結果。但,如果是 SPU,結果會存儲在一種通用的暫存器內 —— 在本例中就是暫存器 4。請注意這是一個向量處理器,所以並非將暫存器 3 與數值 0 作實際對比,相反,所對比的是暫存器 3 的每一個字與數值 0。所以答案有 4 個,而我們只關心其中的一個。結果按如下方式存儲:如果此字的條件為真,那麼目標字的所有位元都將被設置;如果此字的條件為假,那麼目標字的所有位元都將被清除設置。所以此指令會有 4 個結果,根據比較結果的不同,它們或者全是 1 或者全是 0。

下一個指令是 brnz $4, case_zero。brnz 代表的是 “branch relative if not zero”。暫存器 4 是前一個比較的結果,所以這個指令是用來檢查前一個比較結果是 0 還是非 0 的。如果之前是否為零的測試的結果為真,那麼結果暫存器將是非 0(為真,所有位均被置 1)。注意前兩個指令可以合併為一個指令 (brz $3, case_zero),原因是所測試的只是是否為零,我之所以將它們分開為兩個指令是為了能夠讓您更好地理解比較和分支是如何工作的。

如果這些比較的結果有的為真,而其餘的為假,那麼又該如何呢?由於所處理的是 4 個 32 位的值而非一個 128 位的值,所以針對不同的值就可能會有不同的結果。如果結果不同,是否需要進行分支處理呢?幾個 SPU 指令只能處理這些暫存器值中的一個。在這些情況下,所使用的值是在暫存器的首選槽中的那個。對於 64 位值來說,就是暫存器的前半部分;對於 32 位值來說,就是暫存器的第一個字;對於 16 位值來說,就是暫存器的第二個半字;對於 8 位的值來說,就是暫存器的第四個位元組。基本上,第一個字就是首選字,而其他字對齊到最低有效位元組或半字上。當進行條件分支、向函數傳遞值、從函數返回值以及其他操作時,首選槽內的值才是關鍵的。在本例中,假設在函數中傳遞的值就處於暫存器的首選槽內。而且,.data 中的 alignment number 將會被載入進首選槽內。所以,只要值位於暫存器的首選槽之內,分支就會正常發生。

現在假設所處理的暫存器 3 中的數值為非零,這就意味著需要進行遞迴的步驟。遞迴的 C 代碼是 return num * factorial(num - 1)。最裡面的計算要求遞減 num 並將其作為參數傳遞給下一個 factorial 調用。num 已經在暫存器 3 中,所以只需遞減它即可。所以可以這樣進行立即模式添加:ai $3, $3, -1。現在,調用下一個 factorial。要根據 SPU ABI 調用函數,所需做的是將參數放入暫存器,然後調用 brsl $lr, function_name。在本例中,第一個也是惟一一個參數已經載入進暫存器 3。所以,需要發起 brsl $lr, factorial。正如我之前提到過的,brsl 代表的是 “branch relative set link”;目標位址被編碼為相對位址,返回位址則被存儲在指定暫存器的首選槽內,而控制權將由目的地址獲得,在本例中它就回到了. factorial 函數的開始部分。

至此,階乘的結果就應該已經存在於暫存器 3 內了。現在您想用考慮中的當前值去乘該結果。所以必須將其載入回來,因為它在函式呼叫中被截斷過了。lqd 代表的是 “load quadword D-Form”;第一個運算元是目標暫存器,第二個運算元是要載入的 D-Form 位址。因此 lqd $5, LCL_NUM_VALUE($sp) 會將之前堆疊中保存的那個值讀取到暫存器 5 內。

現在需要用暫存器 3 乘以暫存器 5。這可以通過 mpyu 指令(無符號相乘)實現。mpyu$3, $3, $5 用暫存器 3 乘以暫存器 5 並將結果存儲在所列出的第一個暫存器內,即暫存器 3。現在,SPU 上的整數相乘指令多少有點問題,特別是有符號乘法(使用 mpy 指令)。問題在於乘法指令的結果可能會有其運算元的兩倍之長。兩個 32 位數相乘的結果實際上是一個 64 位的值!如果的確如此,那麼目的暫存器將會有源暫存器的兩倍之長。為了解決這一問題,乘法指令只使用每個 32 位的最低有效的 16 位以便結果能夠放進整個 32 位暫存器內。所以,當乘法將源暫存器視為 32 位寬時,它只使用了其中的 16 個位。因此,如果值的長度超過 32 位,該值就會被截斷。而且,如果進行的是有符號乘法,符號還有可能會由於截斷而改變!因此,要成功執行乘法指令,源值就需要為 16 位元寬,但被存儲在 32 位元暫存器內(如果它被符號擴展到 32 位,對於乘法運算來說關係並不大)。這極大地限制了階乘函數的可能範圍。注意浮點乘法沒有這樣的問題。

現在結果出來了,在暫存器 3 內,這也是它應該存在於的位置。剩下要做的只是恢復先前的堆疊框架並返回。所以您只需通過使用 ai $sp, $sp, FRAME_SIZE 將堆疊框架大小加到堆疊指標來移動堆疊指標即可。然後使用 lqd $lr, LR_OFFSET($sp) 恢復連結暫存器。最後,bi $lr(branch indirect)分支跳轉到在連結暫存器內指定的位址(返回位址),進而從函數返回。

基線條件(當函數參數為零時作何處理)十分簡單。階乘(0)的結果為零,所以只需通過 il $3, 1 將數值 1 載入到暫存器 3 中。之後再恢復堆疊框架並返回。但由於基線條件並不調用任何其他函數,所以無需從堆疊框架載入連結暫存器 — 值還在原處。
這就是該函數的工作原理。請注意在 SPE 上編寫深度遞迴函數有些問題,原因是在 SPE 上沒有任何堆疊溢位保護,而且本機存放區也很小。


結論
本文涵蓋了在安裝了 Linux 的 PLAYSTATION 3 的 Cell BE 處理器上進行組合語言程式設計的一些主要概念。

參考資料
• 每一條 SPU 指令的詳細資訊均可在 SPU Instruction Set Architecture Reference 指南找到。但,通常您只需參考 SPU Assembly Language 指南中的簡短概括就可以了。事實上,要更好地瞭解 SPU 的功能,我建議您詳細閱讀組合語言指南。它既簡短又富含信息。如果指令不能正常工作,您可以查看 ISA 文檔中的完整定義。
• 有關 ABI 的更多細節,參見 SPU ABI documentation以及 Linux extensions to the ABI
• 有關 Cell BE 的權威性資訊資源,可參考 Cell BE Handbook
• 從 Barcelona Supercomputing Center下載 SPE Runtime Management Library Version 2.2

關於作者
Jonathan Bartlett 是 Programming from the Ground Up 一書的作者,這本書對使用 Linux 組合語言的程式設計進行了簡介。他是 New Medio 的開發技術總監,為客戶開發 Web 應用程式、視頻應用程式、kiosk 應用程式和桌面應用程式。
uno
Freshman
Freshman
 
文章: 7
註冊時間: 週二 10月 5日, 2004年 1:47 pm

回到 + 高效能運算其他討論

誰在線上

正在瀏覽這個版面的使用者:沒有註冊會員 和 2 位訪客









cron