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