ARM SIMD 指令集:NEON 简介
ARM SIMD 指令集:NEON 簡(jiǎn)介
- 一、NEON 簡(jiǎn)介
- 1.1、NEON 簡(jiǎn)介
- 1.2、NEON 使用方式
- 1.3、編譯器自動(dòng)向量化的編譯選項(xiàng)
- 1.3.1 Arm Compiler 中使能自動(dòng)向量化
- 1.3.2 LLVM-clang 中使能自動(dòng)向量化
- 1.3.3 GCC 中使能自動(dòng)向量化
- 1.4、NEON intrisics 指令在x86平臺(tái)的仿真
- 二、NEON 數(shù)據(jù)類型和指令類型
- 2.1、NEON 數(shù)據(jù)類型
- 2.2、 NEON 指令類型
- 三、NEON 指令簡(jiǎn)介
- 3.1、數(shù)據(jù)讀取指令(內(nèi)存數(shù)據(jù)加載到寄存器)
- 3.2、數(shù)據(jù)存儲(chǔ)指令(寄存器數(shù)據(jù)回寫到內(nèi)存 )
- 3.3、數(shù)據(jù)處理指令
- 3.3.1 獲取寄存器的值
- 3.3.2 設(shè)置寄存器的值
- 3.3.3 加減乘除運(yùn)算
- 3.3.4 邏輯運(yùn)算
- 3.3.5 數(shù)據(jù)類型轉(zhuǎn)換
- 3.3.6 寄存器數(shù)據(jù)重排
- 四、NEON 進(jìn)階
- 五、參考連接
一、NEON 簡(jiǎn)介
1.1、NEON 簡(jiǎn)介
- SIMD,即 single instruction multiple data,單指令流多數(shù)據(jù)流,也就是說(shuō)一次運(yùn)算指令可以執(zhí)行多個(gè)數(shù)據(jù)流,從而提高程序的運(yùn)算速度,實(shí)質(zhì)是通過(guò) 數(shù)據(jù)并行 來(lái)提高執(zhí)行效率
- ARM NEON 是 ARM 平臺(tái)下的 SIMD 指令集,利用好這些指令可以使程序獲得很大的速度提升。不過(guò)對(duì)很多人來(lái)說(shuō),直接利用匯編指令優(yōu)化代碼難度較大,這時(shí)就可以利用 ARM NEON intrinsic 指令,它是底層匯編指令的封裝,不需要用戶考慮底層寄存器的分配,但同時(shí)又可以達(dá)到原始匯編指令的性能。
- NEON 是一種 128 位的 SIMD 擴(kuò)展指令集,由 ARMv7 引入,在 ARMv8 對(duì)其功能進(jìn)行了擴(kuò)展(支持向量化運(yùn)算),支持包括加法、乘法、比較、移位、絕對(duì)值 、極大極小極值運(yùn)算、保存和加載指令等運(yùn)算
- ARM 架構(gòu)下的下一代 SIMD 指令集為 SVE(Scalable Vector Extension,可擴(kuò)展矢量指令),支持可變矢量長(zhǎng)度編程,SVE 指令集的矢量寄存器的長(zhǎng)度最小支持 128 位,最大可以支持 2048 位,以 128 位為增量
- ARM NEON 技術(shù)的核心是 NEON 單元,主要由四個(gè)模塊組成:NEON 寄存器文件、整型執(zhí)行流水線、單精度浮點(diǎn)執(zhí)行流水線和數(shù)據(jù)加載存儲(chǔ)和重排流水線
- ARM 基本數(shù)據(jù)類型有三種:字節(jié)(Byte,8bit)、半字(Halfword,16bit)、字(Word,32bit)
- 新的 Armv8a 架構(gòu)有 32 個(gè) 128bit 向量寄存器,老的 ArmV7a 架構(gòu)有 32 個(gè) 64bit(可當(dāng)作 16 個(gè)128bit)向量寄存器,被用來(lái)存放向量數(shù)據(jù),每個(gè)向量元素的類型必須相同,根據(jù)處理元素的大小可以劃分為 2/4/8/16 個(gè)通道
1.2、NEON 使用方式
- ARM 平臺(tái)提供了四種使用 NEON 技術(shù)的方式,分別為 NEON 內(nèi)嵌函數(shù)(intrinsics)、NEON 匯編、NEON 開源庫(kù)和編譯器自動(dòng)向量化
- NEON 內(nèi)嵌函數(shù):類似于普通函數(shù)調(diào)用,簡(jiǎn)單易維護(hù),編譯器負(fù)責(zé)將 NEON 指令替換成匯編語(yǔ)言的復(fù)雜任務(wù),主要包括寄存器分配和代碼調(diào)度以及指令集重排,來(lái)達(dá)到獲取最高性能的目標(biāo)
- NEON 匯編:匯編語(yǔ)言相對(duì)晦澀難懂,移植較難、不便于維護(hù),但其 效率最高
- NEON 開源庫(kù):如 Ne10、OpenMAX、ffmpeg、Eigen3 和 Math-neon 等
- 編譯器自動(dòng)向量化:目前大多數(shù)編譯器都具有自動(dòng)向量化的功能,將 C/C++ 代碼自動(dòng)替換為 SIMD 指令。從編譯技術(shù)上來(lái)說(shuō),自動(dòng)向量化一般包含兩部分:循環(huán)向量化(Loop vectorization)和超字并行向量化(SLP,Superword-Level Parallelism vectorization,又稱 Basic block vectorization)
- 循環(huán)向量化:將循環(huán)進(jìn)行展開,增加循環(huán)中的執(zhí)行代碼來(lái)減少循環(huán)次數(shù)
- SLP 向量化:編譯器將多個(gè)標(biāo)量運(yùn)算綁定到一起,使其成為向量運(yùn)算
- 編寫代碼時(shí)要加上頭文件:#include <arm_neon.h>,編譯時(shí)要加上相應(yīng)的 編譯選項(xiàng):LOCAL_CFLAGS += -mcpu=cortex-a53 -mfloat-abi=softfp -mfpu=neon-vfpv4 -O3
1.3、編譯器自動(dòng)向量化的編譯選項(xiàng)
- 目前支持自動(dòng)向量化的編譯器有 Arm Compiler 6、Arm C/C++ Compiler、LLVM-clang 以及 GCC,這幾種編譯器間的相互關(guān)系如下表所示:
1.3.1 Arm Compiler 中使能自動(dòng)向量化
-
下文中 Arm Compiler 6 與 Arm C/C++ Compiler 使用 armclang 統(tǒng)稱,armclang 使能自動(dòng)向量化配置信息如下表所示:
-
armclang 實(shí)現(xiàn)自動(dòng)向量化示例:
1.3.2 LLVM-clang 中使能自動(dòng)向量化
- Android NDK 從 r13 開始以 clang 為默認(rèn)編譯器,使用 Android NDK 工具鏈?zhǔn)鼓茏詣?dòng)向量化配置參數(shù)如下表所示:
- 在 CMake 中配置自動(dòng)向量化方式如下:
1.3.3 GCC 中使能自動(dòng)向量化
- 在 gcc 中使能自動(dòng)向量化配置參數(shù)如下:
-
在不明確配置 -mcpu 的情況下,編譯器將使用默認(rèn)配置(取決于編譯工具鏈時(shí)的選項(xiàng)設(shè)置)進(jìn)行編譯,通常情況下 -mfpu 和 -mcpu 的配置存在關(guān)聯(lián)性,對(duì)應(yīng)關(guān)系如下:
-
gcc 中實(shí)現(xiàn)自動(dòng)向量化的編譯配置如下:
1.4、NEON intrisics 指令在x86平臺(tái)的仿真
- 為了便于 NEON 指令從 ARM 平臺(tái)移植到 x86 平臺(tái)使用,Intel 提供了一套轉(zhuǎn)化接口 NEON2SSE,用于將 NEON 內(nèi)聯(lián)函數(shù)轉(zhuǎn)化為 Intel SIMD(SSE) 內(nèi)聯(lián)函數(shù),大部分 x86 平臺(tái) C/C++編譯器均支持 SSE,因此只需下載并包含接口頭文件 NEON_2_SSE.h,即可在x86平臺(tái)調(diào)試 NEON 指令代碼
- x86 上模擬實(shí)現(xiàn)可參考:
- NEON_2_SSE.h 是個(gè)好東西
- https://github.com/intel/ARM_NEON_2_x86_SSE
- https://github.com/christophe-lyon/arm-neon-tests
二、NEON 數(shù)據(jù)類型和指令類型
2.1、NEON 數(shù)據(jù)類型
- NEON 向量數(shù)據(jù)類型是根據(jù)以下模式命名的:<type><size>x<number_of_lanes>_t,eg:int8x16_t 是一個(gè)16 通道 的向量,每個(gè)通道包含一個(gè)有符號(hào) 8 位整數(shù)
- NEON 還提供了數(shù)組向量數(shù)據(jù)類型,命名模式如下:<type><size>x<number of lanes>x<length of array>_t,eg:int8x16x4_t 是一個(gè)長(zhǎng)度為 4 的數(shù)組,每一個(gè)數(shù)據(jù)的類型為 int8x16_t
- 下表列出了 16 個(gè) D 寄存器上的向量數(shù)據(jù)類型及 16 個(gè) Q 寄存器上的向量數(shù)據(jù)類型
- D 寄存器一次能處理 8 個(gè) u8 數(shù)據(jù),Q 寄存器一次能處理 16 個(gè) u8 數(shù)據(jù)
| int8x8_t | int8x16_t |
| int16x4_t | int16x8_t |
| int32x2_t | int32x4_t |
| int64x1_t | int64x2_t |
| uint8x8_t | uint8x16_t |
| uint16x4_t | uint16x8_t |
| uint32x2_t | uint32x4_t |
| uint64x1_t | uint64x2_t |
| float16x4_t | float16x8_t |
| float32x2_t | float32x4_t |
| poly8x8_t | poly8x16_t |
| poly16x4_t | poly16x8_t |
2.2、 NEON 指令類型
NEON指令的函數(shù)名組成格式:v<mod><opname><shape><flags>_<type> ,逐元素進(jìn)行操作
- v:vector 的縮寫,表示向量
- mod:
- q:表示飽和計(jì)算,int8x8_t vqadd_s8(int8x8_t a, int8x8_t b); // a 加 b 的結(jié)果做飽和計(jì)算
- h:表示折半計(jì)算,int8x8_t vhsub_s8(int8x8_t a, int8x8_t b); // a 減 b 的結(jié)果右移一位
- d:表示加倍計(jì)算,int32x4_t vqdmull_s16(int16x4_t a, int16x4_t b); // a 乘 b 的結(jié)果擴(kuò)大一倍, 最后做飽和操作
- r:表示舍入計(jì)算,int8x8_t vrhadd_s8(int8x8_t a, int8x8_t b); // 將 a 與 b 的和減半,同時(shí)做 rounding 操作, 每個(gè)通道可以表達(dá)為: (ai + bi + 1) >> 1
- p:表示 pairwise 計(jì)算,int8x8_t vpadd_s8(int8x8_t a, int8x8_t b); // 將 a, b 向量的相鄰數(shù)據(jù)進(jìn)行兩兩和操作
- opname:表示具體操作,比如 add,sub 等
- shape:
- l:表示 long,輸出向量的元素長(zhǎng)度是輸入長(zhǎng)度的 2 倍,uint16x8_t vaddl_u8(uint8x8_t a, uint8x8_t b);
- w:表示 wide,第一個(gè)輸入向量和輸出向量類型一樣,且是第二個(gè)輸入向量元素長(zhǎng)度的 2 倍,uint16x8_t vsubw_u8(uint16x8_t a, uint8x8_t b);
- n:表示 narrow,輸出向量的元素長(zhǎng)度是輸入長(zhǎng)度的 1/2 倍,uint32x2_t vmovn_u64(uint64x2_t a);
- _high:AArch64專用,而且和 l/n 配合使用,當(dāng)使用 l(Long) 時(shí),表示輸入向量只有高 64bit 有效;當(dāng)使用 n(Narrow) 時(shí),表示輸出只有高 64bit 有效,int16x8_t vsubl_high_s8(int8x16_t a, int8x16_t b); // a 和 b 只有高 64bit 參與運(yùn)算
- _n:表示有標(biāo)量參與向量計(jì)算,int8x8_t vshr_n_s8(int8x8_t a, const int n); // 向量 a 中的每個(gè)元素右移 n 位
- _lane: 指定向量中某個(gè)通道參與向量計(jì)算,int16x4_t vmul_lane_s16(int16x4_t a, int16x4_t v, const int lane); // 取向量 v 中下標(biāo)為 lane 的元素與向量 a 做乘法計(jì)算
- flags:q 表示 quad word,四字,指定函數(shù)對(duì) 128 位 Q 寄存器進(jìn)行操作,不帶 q 則對(duì) 64 位 D 寄存器進(jìn)行操作
- type:表示函數(shù)的參數(shù)類型(u8/16/32/64、s8/16/32/64、f16/32 等)
- 正常指令:
- 生成大小相同且類型通常與操作數(shù)向量相同的結(jié)果向量,結(jié)果大于 2n2^n2n 的除以 2n2^n2n 取余數(shù),結(jié)果小于 0 的加上 2n2^n2n
- eg: int8x8_t vadd_s8 (int8x8_t __a, int8x8_t __b)
- 飽和指令:
- 當(dāng)超過(guò)數(shù)據(jù)類型指定的范圍則自動(dòng)限制在該范圍內(nèi)(結(jié)果大于 2n?12^n - 12n?1 的截?cái)嗟?2n?12^n - 12n?1 ,結(jié)果小于 0 的截?cái)嗟?0 ),函數(shù)中用 q 來(lái)標(biāo)記(在 v 之后)
- eg: int8x8_t vqsub_s8 (int8x8_t __a, int8x8_t __b)
- 長(zhǎng)指令:
- 對(duì)雙字向量操作數(shù)執(zhí)行運(yùn)算,生成四字向量的結(jié)果,所生成的元素一般是操作數(shù)元素寬度的兩倍,并屬于同一類型,函數(shù)中用 l 來(lái)標(biāo)記,結(jié)果大于 2n2^n2n 的減去 2n2^n2n (一般不會(huì)),結(jié)果小于 0 的加上 2n2^n2n (可能出現(xiàn))
- eg:int16x8_t vaddl_s8 (int8x8_t __a, int8x8_t __b)
- 寬指令:
- 一個(gè)雙字向量操作數(shù)和一個(gè)四字向量操作數(shù)執(zhí)行運(yùn)算,生成四字向量結(jié)果。所生成的元素和第一個(gè)操作數(shù)的元素是第二個(gè)操作數(shù)元素寬度的兩倍,函數(shù)中用 w 來(lái)標(biāo)記
- eg:int16x8_t vaddw_s8 (int16x8_t __a, int8x8_t __b)
- 窄指令:
- 四字向量操作數(shù)執(zhí)行運(yùn)算,并生成雙字向量結(jié)果,所生成的元素一般是操作數(shù)元素寬度的一半,函數(shù)中用 hn 來(lái)標(biāo)記
- eg: int8x8_t vaddhn_s16 (int16x8_t __a, int16x8_t __b)
三、NEON 指令簡(jiǎn)介
- NEON 指令執(zhí)行流程如下:
3.1、數(shù)據(jù)讀取指令(內(nèi)存數(shù)據(jù)加載到寄存器)
- 順序讀取
- 交織讀取
3.2、數(shù)據(jù)存儲(chǔ)指令(寄存器數(shù)據(jù)回寫到內(nèi)存 )
- 順序存儲(chǔ)
- 交織存儲(chǔ)
3.3、數(shù)據(jù)處理指令
3.3.1 獲取寄存器的值
// 從寄存器中訪問(wèn)具體元素:extract a single lane (element) from a vector uint8_t vgetq_lane_u8(uint8x16_t vec, __constrange(0,15) int lane);3.3.2 設(shè)置寄存器的值
// 設(shè)置寄存器具體元素值:set a single lane (element) within a vector. // 注意:返回值要用參數(shù)中的 vec 寄存器來(lái)接收 uint16x8_t vsetq_lane_u16(uint16_t value, uint16x8_t vec, __constrange(0,7) int lane); eg: vec = vsetq_lane_u16(111, vec, 5);// 設(shè)置寄存器所有元素的值(以某一個(gè)通道的值):Set all lanes to the value of one lane of a vector uint8x8_t vdup_lane_u8(uint8x8_t vec, __constrange(0,7) int lane) eg: vec = vdup_lane_u8(vec, 5); // 所有元素都設(shè)置成第五通道的值// 設(shè)置寄存器所有元素的值(以某一個(gè)固定的值) uint8x16_t vmovq_n_u8(uint8_t value); eg: uint8x16_t vec = vmovq_n_u8(5); // 所有元素都設(shè)置成 53.3.3 加減乘除運(yùn)算
- 加法
- 減法
- 乘法
- 乘加
- 乘減
- 倒數(shù)/平方根
- 取負(fù)
3.3.4 邏輯運(yùn)算
- 取整
- 比較運(yùn)算:注意返回類型為無(wú)符號(hào)整數(shù)類型
- 絕對(duì)值
- 最大最小值
- 移位運(yùn)算:第二個(gè)參數(shù)是 int 型,參數(shù)均為 vector 的時(shí)候可為負(fù)數(shù)
- 按位運(yùn)算
3.3.5 數(shù)據(jù)類型轉(zhuǎn)換
// 浮點(diǎn)轉(zhuǎn)定點(diǎn) // 在 f32 轉(zhuǎn)到 u32 時(shí),是向下取整,且如果是負(fù)數(shù),則轉(zhuǎn)換后為 0 uint32x4_t vcvtq_u32_f32(float32x4_t a); uint32x4_t vcvtq_n_u32_f32(float32x4_t a, __constrange(1,32) int b);// 定點(diǎn)轉(zhuǎn)浮點(diǎn) float32x4_t vcvtq_f32_u32(uint32x4_t a); float32x4_t vcvtq_n_f32_u32(uint32x4_t a, __constrange(1,32) int b);// 浮點(diǎn)之間轉(zhuǎn)換 float16x4_t vcvt_f16_f32(float32x4_t a); // VCVT.F16.F32 d0, q0 float32x4_t vcvt_f32_f16(float16x4_t a); // 定點(diǎn)之間轉(zhuǎn)換 int16x8_t vmovl_s8 (int8x8_t a); int8x8_t vqmovn_s16 (int16x8_t a); int32x4_t vmovl_s16 (int16x4_t a); int16x4_t vqmovn_s32 (int32x4_t a);// 向量重新解釋類型轉(zhuǎn)換運(yùn)算:將元素類型為 type2 的 vector 轉(zhuǎn)換為元素類型為 type1 的 vector // 將向量視為另一類型而不更改其值 float32x2_t vreinterpret_f32_u32 (uint32x2_t __a);3.3.6 寄存器數(shù)據(jù)重排
- 按索引重排
- 反轉(zhuǎn)向量元素
- 轉(zhuǎn)置
- 交叉
- 反交叉
- 組合向量:將兩個(gè) 64 位向量組合為單個(gè) 128 位向量
- 拆分向量:將一個(gè) 128 位向量拆分為 2 個(gè) 64 位向量
四、NEON 進(jìn)階
-
CPU優(yōu)化技術(shù) - NEON 開發(fā)進(jìn)階:對(duì)齊問(wèn)題解決
-
ARM 官方算子優(yōu)化:https://github.com/ARM-software/ComputeLibrary
-
NCNN NEON 優(yōu)化參考:包含常用算子 sigmoid/softmax/relu 等
-
OPENCV 第三方庫(kù) carotene NEON 算子優(yōu)化
-
NEON 使用建議:
- 每次讀入的數(shù)據(jù)盡可能的占滿 128 位
- 除法使用乘法進(jìn)行代替,浮點(diǎn)計(jì)算使用定點(diǎn)加移位的方式進(jìn)行
- 合并算法種的一些系數(shù),進(jìn)行化簡(jiǎn)
- 算子進(jìn)行融合,避免內(nèi)存的多次讀寫
- 使用多核多線程進(jìn)行加速
五、參考連接
1、Neon Intrinsics各函數(shù)介紹(*****)
2、https://developer.arm.com/documentation(*****)
3、ARM Neon Intrinsics 學(xué)習(xí)指北:從入門、進(jìn)階到學(xué)個(gè)通透(*****)
4、ARM NEON 技術(shù)之 NEON 基礎(chǔ)介紹(***)
5、移動(dòng)端算法優(yōu)化(******)
6、利用 ARM NEON intrinsic 優(yōu)化常用數(shù)學(xué)運(yùn)算(***)
總結(jié)
以上是生活随笔為你收集整理的ARM SIMD 指令集:NEON 简介的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 易学难精的Python入门之前应该了解哪
- 下一篇: imgbb图床API