1. 程式人生 > >ARM和neon指令集

ARM和neon指令集

在移動平臺上進行一些複雜演算法的開發,一般需要用到指令集來進行加速。目前在移動上使用最多的是ARM晶片。

ARM是微處理器行業的一家知名企業,其晶片結構有:armv5、armv6、armv7和armv8系列。晶片型別有:arm7、arm9、arm11、cortex系列。指令集有:armv5、armv6和neon指令。關於ARM到知識參考:http://baike.baidu.com/view/11200.htm

最初的ARM指令集為通用計算型指令集,指令集都是針對單個數據進行計算,沒有平行計算到功能。隨著版本的更新,後面逐漸加入了一些複雜到指令以及平行計算到指令。而NEON指令是專門針對大規模到並行運算而設計的。

NEON 技術可加速多媒體和訊號處理演算法(如視訊編碼/解碼、2D/3D 圖形、遊戲、音訊和語音處理、影象處理技術、電話和聲音合成),其效能至少為ARMv5 效能的3倍,為 ARMv6 SIMD效能的2倍。

關於SIMD和SISD:Single Instruction Multiple Data,單指令多資料流。反之SISD是單指令單資料。以加法指令為例,單指令單資料(SISD)的CPU對加法指令譯碼後,執行部件先訪問記憶體,取得第一個運算元;之後再一次訪問記憶體,取得第二個運算元;隨後才能進行求和運算。而在SIMD型的CPU中,指令譯碼後幾個執行部件同時訪問記憶體,一次性獲得所有運算元進行運算。這個特點使SIMD特別適合於多媒體應用等資料密集型運算。如下圖所示:

   

如何才能快速到寫出高效的指令程式碼?這就需要對各個指令比較熟悉,知道各個指令的使用規範和使用場合。

ARM指令有16個32位通用暫存器,為r0-r15,其中r13為堆疊指標暫存器,r15為指令計算暫存器。實際可以使用的暫存器只有14個。r0-r3一般作為函式引數使用,函式返回值放在r0中。若函式引數超過4個,超過到引數壓入堆疊。

有效立即數的概念:每個立即數採用一個8位的常數(bit[7:0])迴圈右移偶數位而間接得到,其中迴圈右移的位數由一個4位二進位制(bit[11:8] )的兩倍表示。如果立即數記作<immediate> , 8位常數記作immed_8 , 4位的迴圈右移值記作rotate_imm ,有效的立即數是由一個8位的立即數迴圈右移偶數位得到,可以表示成:

<immediate>=immed_8迴圈右移( 2×rotate_imm)

如:mov r4 , #0x8000 000A    #0x8000 000A 由0xA8迴圈右移0x2位得到。

下面介紹一些比較常用到一些指令。

記憶體訪問指令:

LDR和STR,有三種方式,比較容易搞混

