【genius_platform软件平台开发】第八十二讲:ARM Neon指令集一(ARM NEON Intrinsics, SIMD运算, 优化心得)
1. ARM Neon Intrinsics 編程
1.入門:基本能上手寫Intrinsics
1.1 Neon介紹、簡明案例與編程慣例
1.2 如何檢索Intrinsics
1.3 優化效果案例
1.4 如何在Android應用Neon
2. 進階:注意細節處理,學習常用算子的實現
2.1 與Neon相關的ARM體系結構
2.2 對非整數倍元素個數(leftovers)的處理技巧
2.3 算子源碼學習(ncnn庫,AI方向)
2.4 算子源碼學習(Nvidia carotene庫,圖像處理方向 )
3. 學個通透:了解原理
3.1 SIMD加速原理
3.2 了解硬件決定的速度極限:Software Optimization Guide
3.3 反匯編分析生成代碼質量
4. 其他:相關的研討會視頻、庫、文檔等
ncnn是騰訊開源,nihui維護的AI推理引擎。由于Neon實現往往跟循環展開等技巧一起使用,代碼往往比較長。可以先閱讀普通實現的代碼實現了解頂層邏輯,再閱讀Neon實現的代碼。例如,我們希望學習全連接層(innerproduct)的Neon實現,其普通實現的位置在ncnn/src/layer/innerproduct.cpp,對應的Neon加速實現的位置在ncnn/src/layer/arm/innerproduct_arm.cpp。
2. ARMv8 中的 SIMD 運算
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/x0.5的近似指令。這樣一來除法a/b可以表示為a*(1/b),開方a0.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不友好,導致其運行效率不如后者。
示例
- 4x4 矩陣乘法
8x8 矩陣乘法
static void matrix_mul_asm(uint16_t **aa, uint16_t **bb, uint16_t **cc) {printf("===> func: %s, line: %d\n", __func__, __LINE__);uint16_t *a = (uint16_t*)aa;uint16_t *b = (uint16_t*)bb;uint16_t *c = (uint16_t*)cc;asm volatile("ld4 {v0.8h, v1.8h, v2.8h, v3.8h}, [%0] \n\t""ld4 {v8.8h, v9.8h, v10.8h, v11.8h}, [%1] \n\t""mul v0.8h, v0.8h, v8.8h \n\t""mul v1.8h, v1.8h, v9.8h \n\t""mul v2.8h, v2.8h, v10.8h \n\t""mul v3.8h, v3.8h, v11.8h \n\t""st4 {v0.8h, v1.8h, v2.8h, v3.8h}, [%2] \n\t""add x1, %0, #64 \n\t""add x2, %1, #64 \n\t""add x3, %2, #64 \n\t"//"ld4 {v4.8h-v7.8h}, [x1] \n\t""ld4 {v4.8h, v5.8h, v6.8h, v7.8h}, [x1] \n\t""ld4 {v12.8h, v13.8h, v14.8h, v15.8h}, [x2] \n\t""mul v4.8h, v4.8h, v12.8h \n\t""mul v5.8h, v5.8h, v13.8h \n\t""mul v6.8h, v6.8h, v14.8h \n\t""mul v7.8h, v7.8h, v15.8h \n\t""st4 {v4.8h, v5.8h, v6.8h, v7.8h}, [x3] \n\t": "+r"(a), //%0"+r"(b), //%1"+r"(c) //%2:: "cc", "memory", "x1", "x2", "x3", "v0", "v1", "v2", "v3", "v4", "v5", "v6", "v7","v8", "v9", "v10", "v11", "v12", "v13", "v14", "v15"); }3. NEON編程, 優化心得及內聯匯編使用心得
Very thanks to Orchid (Orchid Blog).
NEON intrinsics
提供了一個連接NEON操作的C函數接口,編譯器會自動生成相關的NEON指令,支持ARMv7-A或ARMv8-A平臺。
所有的intrinsics函數都在GNU官方說明文檔。
一個簡單的例子:
//add for int array. assumed that count is multiple of 4 #include<arm_neon.h> // C version void add_int_c(int* dst, int* src1, int* src2, int count) {int i;for (i = 0; i < count; i++)dst[i] = src1[i] + src2[i];} }// NEON version void add_float_neon1(int* dst, int* src1, int* src2, int count) {int i;for (i = 0; i < count; i += 4){int32x4_t in1, in2, out;in1 = vld1q_s32(src1);src1 += 4;in2 = vld1q_s32(src2);src2 += 4;out = vaddq_s32(in1, in2);vst1q_s32(dst, out);dst += 4;} }代碼中的vld1q_s32會被編譯器轉換成vld1.32 {d0, d1}, [r0]指令,同理vaddq_s32和vst1q_s32被轉換成vadd.i32 q0, q0, q0,vst1.32 {d0, d1}, [r0]。若不清楚指令意義,請參見ARM? Compiler armasm User Guide - Chapter 12 NEON and VFP Instructions。
參考
ARMv8 Neon Programming
Introducing NEON
Coding for NEON - Part 1: Load and Stores
ARM? Cortex?-A72 MPCore Processor Technical Reference Manual
總結
以上是生活随笔為你收集整理的【genius_platform软件平台开发】第八十二讲:ARM Neon指令集一(ARM NEON Intrinsics, SIMD运算, 优化心得)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Nginx sendfile作用
- 下一篇: 补交20145226蓝墨云班课 -- A