NO IMAGE

PowerPC Figure – PPC入門與優化

By Skywind(2007)
http://www.joynb.net/blog/

背景介紹

PowerPC於1991年IBM/MOTO/APPLE研製,大量應用於伺服器(AIX / AS400系列及蘋果系列伺服器),家用遊戲機(PS3, Wii, XBOX, GameCube),以及嵌入式(僅次於Arm/x86排第三)。PowerPC核心在於開放系統軟體標準,其應用範圍僅次於x86,是除去x86外最值得開發者瞭解的體系。

不需要寫出非常高效的程式碼,但要了解基本效率原則;不需要大規模開發PPC程式,但需要時能寫幾段、除錯時能看懂哪裡錯了。本文將從對比x86入手,引入RISC及PowerPC體系概念,向讀者介紹該體系指令集,常用優化方法和交叉編譯環境及模擬器的搭建等內容。

PowerPC基礎知識

1990年 IBM時任總裁 Kuehler說服了摩托羅拉公司和蘋果公司與IBM公司共同參與制訂 PowerPC體系結構。為了讓 AS/400也成為其中一員,1991 / 1992年羅徹斯特實驗室開始為 AS/400擴充並制訂PowerPC的64位結構。
                                                                                                        
—-《羅徹斯特城堡》

大部分CPU指令集都可以分為:資料讀寫、數值計算、流程控制與裝置管理四個部分,其中裝置管理不屬於介紹範圍。開放系統軟體標準在於硬體/軟體只要符合該標準都能在 PowerPC下執行,也就是說先今有大量CPU雖然實現不一,但是他們在標準上都支援了 PowerPC體系,使得開發與介面更為方便。

PPC使用RISC(精簡指令集),指令字長都是32bit,一條Intel指令往往可以由多條 PPC指令組合表示。Endian一般都是可調的,預設使用BE(Big Endian),同時PPC沒有棧,也就是說應用程式需要自己實現相關操作。

常用術語介紹

image

image

常用暫存器

image

問題1:如何載入32位立即數?

在PPC下如何載入32位的立即數呢?RISC下PPC的每條指令都是4個位元組定長。除去指令與暫存器引數編碼,只有剩下16bit的長度用來描述立即數,比如立即數載入指令 LI:

LI rD, SIMM

clip_image002

立即數SIMM欄位僅16位,如何表示32位?

答案:只有分兩次載如,使用LIS(立即數載入並左移)和ADDI(立即數加法)分兩次載入。因此32bit的立即數載入需要分兩次完成:

LIS R3, 0x1122           載入並左移16位
ADDI R3, R3, 0x3344  再加上低16位

兩條指令後,R3完成對 0x11223344的載入

特性:不一樣的子程式呼叫

•    f1:             子程式入口
•       blr          返回(跳轉到LR地址)
•    start:
•       bl f1       呼叫f1(跳轉並儲存地址到LR)
•       li r1, 1    設定r1 = 1
•       li r3, 1    設定r3 = 1
•       sc           系統呼叫:結束程式

PPC使用了LR暫存器(Link Register)來完成:在bl指令跳轉前,下條指令(li r1,1)的地址會被儲存到LR而執行到f1中的blr時,系統會跳轉到LR所表示的地址,完成返回。

資料讀寫指令 

image

注意:LBZ R3, 0(R2)與LHZ R3,10(R2)並不全等同於MOV AL,[EBX]和MOV AX,[EBX 10]。前者將位元組和半字載入到R3時順便清空了高位,而後兩條指令載入資料到EAX並不會清空高位。

第一個程式:Hello World !!

把下面的程式儲存成 hello.s,並交叉編譯:

# powerpc-eabi-as -gstabs hello.s -o hello.o
# powerpc-eabi-ld hello.o -o hello

•    .global _start                         /* 請將本程式儲存成 hello.s */
•    .data                                      /* 後面將講解如何在虛擬機器中除錯 */
•    msg: .asciz “Hello, PowerPC World !!/n”
•    len = . – msg
•    .text                                       /* 程式碼部分開始 */
•    _start:
•        li %r0, 4                             /* r0 = 4   */
•        li %r3, 1                             /* r3 = 1   */
•        lis %r4, [email protected]                /* r4 = msg(high) << 16 */
•        addi %r4, %r4, [email protected]       /* r4 = r4 msg(low)     */
•        li %r5, len                          /* r5 = len */
•        sc                                      /* system call (print) */
•        li %r0, 1                            /* r0 = 1 */
•        li %r3, 1                            /* r3 = 1 */
•        sc                                      /* system call (exit) */

