ARM彙編筆記(3)——NEON intrinsics 函數

本文打算介紹下ARM的SIMD指令在C語言下intrinsics函數的使用方法,算是對於NEON的一個入門吧。嚴格來說本文並不是關於ARM彙編的,但是多多少少有關係。

SIMD

什麼是SIMD呢?就是一條指令處理多個數據,可以算作是一種並行計算。比如我們要做一個4維向量的加法,用一般的指令完成必須使用4次加法指令才行,而用SIMD指令可能只需要一次加法,而且花費的時間和一般指令做一次加法的時間相同。很顯然,SIMD可以大大提高一些計算密集型任務的執行效率。這種SIMD指令功能,主流的體系結構一般都用一組特殊的指令子集給予支持,比如x86的SSE,還比如本文講的ARM的NEON。

NEON

NEON是ARM下的一個SIMD指令集合。可實現64位/128位的並行計算。64位/128位並行怎麼理解呢?舉例說,在128位並行的情況下,如果是8位整數,可以並行進行16對整數的加法;如果是16位整數,就可以並行進行8對整數的加法;以此類推。

指令集合自然也離不開寄存器。NEON寄存器分兩種。一種寄存器以D開頭,共32個,每個64位;另一種寄存器以Q開頭,共16個,每個128位。Q0與D0,D1重合(共用128比特),Q1與D2,D3重合,以此類推。因此用D寄存器可並行8個8位整數加法,而用Q寄存器可並行16個8位整數加法。

NEON intrinsics

如果直接用匯編寫NEON固然可以,但是coding的效率不會很高。C編譯器支持將NEON指令封裝成內置函數供程序員直接使用,這樣一來無疑會大大提高開發效率和代碼可維護性。

同時,執行效率也並不會降低很多,因爲使用NEON intrinsics時,雖然像是在調用各種結構體和函數,但將生成的代碼反彙編後可以發現,其實沒有調用函數,只是在使用NEON寄存器和指令罷了。

即便目的是寫彙編代碼,使用intrinsics也有好處。比如先用intrinsics寫好代碼編譯後在反彙編,在此基礎上進行優化,可能比較省力。

數據類型

<基本類型>x<lane個數>x<向量個數>_t,向量個數如果省略表示只有一個。如int8x8_t,uint8x8x3_t。

基本類型int8,int16,int32,int64,uint8,uint16,uint32,uint64,float16,float32

lane個數表示並行處理的基本類型數據的個數。

對於多個向量的類型實際上是結構體

typedef struct {
    uint8x8_t val[3];
} uint8x8x3_t;
指令命名

<指令名>[後綴]_<數據基本類型簡寫>

其中後綴如果沒有,表示64位並行;如果後綴是q,表示128位並行。

如果後綴是l,表示長指令,輸出數據的基本類型位數是輸入的2倍;如果後綴是n,表示窄指令,輸出數據的基本類型位數是輸入的一半。

數據基本類型簡寫:s8,s16,s32,s64,u8,u16,u32,u64,f16,f32

例如:

vadd_u16:兩個uint16x4相加爲一個uint16x4

vaddq_u16:兩個uint16x8相加爲一個uint16x8

vaddl_u16:兩個uint8x8相加爲一個uint16x8

指令分類說明
算術和位運算指令

vadd,vsub,vmul,vand,vorr,vshl,vshr等。

但是NEON不直接提供除法和開平方指令,而是提供了對於倒數1/x和開方的倒數1/x^0.5的近似指令。這樣一來除法a/b可以表示爲a*(1/b),開方a^0.5可以表示爲a*(1/a^0.5)。

示例:

//近似求倒數
inline static float32x4_t vrecp(float32x4_t v) {
    float32x4_t r = vrecpeq_f32(v);        //求得初始估計值
    r = vmulq_f32(vrecpsq_f32(v, r), r);    //逼近
    r = vmulq_f32(vrecpsq_f32(v, r), r);    //再次逼近
    return r;
}
//近似求開方
inline float32x4_t vsqrt(float32x4_t v) {
    float32x4_t r = vrsqrteq_f32(v);        //求得開方倒數的初始估計值
    r = vmulq_f32(vrsqrtsq_f32(v, r), r);    //逼近
    return vmulq_f32(v, r);                //通過乘法轉爲開方
}
數據移動指令

實際編程中經常要在不同NEON數據類型間轉移數據,有時還要按lane來get/set向量值,NEON intrinsics也提供了這類操作。

vdup[後綴]_n_<數據基本類型簡寫>:用同一個標量值初始化一個向量全部的lane;

vset[後綴]_lane_<數據基本類型簡寫>:對指定的一個lane進行設置

vget[後綴]_lane_<數據基本類型簡寫>:獲取指定的一個lane的值

vmov[後綴]_<數據基本類型簡寫>:數據間移動

訪存指令

NEON訪存指令可以將內存讀到NEON數據類型中去,或者將NEON數據類型寫進內存。可以支持一次讀寫多向量數據類型。

vld<向量數>[後綴]_<數據基本類型簡寫>:讀內存

vst<向量數>[後綴]_<數據基本類型簡寫>:寫內存

例如,vld1_u8從內存讀取一個uint8x8_t數據,vst3q_u8寫入一個u8x16x3_t數據。

需要注意的是,默認情況下對多個向量數據的讀寫使用了interleave模式,可以理解爲向多向量數據讀入或從其寫出時外層按照lane循環,內層再按照向量循環。

例如將一個16像素的RGB圖片解析成R,G,B三個plane的時候,可以寫如下代碼:

void split(uint8_t *rgb, uint8_t *r, uint8_t *g, uint8_t *b) {
    uint8x16x3_t v = vld3q_u8(rgb);
    vst1q_u8(r, v.val[0]);
    vst1q_u8(g, v.val[1]);
    vst1q_u8(b, v.val[2]);
}
條件指令

如同非SIMD程序需要分支語句一樣,NEON程序有時候需要對一個向量的各個lane的值的情況來判斷另一個向量對應的lane如何進行處理。

vce[後綴]_<數據基本類型簡寫>:v[n] = v1[n] == v2[n] ? 全0 : 全1

vcle[後綴]_<數據基本類型簡寫>:v[n] = v1[n] <= v2[n] ? 全0 : 全1

vclt[後綴]_<數據基本類型簡寫>:v[n] = v1[n] < v2[n] ? 全0 : 全1

vcge[後綴]_<數據基本類型簡寫>:v[n] = v1[n] >= v2[n] ? 全0 : 全1

vcgt[後綴]_<數據基本類型簡寫>:v[n] = v1[n] > v2[n] ? 全0 : 全1

得出的結果結合位運算即可實現條件判斷。

注意事項

NEON intrinsics的注意事項同時也是NEON彙編的注意事項。

處理數組時要注意數組元素個數不能被NEON向量lane個數整除的情況,多出的元素應補齊或者通過非SIMD方式處理。

NEON不是萬能的,比如把地址放在向量裏讓內存同時讀寫就辦不到。設計算法時應儘量避免這種情況。

對cache友好仍然是最重要的。有時一個算法看上去似乎訪存次數和計算次數都比另一個算法少,但是由於其訪存方式對cache不友好,導致其運行效率不如後者。

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