【genius_platform软件平台开发】第八十二讲:ARM Neon指令集一(ARM NEON Intrinsics, SIMD运算, 优化心得)
1. ARM Neon Intrinsics 編程
1.入門:基本能上手寫Intrinsics
1.1 Neon介紹、簡明案例與編程慣例
1.2 如何檢索Intrinsics
1.3 優(yōu)化效果案例
1.4 如何在Android應(yīng)用Neon
2. 進(jìn)階:注意細(xì)節(jié)處理,學(xué)習(xí)常用算子的實(shí)現(xiàn)
2.1 與Neon相關(guān)的ARM體系結(jié)構(gòu)
2.2 對非整數(shù)倍元素個(gè)數(shù)(leftovers)的處理技巧
2.3 算子源碼學(xué)習(xí)(ncnn庫,AI方向)
2.4 算子源碼學(xué)習(xí)(Nvidia carotene庫,圖像處理方向 )
3. 學(xué)個(gè)通透:了解原理
3.1 SIMD加速原理
3.2 了解硬件決定的速度極限:Software Optimization Guide
3.3 反匯編分析生成代碼質(zhì)量
4. 其他:相關(guān)的研討會視頻、庫、文檔等
ncnn是騰訊開源,nihui維護(hù)的AI推理引擎。由于Neon實(shí)現(xiàn)往往跟循環(huán)展開等技巧一起使用,代碼往往比較長。可以先閱讀普通實(shí)現(xiàn)的代碼實(shí)現(xiàn)了解頂層邏輯,再閱讀Neon實(shí)現(xiàn)的代碼。例如,我們希望學(xué)習(xí)全連接層(innerproduct)的Neon實(shí)現(xiàn),其普通實(shí)現(xiàn)的位置在ncnn/src/layer/innerproduct.cpp,對應(yīng)的Neon加速實(shí)現(xiàn)的位置在ncnn/src/layer/arm/innerproduct_arm.cpp。
2. ARMv8 中的 SIMD 運(yùn)算
SIMD
什么是SIMD呢?就是一條指令處理多個(gè)數(shù)據(jù),可以算作是一種并行計(jì)算。比如我們要做一個(gè)4維向量的加法,用一般的指令完成必須使用4次加法指令才行,而用SIMD指令可能只需要一次加法,而且花費(fèi)的時(shí)間和一般指令做一次加法的時(shí)間相同。很顯然,SIMD可以大大提高一些計(jì)算密集型任務(wù)的執(zhí)行效率。這種SIMD指令功能,主流的體系結(jié)構(gòu)一般都用一組特殊的指令子集給予支持,比如x86的SSE,還比如本文講的ARM的NEON。
NEON
NEON是ARM下的一個(gè)SIMD指令集合。可實(shí)現(xiàn)64位/128位的并行計(jì)算。64位/128位并行怎么理解呢?舉例說,在128位并行的情況下,如果是8位整數(shù),可以并行進(jìn)行16對整數(shù)的加法;如果是16位整數(shù),就可以并行進(jìn)行8對整數(shù)的加法;以此類推。
指令集合自然也離不開寄存器。NEON寄存器分兩種。一種寄存器以D開頭,共32個(gè),每個(gè)64位;另一種寄存器以Q開頭,共16個(gè),每個(gè)128位。Q0與D0,D1重合(共用128比特),Q1與D2,D3重合,以此類推。因此用D寄存器可并行8個(gè)8位整數(shù)加法,而用Q寄存器可并行16個(gè)8位整數(shù)加法。
NEON intrinsics
如果直接用匯編寫NEON固然可以,但是coding的效率不會很高。C編譯器支持將NEON指令封裝成內(nèi)置函數(shù)供程序員直接使用,這樣一來無疑會大大提高開發(fā)效率和代碼可維護(hù)性。
同時(shí),執(zhí)行效率也并不會降低很多,因?yàn)槭褂肗EON intrinsics時(shí),雖然像是在調(diào)用各種結(jié)構(gòu)體和函數(shù),但將生成的代碼反匯編后可以發(fā)現(xiàn),其實(shí)沒有調(diào)用函數(shù),只是在使用NEON寄存器和指令罷了。
即便目的是寫匯編代碼,使用intrinsics也有好處。比如先用intrinsics寫好代碼編譯后在反匯編,在此基礎(chǔ)上進(jìn)行優(yōu)化,可能比較省力。
數(shù)據(jù)類型
<基本類型>x<lane個(gè)數(shù)>x<向量個(gè)數(shù)>_t,向量個(gè)數(shù)如果省略表示只有一個(gè)。如int8x8_t,uint8x8x3_t。
基本類型int8,int16,int32,int64,uint8,uint16,uint32,uint64,float16,float32
lane個(gè)數(shù)表示并行處理的基本類型數(shù)據(jù)的個(gè)數(shù)。
對于多個(gè)向量的類型實(shí)際上是結(jié)構(gòu)體
typedef struct {
uint8x8_t val[3];
} uint8x8x3_t;
指令命名
<指令名>[后綴]_<數(shù)據(jù)基本類型簡寫>
其中后綴如果沒有,表示64位并行;如果后綴是q,表示128位并行。
如果后綴是l,表示長指令,輸出數(shù)據(jù)的基本類型位數(shù)是輸入的2倍;如果后綴是n,表示窄指令,輸出數(shù)據(jù)的基本類型位數(shù)是輸入的一半。
數(shù)據(jù)基本類型簡寫:s8,s16,s32,s64,u8,u16,u32,u64,f16,f32
例如:
vadd_u16:兩個(gè)uint16x4相加為一個(gè)uint16x4
vaddq_u16:兩個(gè)uint16x8相加為一個(gè)uint16x8
vaddl_u16:兩個(gè)uint8x8相加為一個(gè)uint16x8
指令分類說明
算術(shù)和位運(yùn)算指令
vadd,vsub,vmul,vand,vorr,vshl,vshr等。
但是NEON不直接提供除法和開平方指令,而是提供了對于倒數(shù)1/x和開方的倒數(shù)1/x0.5的近似指令。這樣一來除法a/b可以表示為a*(1/b),開方a0.5可以表示為a*(1/a^0.5)。
示例://近似求倒數(shù) inline static float32x4_t vrecp(float32x4_t v) {float32x4_t r = vrecpeq_f32(v); //求得初始估計(jì)值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); //求得開方倒數(shù)的初始估計(jì)值r = vmulq_f32(vrsqrtsq_f32(v, r), r); //逼近return vmulq_f32(v, r); //通過乘法轉(zhuǎn)為開方 }數(shù)據(jù)移動指令
實(shí)際編程中經(jīng)常要在不同NEON數(shù)據(jù)類型間轉(zhuǎn)移數(shù)據(jù),有時(shí)還要按lane來get/set向量值,NEON intrinsics也提供了這類操作。
vdup[后綴]n<數(shù)據(jù)基本類型簡寫>:用同一個(gè)標(biāo)量值初始化一個(gè)向量全部的lane;
vset[后綴]lane<數(shù)據(jù)基本類型簡寫>:對指定的一個(gè)lane進(jìn)行設(shè)置
vget[后綴]lane<數(shù)據(jù)基本類型簡寫>:獲取指定的一個(gè)lane的值
vmov[后綴]_<數(shù)據(jù)基本類型簡寫>:數(shù)據(jù)間移動
訪存指令
NEON訪存指令可以將內(nèi)存讀到NEON數(shù)據(jù)類型中去,或者將NEON數(shù)據(jù)類型寫進(jìn)內(nèi)存。可以支持一次讀寫多向量數(shù)據(jù)類型。
vld<向量數(shù)>[后綴]_<數(shù)據(jù)基本類型簡寫>:讀內(nèi)存
vst<向量數(shù)>[后綴]_<數(shù)據(jù)基本類型簡寫>:寫內(nèi)存
例如,vld1_u8從內(nèi)存讀取一個(gè)uint8x8_t數(shù)據(jù),vst3q_u8寫入一個(gè)u8x16x3_t數(shù)據(jù)。
需要注意的是,默認(rèn)情況下對多個(gè)向量數(shù)據(jù)的讀寫使用了interleave模式,可以理解為向多向量數(shù)據(jù)讀入或從其寫出時(shí)外層按照lane循環(huán),內(nèi)層再按照向量循環(huán)。
例如將一個(gè)16像素的RGB圖片解析成R,G,B三個(gè)plane的時(shí)候,可以寫如下代碼:
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程序有時(shí)候需要對一個(gè)向量的各個(gè)lane的值的情況來判斷另一個(gè)向量對應(yīng)的lane如何進(jìn)行處理。
vce[后綴]_<數(shù)據(jù)基本類型簡寫>:v[n] = v1[n] == v2[n] ? 全0 : 全1
vcle[后綴]_<數(shù)據(jù)基本類型簡寫>:v[n] = v1[n] <= v2[n] ? 全0 : 全1
vclt[后綴]_<數(shù)據(jù)基本類型簡寫>:v[n] = v1[n] < v2[n] ? 全0 : 全1
vcge[后綴]_<數(shù)據(jù)基本類型簡寫>:v[n] = v1[n] >= v2[n] ? 全0 : 全1
vcgt[后綴]_<數(shù)據(jù)基本類型簡寫>:v[n] = v1[n] > v2[n] ? 全0 : 全1
得出的結(jié)果結(jié)合位運(yùn)算即可實(shí)現(xiàn)條件判斷。
注意事項(xiàng)
NEON intrinsics的注意事項(xiàng)同時(shí)也是NEON匯編的注意事項(xiàng)。
處理數(shù)組時(shí)要注意數(shù)組元素個(gè)數(shù)不能被NEON向量lane個(gè)數(shù)整除的情況,多出的元素應(yīng)補(bǔ)齊或者通過非SIMD方式處理。
NEON不是萬能的,比如把地址放在向量里讓內(nèi)存同時(shí)讀寫就辦不到。設(shè)計(jì)算法時(shí)應(yīng)盡量避免這種情況。
對cache友好仍然是最重要的。有時(shí)一個(gè)算法看上去似乎訪存次數(shù)和計(jì)算次數(shù)都比另一個(gè)算法少,但是由于其訪存方式對cache不友好,導(dǎo)致其運(yùn)行效率不如后者。
示例
- 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編程, 優(yōu)化心得及內(nèi)聯(lián)匯編使用心得
Very thanks to Orchid (Orchid Blog).
NEON intrinsics
提供了一個(gè)連接NEON操作的C函數(shù)接口,編譯器會自動生成相關(guān)的NEON指令,支持ARMv7-A或ARMv8-A平臺。
所有的intrinsics函數(shù)都在GNU官方說明文檔。
一個(gè)簡單的例子:
//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會被編譯器轉(zhuǎn)換成vld1.32 {d0, d1}, [r0]指令,同理vaddq_s32和vst1q_s32被轉(zhuǎn)換成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
總結(jié)
以上是生活随笔為你收集整理的【genius_platform软件平台开发】第八十二讲:ARM Neon指令集一(ARM NEON Intrinsics, SIMD运算, 优化心得)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Nginx sendfile作用
- 下一篇: 8、Horizon 事件数据库安装配置