日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

【genius_platform软件平台开发】第八十二讲:ARM Neon指令集一(ARM NEON Intrinsics, SIMD运算, 优化心得)

發布時間:2023/12/8 编程问答 40 豆豆
生活随笔 收集整理的這篇文章主要介紹了 【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 矩陣乘法
#include <stdio.h> #include <stdlib.h> #include <stdint.h> #include <string.h> #include <sys/time.h>#if __aarch64__ #include <arm_neon.h> #endifstatic void dump(uint16_t **x) {int i, j;uint16_t *xx = (uint16_t *)x;printf("%s:\n", __func__);for(i = 0; i < 4; i++) {for(j = 0; j < 4; j++) {printf("%3d ", *(xx + (i << 2) + j));}printf("\n");} }static void matrix_mul_c(uint16_t aa[][4], uint16_t bb[][4], uint16_t cc[][4]) {int i = 0, j = 0;printf("===> func: %s, line: %d\n", __func__, __LINE__);for(i = 0; i < 4; i++) {for(j = 0; j < 4; j++) {cc[i][j] = aa[i][j] * bb[i][j];}}}#if __aarch64__ static void matrix_mul_neon(uint16_t **aa, uint16_t **bb, uint16_t **cc) {printf("===> func: %s, line: %d\n", __func__, __LINE__); #if 1uint16_t (*a)[4] = (uint16_t (*)[4])aa;uint16_t (*b)[4] = (uint16_t (*)[4])bb;uint16_t (*c)[4] = (uint16_t (*)[4])cc;printf("aaaaaaaa\n");asm("nop");asm("nop");asm("nop");asm("nop");uint16x4_t _cc0;uint16x4_t _cc1;uint16x4_t _cc2;uint16x4_t _cc3;uint16x4_t _aa0 = vld1_u16((uint16_t*)a[0]);uint16x4_t _aa1 = vld1_u16((uint16_t*)a[1]);uint16x4_t _aa2 = vld1_u16((uint16_t*)a[2]);uint16x4_t _aa3 = vld1_u16((uint16_t*)a[3]);uint16x4_t _bb0 = vld1_u16((uint16_t*)b[0]);uint16x4_t _bb1 = vld1_u16((uint16_t*)b[1]);uint16x4_t _bb2 = vld1_u16((uint16_t*)b[2]);uint16x4_t _bb3 = vld1_u16((uint16_t*)b[3]);_cc0 = vmul_u16(_aa0, _bb0);_cc1 = vmul_u16(_aa1, _bb1);_cc2 = vmul_u16(_aa2, _bb2);_cc3 = vmul_u16(_aa3, _bb3);vst1_u16((uint16_t*)c[0], _cc0);vst1_u16((uint16_t*)c[1], _cc1);vst1_u16((uint16_t*)c[2], _cc2);vst1_u16((uint16_t*)c[3], _cc3);asm("nop");asm("nop");asm("nop");asm("nop"); #elseprintf("bbbbbbbb\n");int i = 0;uint16x4_t _aa[4], _bb[4], _cc[4];uint16_t *a = (uint16_t*)aa;uint16_t *b = (uint16_t*)bb;uint16_t *c = (uint16_t*)cc;for(i = 0; i < 4; i++) {_aa[i] = vld1_u16(a + (i << 2));_bb[i] = vld1_u16(b + (i << 2));_cc[i] = vmul_u16(_aa[i], _bb[i]);vst1_u16(c + (i << 2), _cc[i]);}#endif }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;#if 0asm volatile("ldr d3, [%0, #0] \n\t""ldr d2, [%0, #8] \n\t""ldr d1, [%0, #16] \n\t""ldr d0, [%0, #24] \n\t""ldr d7, [%1, #0] \n\t""ldr d6, [%1, #8] \n\t""ldr d5, [%1, #16] \n\t""ldr d4, [%1, #24] \n\t""mul v3.4h, v3.4h, v7.4h \n\t""mul v2.4h, v2.4h, v6.4h \n\t""mul v1.4h, v1.4h, v5.4h \n\t""mul v0.4h, v0.4h, v4.4h \n\t"//"add v3.4h, v3.4h, v7.4h \n\t"//"add v2.4h, v2.4h, v6.4h \n\t"//"add v1.4h, v1.4h, v5.4h \n\t"//"add v0.4h, v0.4h, v4.4h \n\t""str d3, [%2,#0] \n\t""str d2, [%2,#8] \n\t""str d1, [%2,#16] \n\t""str d0, [%2,#24] \n\t": "+r"(a), //%0"+r"(b), //%1"+r"(c) //%2:: "cc", "memory", "d0", "d1", "d2", "d3", "d4", "d5", "d6", "d7"); #else// test, OKasm("nop");asm("nop");asm("nop");asm("nop");asm("nop");asm volatile(//"ld4 {v0.4h, v1.4h, v2.4h, v3.4h}, [%0] \n\t""ld4 {v0.4h-v3.4h}, [%0] \n\t""ld4 {v4.4h, v5.4h, v6.4h, v7.4h}, [%1] \n\t""mul v3.4h, v3.4h, v7.4h \n\t""mul v2.4h, v2.4h, v6.4h \n\t""mul v1.4h, v1.4h, v5.4h \n\t""mul v0.4h, v0.4h, v4.4h \n\t""st4 {v0.4h, v1.4h, v2.4h, v3.4h}, [%2] \n\t": "+r"(a), //%0"+r"(b), //%1"+r"(c) //%2:: "cc", "memory", "v0", "v1", "v2", "v3", "v4", "v5", "v6", "v7");asm("nop");asm("nop");asm("nop");asm("nop");asm("nop"); #endif } #endifint main(int argc, const char *argv[]) {uint16_t aa[4][4] = {{1, 2, 3, 4},{5, 6, 7, 8},{3, 6, 8, 1},{2, 6, 7, 1}};uint16_t bb[4][4] = {{1, 3, 5, 7},{2, 4, 6, 8},{2, 5, 7, 9},{5, 2, 7, 1}};uint16_t cc[4][4] = {0};int i, j;struct timeval tv;long long start_us = 0, end_us = 0;dump((uint16_t **)aa);dump((uint16_t **)bb);dump((uint16_t **)cc);/* ******** C **********/gettimeofday(&tv, NULL);start_us = tv.tv_sec + tv.tv_usec;matrix_mul_c(aa, bb, cc);gettimeofday(&tv, NULL);end_us = tv.tv_sec + tv.tv_usec;printf("aa[][]*bb[][] C time %lld us\n", end_us - start_us);dump((uint16_t **)cc);#if __aarch64__/* ******** NEON **********/memset(cc, 0, sizeof(uint16_t) * 4 * 4);gettimeofday(&tv, NULL);start_us = tv.tv_sec + tv.tv_usec;matrix_mul_neon((uint16_t **)aa, (uint16_t **)bb, (uint16_t **)cc);gettimeofday(&tv, NULL);end_us = tv.tv_sec + tv.tv_usec;printf("aa[][]*bb[][] neon time %lld us\n", end_us - start_us);dump((uint16_t **)cc);/* ******** asm **********/memset(cc, 0, sizeof(uint16_t) * 4 * 4);gettimeofday(&tv, NULL);start_us = tv.tv_sec + tv.tv_usec;matrix_mul_asm((uint16_t **)aa, (uint16_t **)bb, (uint16_t **)cc);gettimeofday(&tv, NULL);end_us = tv.tv_sec + tv.tv_usec;printf("aa[][]*bb[][] asm time %lld us\n", end_us - start_us);dump((uint16_t **)cc); #endifreturn 0; } aarch64-linux-gcc -O3 matrix_4x4_mul.c gcc –march=armv8-a [input file] -o [output file]

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运算, 优化心得)的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。