完成交叉編譯後用 qemu模擬器執行:
# qemu-ppc hello
Hello, PowerPC World !!

關於如何在x86環境下交叉編譯與除錯,詳細見第三部分的的“PowerPC編譯除錯”。

特殊暫存器操作

image

問題2:沒有棧僅靠LR如何遞迴?

•    f1:               
•       mflr r2                     儲存LR中記錄的地址到r2
•       stw r2, -8(r1)          記錄r2的數值到MEM[r1-8]處
•       addi r1, r1, -60        r1後移60個位元組,完成進棧操作
•       ….
•       addi r1, r1, 60        r1前移60個位元組,準備出棧
•       lwz r2, -8(r1)          讀出老的LR值到r2
•       mtfr r2                    將r2的內容複製到LR
•       blr                           返回(跳轉到LR地址)
•    start:
•       ….

雖然PPC沒有直接提供棧相關指令(PUSH/POP/CALL/RET),應用程式卻常用R1來模擬棧指標,實現多層呼叫時對LR的記錄與恢復。

數值計算指令

image

特性:RISC的“載入/儲存”體系

RISC決定了PowerPC使用載入/儲存體系,即所有計算都是在暫存器中完成,而不是在主存中。除去載入/儲存指令,所有操作都是針對暫存器的(少部分立即數),執行消耗週期相同且無須訪問主存。

CISC體系(如x86)幾乎所有操作都可對記憶體、暫存器或兩者同時進行操作。傳統上,處理器被設計成適應更加複雜的指令。

RISC是基於 “最簡單的計算機指令是最經常被執行的” 這一研究基礎。用簡單指令的組合來執行復雜的指令。這樣處理器的時間安排能以較簡單和快速運算為基礎,能在給定時鐘速度下執行較多指令。

現代的CISC處理器將自己的指令轉換成了內部使用RISC格式,以實現更高的效率。

PowerPC 流程控制

IBM公司70年代首次開發RISC,但知道多年以後才應用到IBM公司的系統中。儘管第一款IBM RISC處理器早在1986年就被應用到 IBM PC-RT中,但直道 IBM 1990年推出RS/6000伺服器時,該技術才開始受到重視。
                                                                                                 
《羅徹斯特城堡》


條件暫存器

CR(Condition Register)一共32位,從低位到高位被分成 CR0-CR7八段,每段四位。每個四位的CRn從低到高分別是:LT(小於標誌)、GT(大於)、EQ(等於)和SO(溢位)比較指令或條件跳轉指令均可指明具體操作哪個 CRn,由此可以同時判斷多個條件。整數計算預設更改CR0,浮點數計算預設更改CR1。

舉例:求絕對值

•    _ABS:
•       cmpwi %r3, 0              /* 引數R3與0比較     */
•       bgt greater_than        /* 如大於就跳轉     */
•       neg %r3, %r3             /* 取負值: R3 = Not(R3) 1 */
•    greater_than:
•       blr                               /* 返回 (從LR取出地址並跳轉)    */
•    _start:
•       li %r3, 123                  /* 載入立即數 123 */
•       bl _ABS                       /* 呼叫 _ABS (跳轉前記錄地址到LR)*/
•       li %r0, 1                     
•       li %r3, 1
•       sc                               /* 系統呼叫:結束程式 */

比較:cmpw rA, rB (比較有符號),cmpwi rA, IMM(立即數比較),cmpwl rA, rB(無符號)。跳轉:blt addr (小於跳轉),bgt addr(大於跳轉),beq addr(等於跳轉)
類似:bne(不等),ble(小於等於),bge(大於等於)

資料比較指令

image

特性:多條件暫存器

判斷相同 – 老程式碼:
    cmpw r3, r4
    beq _branch_1

判斷相同 – 新程式碼:
    cmpw cr4, r3, r4
    beq cr4, _branch_1