LDR r0, [r1, #4]   r0 := mem[r1+4]   ,#4是直接偏移量,這時候只能在正負4Kb到範圍內。也可以是暫存器偏移,用+/-表示。記住r1不進行偏移。

LDR r0, [r1, #4]!  r0 :=mem[r1+4],r1 := r1 + 4,取值是取偏移量到值,並且r1進行偏移。

LDR r0, [r1], #4   r0 :=mem[r1] ,r1 := r1 +4,取值是取r1地方到值,取值後進行偏移。運算後自動加4,後變址。

另外:LDRB是無符號位元組,SB是有符號位元組,H無符號半字,SH有符號半字。

儲存器和暫存器資料交換:SWP,SWPB

如SWP r0, r1, [r2]   r0 := mem[r2],mem[r2] := r1

多暫存器資料傳輸:

LDMIA r1, {r0,r2,r5}  r0 = mem[r1], r2 = mem[r1+4], r5=mem[r1+8]

通用資料處理指令

第二運算元,常用到有LSR,LSL等,如mov r1, r2, lsl #2 將r2左移2位然後賦值到r1中。

常用到操作有ADD、SUB、AND、ORR、EOR、BIC、ORN,如果加上了S則會更新條件標記。

MOV移動,MVN取反移動。MOV可以是R暫存器,立即數以及接第二運算元。

REV:在字或半字內反轉位元組或位到順序

MUL、MLA和MLS,乘法、乘加和乘減。MLA R1,R2,R3,R4表示R1=R2*R3+R4,還有有符號和無符號乘法等。

跳轉指令

B:無條件跳轉,BL:帶連結到跳轉,BX跳轉並交換指令集等。

 

重點介紹一下NEON指令,目前使用較多。而且使用難度也較大,很多文件上都沒有比較詳細到介紹,也沒有給出相應到例子或者圖示。

一、NEON基本知識

NEON的暫存器:

有16個128位四字到暫存器Q0-Q15,32個64位雙子暫存器D0-D31,兩個暫存器是重疊的,在使用到時候需要特別注意,不小心就會覆蓋掉。如下圖所示:

兩個暫存器的關係:Qn =D2n和D2n+1,如Q8是d16和d17的組合。

 

NEON的資料型別:

注意資料型別針對到時運算元,而不是目標數,這點在寫的時候要特別注意,很容易搞錯,尤其是對那些長指令寬指令的時候,因為經常Q和D一起操作。

 

NEON中的正常指令、寬指令、窄指令、飽和指令、長指令

正常指令:生成大小相同且型別通常與運算元向量相同到結果向量

長指令:對雙字向量運算元執行運算,生產四字向量到結果。所生成的元素一般是運算元元素寬度到兩倍,並屬於同一型別。L標記,如VMOVL。

寬指令:一個雙字向量運算元和一個四字向量運算元執行運算,生成四字向量結果。W標記,如VADDW。

窄指令:四字向量運算元執行運算,並生成雙字向量結果,所生成的元素一般是運算元元素寬度的一半。N標記,如VMOVN。

飽和指令:當超過資料型別指定到範圍則自動限制在該範圍內。Q標記,如VQSHRUN

二、NEON指令

NEON指令較多,下面主要介紹一些常見的指令用法。

複製指令:

VMOV:

兩個arm暫存器和d之間

vmov d0, r0, r1:將r1的內容送到d0到低半部分,r0的內容送到d0到高半部分

vmov r0, r1, d0:將d0的低半部分送到r0,d0的高半部分內容送到r1

一個arm暫存器和d之間

vmov.U32 d0[0], r0:將r0的內容送到d0[0]中,d0[0]指d0到低32位

vmov.U32 r0, d0[0]:將d0[0]的內容送到r0中

立即數:

vmov.U16 d0, #1:將立即數1賦值給d0的每個16位

vmov.U32 q0, #1:將立即數1賦值給q0的每個32位

長指令:VMOVL:d賦值給q

vmovl.U16 q0, d0:將d0的每個16位資料賦值到q0的每個32位資料中

窄指令:VMOVN:q賦值給d

vmovn.I32 d0, q0:將q0的每32位資料賦值到q0的每16位資料中

飽和指令:VQMOVN等,飽和到指定的資料型別

 vqmovun.S32 d0, q0:將q0到每個32位移動到d0中到每個16位中,範圍是0-65535

VDUP:

VDUP.8 d0, r0:將r0複製到d0中,8位

VDUP.16 q0, r0:將r0複製到q0中,16位

VDUP.32 q0, d2[0]:將d2的一半複製到q0中

VDUP.32 d0, d2[1]:將d2的一半複製到d0中

注意是vdup可以將r暫存器中的內容複製到整個neon暫存器中,不能將立即數進行vdup,立即數只能用vmov

邏輯運算

VADD:按位與;VBIC:位清除;VEOR:按位異或;VORN:按位或非;VORR:按位或

移位指令:

VSHL:左移、VSHLL:左移擴充套件、VQSHL:左移飽和、VQSHLU:無符號左移飽和擴充套件

VSHR:右移、VSHRN:右移窄、VRSHR:右移舍入、VQSHRUN:無符號右移飽和舍入

通用算術指令:

VABA:絕對值累加、VABD:絕對值相加、VABS:絕對值、VNEG:求反、VADD、VADDW、VADDL、VSUB、VSUBL、VSUBW:加減

VPADD:將兩個向量的相鄰元素相加

如VPADD.I16 {d2}, d0, d1


VPADDL:VPADDL.S16 d0, d1


VMAX:最大值,VMIN:最小值

VMUL、VMULL、VMLA(乘加)、VMLS(乘減)、

載入儲存指令:

VLD和VST



交叉存取的示意圖:

VREV反轉元素指令:

 

VEXT移位指令:

 

VTRN轉置指令:可以用於矩陣的轉置



VZIP指令:壓縮,類似交叉存取

VUZP指令:解壓操作,類似交叉存取


VTBL查表指令:從d0,d1中查詢d3中的索引值,如果找到則取出,沒有找到則為0,存入d2中

 

三、需要注意的地方

    load資料的時候,第一次load會把資料放在cache裡面,只要不超過cache的大小,下一次load同樣資料的時候,則會比第一次load要快很多,會直接從cache中load資料,這樣在彙編程式設計的時候是非常需要考慮的問題。

     如:求取一個影象的均值,8*8的視窗,先行求和,然後列求和出來均值,這時候會有兩個函式,資料會載入兩遍,如果按照這樣去優化的話則優化不了多少。如果換成上面這種思路,先做行16行,然後再做列,這樣資料都在cache裡面,做列的時候load資料會很快。

   在做neon乘法指令的時候會有大約2個clock的阻塞時間,如果你要立即使用乘法的結果,則就會阻塞在這裡,在寫neon指令的時候需要特別注意。乘法的結果不能立即使用,可以將一些其他的操作插入到乘法後面而不會有時間的消耗。

如:vmul.u16 q1, d3, d4 

         vadd.u32 q1, q2, q3

此時直接使用乘法的結果q1則會阻塞,執行vadd需要再等待2個clock的時間

使用飽和指令的時候,如乘法飽和的時候,在做乘法後會再去做一次飽和,所以時間要比直接做乘法要慢。

如:  vmul.u16 q1, d3, d4

          vqmul.u32 q1, q2, q3

後一個的時間要比第一個的時間要久。

在對16位資料進行load或者store操作的時候,需要注意的是位元組移位。比如是16位資料,則load 8個16位資料,如果指定暫存器進行偏移,此時需要特別注意。

例如:vld1.64 {d0}, [r0], r1

參考資料: