PowerPC Figure – PPC入門與優化

背景介紹

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, msg@ha                /* r4 = msg(high) << 16 */
•        addi %r4, %r4, msg@l       /* 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/


http://blog.csdn.net/skywind/article/details/6347684


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章