可以在比較和跳轉命令第一個引數指明所使用的條件暫存器,如果不寫的話,預設 CR0。由此我們可以用更多條件暫存器同時判斷若干條件,再用cand/ cor/ cxor複合運算。

 
數值比較 – 有符號

CMP crfD, L, rA, rB
   
a <- EXTS(rA) 擴充套件符號到a (如果無符號比較 cmpl 則直接 a = rA)
    b <- EXTS(rB) 擴充套件符號到b (如果無符號比較 cmpl 則直接 b = rB)
    If a < b then c = 0b100 設定小於標誌
    else if a > b then c = 0b010 設定大於標誌
    else c = 0b001 設定等於標誌
    CR[4 * crfD – 4 * crfD 3] <- c || XER[SO] 記錄4位狀態

舉例說明:
cmpw rA, rB 比較 rA, rB的低32位結果存cr0 (同 cmp 0, 0, rA, rB)
cmpd rA, rB 比較 rA, rB的全64位結果存cr0 (同 cmp 0, 1, rA, rB)
cmpwc r3, rA, rB 比較 rA, rB的低32位結果存cr3 (同 cmp 3, 0, rA, rB)

 
轉移指令

指令B(branch)是絕對地址無條件跳轉,BA是相對地址無條件跳轉,BL是跳轉前將下一條指令的地址記錄到LR(可以用blr跳轉到LR所指地址),BLA是相對地址跳轉,並將下條指令地址記錄地址到LR。

image

條件跳轉中 BI用來表示具體需要測試的條件暫存器CR的位,BO用來表示測試方式,比如是測試大於/小於/等於還是測試計數器CTR的值,故此blr等同 bclr 0b10100, 0。

特性:指令的別名

PowerPC指令助記符有大量別名:
比如 CMPW rA, rB 其實是 CMP 0, 0, rA, rB
比如 BEQ addr 其實是 BC 0xC, 2, addr
比如 BLR 其實是 BCLR 0x14, 0

image
轉移指令如果沒有指明條件暫存器,則預設使用CR0(CR的0-3位);bca相對於bc或者ba相對於b,他們的指令碼都相同,只是AA位(是否用絕對地址)為1 ;bcl相對於bc或者bl相對於b,他們的指令碼亦同,僅LK位(是否記錄地址)為1。

問題3:如何跳轉到R3所記錄地址

BC, B 等都是用相對地址跳轉的。如何實現類似C裡面的函式指標呼叫?

答案:需要用到LR暫存器:
        mtlr r3 將R3的值儲存到LR
        blr 跳轉到LR所指位置

 

條件轉移原理(瞭解)

BC BO, BI, target_addr (AA=0, LK=0)
    m <- 32
    If BO[2]=0 then CTR <- CTR – 1                   如果BO[2]==0則計數器自減
    ctr_ok <- BO[2] | BO[3]                               判斷計數器條件
    cond_ok <- BO[0] | (CR[BI] == BO[1])         判斷條件暫存器某位是否符合需求
    If ctr_ok & cond_ok then                             如果兩個條件同時成立則執行跳轉
        if AA then NIA <- iea EXTS(BD || 0b00)    如果使用絕對地址
        else NIA <- iea CIA EXTS(BD || 0b00)   如果使用相對地址
        if LK then LR <- iea CIA 4                     判斷是否記錄指令地址到LR

注:NIA – 新指令地址;CIA – 當前指令地址;EXTS – 擴充套件正負符號;AA – 是否使用絕對跳轉的標誌;LK – 是否用LR儲存下條指令地址(CIA 4)。

BO欄位常用操作碼:
BO=00100 如果條件成立(CR[BI]==0)則發生跳轉
BO=01100 如果條件不成立(CR[BI]==1)則發生跳轉
BO=10100 直接跳轉

問題4:求絕對值指令原理

下面程式碼請直接用CMP/ BC兩條指令實現(提示:參考前面關於BC/CMP兩條指令原理)

cmpw r3, r4
beq _branch_1

答案(瞭解即可):
cmp 0, 0, r3, r4
bc 0b01100, 2, _branch_1

其實在實際開發中都是直接書寫替代的別名

問題5:PowerPC與x86的編碼區別

PPC指令系統比x86/arm晦澀,同時RISC載入常數等指令等要分兩次;PPC大部分指令都是三運算元,而x86幾乎都是雙運算元;PPC指令比x86更細緻精準,同樣程式PPC程式碼要比x86短。

示例:演示遞迴 – 求階乘

接下來的程式將通過求階乘演示遞迴。之前曾經說過:PPC沒有棧,故而實際遞迴時需要儲存現場與返回地址的工作交給了應用程式,我們一般使用R1來模擬棧指標:

• _factoria:                             /* 求階乘,輸入R3,返回R3 */
•     mflr %r2
•     stw %r2, -8(%r1)
•     addi %r1, %r1, –60
• _factoria.start:
•     cmpwi %cr0, %r3, 1
•     bgt _factoria.n1               /* branch to n1 if r3 > 1 */
•     li %r3, 1                          /* return 1 (if r3 <= 1) */
•     b _factoria.exit
• _factoria.n1:
•     stw %r3, 8(%r1)              /* save r3 to stack */
•     addi %r3, %r3, –1           /* r3 = r3 – 1 */
•     bl _factoria                      /* call _factoria */
•     lwz %r11, 8(%r1)            /* r11 = [r1 8] (old r3) */
•     mullw %r3, %r3, %r11    /* r3 = r3 * r11 */
• _factoria.exit:
•     addi %r1, %r1, 60           /* restore stack point */
•     lwz %r2, -8(%r1)             /* resotre LR */
•     mtlr %r2
•     blr

根據作業系統的不同,規定了不同的ABI(應用程式二進位制介面),詳細定義了棧如何操作,引數如何傳遞等關鍵介面規範,開發時需注意檢視。

PowerPC 編譯除錯

交叉編譯(在一個平臺下編譯另一個平臺執行的程式)需要一臺Unix機器或者Cygwin,下載並重新編譯binutils即可:

# wget http://ftp.gnu.org/gnu/binutils/binutils-2.18.tar.bz2 
# tar -jvxf binutils-2.18.tar.bz2
# cd binutils-2.18
# ./configure –target=powerpc-linux-eabi
# make all install

模擬器QEMU最好在Linux環境中使用(才能支援使用者模式模擬)

# apt-get install qemu (debian直接安裝)

其他平臺需要手工編譯。所謂使用者模式在於不需要模擬整個PPC作業系統,而是模擬執行PPC-Linux下二進位制可執行檔案,PPC程式的系統呼叫將會轉化為本機 Linux的系統呼叫。所以我們不需要再在QEMU下重新安裝一個 Mac OS X之類的系統:

# powerpc-linux-eabi-as -gstabs hello.s -o hello.o
# powerpc-linux-eabi-ld hello.o -o hello
# qemu-ppc ./hello
Hello, PowerPC World !!
#

上面是使用第一章中的 hello.s進行編譯,並在虛擬機器中執行以後的效果。

PowerPC 指令優化

首先需要認識到PPC體系下的CPU種類繁多,對具體需要優化的環境需要詳細瞭解。例如流水線的型別如何?以往習慣了x86的思維後,我們都以為主頻越高越好,流水線越長越好。其實不然。越長的流水線,分支預測失誤代價越大,單條指令通過的時間越長。因此如果單算執行一條指令的速度,流水線長20的P4 2.0GHz 速度還沒有流水線長 10的賽揚 1.2GHz快,而且Intel僅僅為了增加並行處理部分指令的機會而增加流水線長度,同時又要保持無法並行時的處理速度,為此只有增加主頻,帶來功耗的上升,以及分支預測失誤的昂貴代價。

CPU需要根據科學型還是商務型及多媒體型來採取不同的設計優化策略:比如科學型計算機多用小而密集的迴圈計算,因此普通的分支預測命中率高(90%以上),因為大部分跳轉都是向上跳轉的迴圈,而商務型卻只有50%的命中(大部分無規律的邏輯),多媒體型不但計算密集,而且記憶體吞吐量大。不同應用的CPU設計有所不同,優化也不同。

PowerPC以AS/400為例,多為短流水線體系,分支預測失誤的代價更少,且主頻更低(功耗更小),採用更“聰明”的預測機制,大部分主頻很低,但速度驚人。以上流水線設計的兩派技術體系爭鬥了十多年,各有千秋,很多主頻比Intel低很多的PowerPC的晶片,卻表現出了更優越的效能,而市場上大部分人只喜歡盲目追求主頻,這是一個誤區。

1. 指令結對原則

在類PPC405/440的系統中,指令被分為下面三類的其中之一,類405/440系統能夠在單一週期同時執行兩條不屬於同一種類的指令:

(1) 資料的載入與儲存
(2) 任意義下處理:設定CR暫存器進行比較,分支,乘除SPR暫存器更新
(3) 其他種類操作:非SPR/CR暫存器更改,算術與邏輯

如果兩條鄰接的指令屬於同一類別,那麼第二條必須等待第一條處理完以後才能被排程,這樣做就浪費了時鐘週期;而如果鄰接的兩條指令屬於上述不同類別且無結果依賴,那麼兩條指令能夠被同時排程,這樣做就能獲得比較高的效率。

這與我們x86下優化的經驗並部相同,在x86的流水線中只要無倚賴的程式碼基本都可以並行執行,比如我們可以並行處理若干無相關的載入或計算,從而在x86下達到較高的效率:

mov eax, [esi 10]           三條無依賴載入能並行
mov ebx, [esi 14]
mov ecx, [esi 18]
add eax, edx                     三條無依賴加法能並行
add ebx, edx
add ecx, edx

而這樣在大多數類405/440的PPC下卻是有問題的。整數計算屬於同一類別,鄰接的無依賴計算指令不能如現代x86體系中得到同時執行;載入指令也相同,而整數計算和載入混合卻能很好的並行排程:

lwz r3, 0(r10)                此載入和下一條加法無依賴,且屬不同類別
add r4, r5, r5                加法能和前一條載入並行執行
lwz r6, 4(r10)
add r7, r8, r8
lwz r9, 8(r10)
add r3, r4, r4

因此如果我們的潛意識裡過分熟悉x86優化方式,進而在用C開發的時候也會體現出來的話,可以說,這樣的C程式碼在PPC下很難發揮效果的,即便編譯器能優化,也需要給編譯器留有優化的餘地。

2. 載入依賴原則

當資料從快取被載入到某暫存器的時候,需要數個週期以後資料才能被其他指令所使用,一條使用到剛被載入資料的指令需在資料被載入後第三個週期才能被排程。故在資料被載入與被使用兩條指令之間的數個週期內形成了一段非常有效的優化區間,我們用來放置其他一些指令。載入與處理命令之間能放置的指令數決定於這些指令的種類,決定於他們是否能夠結對並行處理,最大能有五條指令的優化空間。

image 

在載入與使用命令之間能夠並行插入的指令數取決於這些指令的混合方式,最少我們也可以插入兩條指令進行優化。參考下面的指令,stw和lbz兩條處於n 1和n 2週期的指令不能被結對並行執行,因為他們屬於同一類別的指令。

image

雖然大部分載入指令只有一個目標暫存器,但是需要注意一些帶“更新”功能的載入指令,諸如lwzu它除了更新目標暫存器外,同時也會更新源暫存器,此時對源暫存器的使用也必須等待該指令被完成執行以後才行。

3. 指令依賴原則

同x86類似,有上下文依賴的指令不能同時被排程。在兩條指令中,如果第一條指令更新的暫存器會被第二條指令用到,那麼這兩條指令不能被同時排程,利用這個特性我們將依賴關係的兩條指令分開,並且插入至少一條指令完成優化。

比如我們在週期n用add r4,r5,r6更新了R4暫存器,那麼就只能到週期n 1才能排程到使用R4暫存器的srawi r7,r4,4指令。在第一條指令的第n週期,沒有指令能夠與之並行執行而造成了浪費,所有正確的方式是在這兩條指令之間加入一條無相關的指令,這樣便能和add指令進行結對而得到同時排程,充分利用了時鐘週期。

4. 快取優化原則

快取優化的方法基本和x86相同,這裡再對快取優化的原理做一下說明,即處理器要使用主存某地址的資料時,需要先將他們載入到快取,然後才能處理,最後更新回主存去。根據前面的載入依賴原理的闡述,我們知道從快取載入到暫存器需要三個時鐘週期後才可以使用該暫存器,然而如果該地址的資料不在快取中的話,前面就需要加上更多週期的等待週期,讓資料先載入到快取,最終再經過三個週期的等待後才能使用該資料。

為了降低直接從外存到快取昂貴代價,現代的處理器都增加了一條預取指令,在x86下叫做prefetch而在PowerPC中叫做DCBT(Data Cache Block Touch):

DCBT rA, rB – 將(rA rB)所表示的地址資料預取到快取

該指令將提前告訴CPU將用到哪塊記憶體,CPU提前將該記憶體讀入快取,幾個週期以後等到用時就該指令已經在快取中了。用dcbt同x86的 prefetch指令,現代CPU的主要瓶頸在主存到快取之間,高效使用快取是優化的關鍵。

下面是一段x86下比memcpy快1.6倍的記憶體拷貝程式碼,原因在於對快取的使用上,先mm0-mm7順序載入,再順序寫入,讀入到mm0與從mm0寫入中間間隔7條指令,讓CPU有足夠的時間載入,同時使用了預取。

loop:
    prefetchnta [esi 256]     預取 esi 256地址的資料 
    movq mm0, [esi 0]         載入 esi 0 到 mm0
    movq mm1, [esi 8]
   
    movq mm7, [esi 56]
    movntq [edi 0], mm0      寫入mm0到 edi 0
    movntq [edi 8], mm1      使用穿透快取方式寫入
   
    movntq [edi 56], mm7
    add esi, 64                        指標後移 64位元組
    add edi, 64                        指標後移 64位元組
    dec ecx
    jnz loop                             判斷計數器並迴圈跳轉

而如果在PowerPC下寫記憶體拷貝,我們就不能並列寫若干載入指令,因為大部分PPC不能並行處理載入,我們需要將載入與儲存交叉寫:

loop:
    dcbt r12, r11                     預先載入 (r12 r11) 處記憶體到快取
    lwzu r3, 4(r11)                  載入記憶體到r3並且移動指標
    lwzu r4, 4(r11)
    lwzu r5, 4(r11)                  爭取載入指令與寫入指令並行執行
    stwu r3, 4(r10)                  寫入資料從r3並且移動指標
    lwzu r6, 4(r11)
    stwu r4, 4(r10)
    lwzu r7, 4(r11)
    stwu r3, 4(r10)                  利用多暫存器的特點寫下去
    lwzu r8, 4(r11)
    ….
    addi r9, 0, –1                     減少計數器
    cmpwi cr4, r9, 0
    blt loop                              計數器未到就跳轉

該段程式有三處優化,首先是快取預取,dcbt在每個迴圈預先取後面的內容,其次是充分利用PPC多暫存器的特點,最後是讓載入和儲存指令交叉進行充分的發揮並行作用。

如果你所使用的PowerPC沒有DCBT指令的支援,那麼我們可以用一些小技巧來達到快取預取的效果,即將DCBT指令替換成一條lwz來載入該地址資料到一個無用的暫存器,這種方法稱為“硬預取”,在x86中也能可以使用該方法來起到快取預取的作用。

5. PPC的AltiVec ™ 指令優化:

在PowerPC G4後開始支援AltiVec ™ ,這是一套類似x86下MMX SSE的SIMD指令集,提供128位的向量平行計算(8bit/16bit/32bit三種計算元)的功能,使多媒體計算平均提高4-5倍,而具體的AltiVec ™ 優化方法,超出本文敘述範圍,讀者可以自行查詢相關資料。

6. 最終優化方法:

開啟C編譯器的彙編輸出,在最大優化模式下思考編譯器的優化策略。反覆閱讀對應 CPU的官方文件,試驗、試驗、再試驗!最終您能寫出漂亮高效的PPC程式碼。

參考資料

《PowerPC860嵌入式系統及應用》,機械工業出版社,陳曉竹,2006
《Linux PowerPC詳解-核心篇》, 機械工業出版社, 王齊, 1997
《羅徹斯特城堡》,機械工業出版社,IBM,2003
《基於POWERPC的嵌入式LINUX》, 北京航空航天大學出版社, 漆昭鈴, 2004

PowerPC Architecture Book,
http://www.ibm.com/developerworks/eserver/library/es-archguide-v2.html 

Software optimization techniques for PowerPC 405 and 440,
http://www.ibm.com/developerworks/eserver/library/es-plib1app.html

Unrolling AltiVec Part 1 – Introducing PowerPC SIMD Unit
http://www.ibm.com/developerworks/library/pa-unrollav1/