ARM NEON 編程系列2 - 基本指令集


ARM NEON 編程系列2 - 基本指令集

前言

本系列博文用於介紹ARM CPU下NEON指令優化。

  • 博文github地址:github
  • 相關代碼github地址:github

NEON指令集

主流支持目標平台為ARM CPU的編譯器基本都支持NEON指令。可以通過在代碼中嵌入NEON匯編來使用NEON,但是更加常見的方式是通過類似C函數的NEON Instrinsic來編寫NEON代碼。就如同NEON hello world一樣。NEON Instrinsic是編譯器支持的一種buildin類型和函數的集合,基本涵蓋NEON的所有指令,通常這些Instrinsic包含在arm_neon.h頭文件中。
本文以android-ndk-r11c中armv7的arm_neon.h為例,講解NEON的指令類型。

寄存器

ARMV7架構包含:

  • 16個通用寄存器(32bit),R0-R15
  • 16個NEON寄存器(128bit),Q0-Q15(同時也可以被視為32個64bit的寄存器,D0-D31)
  • 16個VFP寄存器(32bit),S0-S15

NEON和VFP的區別在於VFP是加速浮點計算的硬件不具備數據並行能力,同時VFP更盡興雙精度浮點數(double)的計算,NEON只有單精度浮點計算能力。更多請參考stackoverflow:neon vs vfp

基本數據類型

  • 64bit數據類型,映射至寄存器即為D0-D31
    相應的c/c++語言類型(stdint.h或者csdtint頭文件中類型)在注釋中說明。
//typedef int8_t[8] int8x8_t;
typedef __builtin_neon_qi int8x8_t	__attribute__ ((__vector_size__ (8)));
//typedef int16_t[4] int16x4_t;
typedef __builtin_neon_hi int16x4_t	__attribute__ ((__vector_size__ (8)));
//typedef int32_t[2] int32x2_t;
typedef __builtin_neon_si int32x2_t	__attribute__ ((__vector_size__ (8)));
//typedef int64_t[1] int64x1_t;
typedef __builtin_neon_di int64x1_t;
//typedef float16_t[4] float16x4_t;
//(注:該類型為半精度,在部分新的CPU上支持,c/c++語言標注中尚無此基本數據類型)
typedef __builtin_neon_hf float16x4_t	__attribute__ ((__vector_size__ (8)));
//typedef float32_t[2] float32x2_t;
typedef __builtin_neon_sf float32x2_t	__attribute__ ((__vector_size__ (8)));
//poly8以及poly16類型在常用算法中基本不會使用
//詳細解釋見:
//http://stackoverflow.com/questions/22224282/arm-neon-and-poly8-t-and-poly16-t
typedef __builtin_neon_poly8 poly8x8_t	__attribute__ ((__vector_size__ (8)));
typedef __builtin_neon_poly16 poly16x4_t	__attribute__ ((__vector_size__ (8)));
#ifdef __ARM_FEATURE_CRYPTO
typedef __builtin_neon_poly64 poly64x1_t;
#endif
//typedef uint8_t[8] uint8x8_t;
typedef __builtin_neon_uqi uint8x8_t	__attribute__ ((__vector_size__ (8)));
//typedef uint16_t[4] uint16x4_t;
typedef __builtin_neon_uhi uint16x4_t	__attribute__ ((__vector_size__ (8)));
//typedef uint32_t[2] uint32x2_t;
typedef __builtin_neon_usi uint32x2_t	__attribute__ ((__vector_size__ (8)));
//typedef uint64_t[1] uint64x1_t;
typedef __builtin_neon_udi uint64x1_t;
  • 128bit數據類型,映射至寄存器即為Q0-Q15
    相應的c/c++語言類型(stdint.h或者csdtint頭文件中類型)在注釋中說明。
//typedef int8_t[16] int8x16_t;
typedef __builtin_neon_qi int8x16_t	__attribute__ ((__vector_size__ (16)));
//typedef int16_t[8] int16x8_t;
typedef __builtin_neon_hi int16x8_t	__attribute__ ((__vector_size__ (16)));
//typedef int32_t[4] int32x4_t;
typedef __builtin_neon_si int32x4_t	__attribute__ ((__vector_size__ (16)));
//typedef int64_t[2] int64x2_t;
typedef __builtin_neon_di int64x2_t	__attribute__ ((__vector_size__ (16)));
//typedef float32_t[4] float32x4_t;
typedef __builtin_neon_sf float32x4_t	__attribute__ ((__vector_size__ (16)));
//poly8以及poly16類型在常用算法中基本不會使用
//詳細解釋見:
//http://stackoverflow.com/questions/22224282/arm-neon-and-poly8-t-and-poly16-t
typedef __builtin_neon_poly8 poly8x16_t	__attribute__ ((__vector_size__ (16)));
typedef __builtin_neon_poly16 poly16x8_t	__attribute__ ((__vector_size__ (16)));
#ifdef __ARM_FEATURE_CRYPTO
typedef __builtin_neon_poly64 poly64x2_t	__attribute__ ((__vector_size__ (16)));
#endif
//typedef uint8_t[16] uint8x16_t;
typedef __builtin_neon_uqi uint8x16_t	__attribute__ ((__vector_size__ (16)));
//typedef uint16_t[8] uint16x8_t;
typedef __builtin_neon_uhi uint16x8_t	__attribute__ ((__vector_size__ (16)));
//typedef uint32_t[4] uint32x4_t;
typedef __builtin_neon_usi uint32x4_t	__attribute__ ((__vector_size__ (16)));
//typedef uint64_t[2] uint64x2_t;
typedef __builtin_neon_udi uint64x2_t	__attribute__ ((__vector_size__ (16)));
typedef float float32_t;
typedef __builtin_neon_poly8 poly8_t;
typedef __builtin_neon_poly16 poly16_t;
#ifdef __ARM_FEATURE_CRYPTO
typedef __builtin_neon_poly64 poly64_t;
typedef __builtin_neon_poly128 poly128_t;
#endif

結構化數據類型

下面這些數據類型是上述基本數據類型的組合而成的結構化數據類型,通常為被映射到多個寄存器中。

typedef struct int8x8x2_t
{
  int8x8_t val[2];
} int8x8x2_t;
...
//省略...
...
#ifdef __ARM_FEATURE_CRYPTO
typedef struct poly64x2x4_t
{
  poly64x2_t val[4];
} poly64x2x4_t;
#endif

基本指令集

NEON指令按照操作數類型可以分為正常指令、寬指令、窄指令、飽和指令、長指令。

  • 正常指令:生成大小相同且類型通常與操作數向量相同到結果向量。
  • 長指令:對雙字向量操作數執行運算,生產四字向量到結果。所生成的元素一般是操作數元素寬度到兩倍,並屬於同一類型。L標記,如VMOVL。
  • 寬指令:一個雙字向量操作數和一個四字向量操作數執行運算,生成四字向量結果。W標記,如VADDW。
  • 窄指令:四字向量操作數執行運算,並生成雙字向量結果,所生成的元素一般是操作數元素寬度的一半。N標記,如VMOVN。
  • 飽和指令:當超過數據類型指定到范圍則自動限制在該范圍內。Q標記,如VQSHRUN

NEON指令按照作用可以分為:加載數據、存儲數據、加減乘除運算、邏輯AND/OR/XOR運算、比較大小運算等,具體信息參考資料[1]中附錄C和附錄D部分。

常用的指令集包括:

  • 初始化寄存器 寄存器的每個lane(通道)都賦值為一個值N
Result_t vcreate_type(Scalar_t N)
Result_t vdup_type(Scalar_t N)
Result_t vmov_type(Scalar_t N)

lane(通道)在下面有說明。

  • 加載內存數據進寄存器
    間隔為x,加載數據進NEON寄存器
Result_t vld[x]_type(Scalar_t* N)
Result_t vld[x]q_type(Scalar_t* N)

間隔為x,加載數據進NEON寄存器的相關lane(通道),其他lane(通道)的數據不改變

Result_t vld[x]_lane_type(Scalar_t* N,Vector_t M,int n)
Result_t vld[x]q_lane_type(Scalar_t* N,Vector_t M,int n)

從N中加載x條數據,分別duplicate(復制)數據到寄存器0-(x-1)的所有通道

Result_t vld[x]_dup_type(Scalar_t* N)
Result_t vld[x]q_dup_type(Scalar_t* N)
  • lane(通道):比如一個float32x4_t的NEON寄存器,它具有4個lane(通道),每個lane(通道)有一個float32的值,因此 c++ float32x4_t dst = vld1q_lane_f32(float32_t* ptr,float32x4_t src,int n=2) 的意思就是先將src寄存器的值復制到dst寄存器中,然后從ptr這個內存地址中加載第3個(lane的index從0開始)float到dst寄存器的第3個lane(通道中)。最后dst的值為:{src[0],src[1],ptr[2],src[3]}。
  • 間隔:交叉存取,是ARM NEON特有的指令,比如 c++ float32x4x3_t = vld3q_f32(float32_t* ptr) ,此處間隔為3,即交叉讀取12個float32進3個NEON寄存器中。3個寄存器的值分別為:{ptr[0],ptr[3],ptr[6],ptr[9]},{ptr[1],ptr[4],ptr[7],ptr[10]},{ptr[2],ptr[5],ptr[8],ptr[11]}。
  • 存儲寄存器數據到內存
    間隔為x,存儲NEON寄存器的數據到內存中
void vstx_type(Scalar_t* N)
void vstxq_type(Scalar_t* N)

間隔為x,存儲NEON寄存器的相關lane(通道)到內存中

Result_t vst[x]_lane_type(Scalar_t* N,Vector_t M,int n)
Result_t vst[x]q_lane_type(Scalar_t* N,Vector_t M,int n)
  • 讀取/修改寄存器數據 讀取寄存器第n個通道的數據
Result_t vget_lane_type(Vector_t M,int n)

讀取寄存器的高/低部分到新的寄存器中,數據變窄(長度減半)。

Result_t vget_low_type(Vector_t M)
Result_t vget_high_type(Vector_t M)

返回在復制M的基礎上設置通道n為N的寄存器數據

Result_t vset_lane_type(Scalar N,Vector_t M,int n)
  • 寄存器數據重排 從寄存器M中取出后n個通道的數據置於低位,再從寄存器N中取出x-n個通道的數據置於高位,組成一個新的寄存器數據。
Result_t vext_type(Vector_t N,Vector_t M,int n)
Result_t vextq_type(Vector_t N,Vector_t M,int n)

其他數據重排指令還有:

vtbl_tyoe,vrev_type,vtrn_type,vzip_type,vunzip_type,vcombine ...
等以后有時間一一講解。

  • 類型轉換指令 強制重新解釋寄存器的值類型,從SrcType轉化為DstType,其內部實際值不變且總的字節數不變,舉例:vreinterpret_f32_s32(int32x2_t),從int32x2_t轉化為float32x2_t。
vreinterpret_DstType_SrcType(Vector_t N)
  • 算數運算指令 [普通指令] 普通加法運算 res = M+N
Result_t vadd_type(Vector_t M,Vector_t N)
Result_t vaddq_type(Vector_t M,Vector_t N)

[長指令] 變長加法運算 res = M+N,為了防止溢出,一種做法是使用如下指令,加法結果存儲到長度x2的寄存器中,如:vuint16x8_t res = vaddl_u8(uint8x8_t M,uint8x8_t N)。

Result_t vaddl_type(Vector_t M,Vector_t N)

[寬指令] 加法運算 res = M+N,第一個參數M寬度大於第二個參數N。

Result_t vaddw_type(Vector_t M,Vector_t N)

[普通指令] 加法運算 res = trunct(M+N)(溢出則截斷)之后向右平移1位,即計算M和N的平均值

Result_t vhadd_type(Vector_t M,Vector_t N)

[普通指令] 加法運算 res = round(M+N)(溢出則循環)之后向右平移1位,即計算M和N的平均值

Result_t vrhadd_type(Vector_t M,Vector_t N)

[飽和指令] 飽和加法運算 res = st(M+N),如:vuint8x8_t res = vqadd_u8(uint8x8_t M,uint8x8_t N),res超出int8_t的表示范圍(0,255),比如256,則設為255.

Result_t vqadd_type(Vector_t M,Vector_t N)

[窄指令] 加法運算 res = M+N,結果比參數M/N的長度小一半,如 uint8x8_t res = vaddhn_u16(uint16x8_t M,uint16x8_t N)

Result_t vaddhn_type(Vector_t M,Vector_t N)

[普通指令] 減法運算 res = M-N

Result_t vsub_type(Vector_t M,Vector_t N)

[普通指令] 乘法運算 res = M*N

Result_t vmul_type(Vector_t M,Vector_t N)
Result_t vmulq_type(Vector_t M,Vector_t N)

[普通指令] 乘&加法運算 res = M+N*P

Result_t vmla_type(Vector_t M,Vector_t N,Vector_t P)
Result_t vmlaq_type(Vector_t M,Vector_t N,Vector_t P)

[普通指令] 乘&減法運算 res = M-N*P

Result_t vmls_type(Vector_t M,Vector_t N,Vector_t P)
Result_t vmlsq_type(Vector_t M,Vector_t N,Vector_t P)

類似加法運算,減法和乘法運算也有一系列變種...

  • 數據處理指令 [普通指令] 計算絕對值 res=abs(M)
Result_t vabs_type(Vector_t M)

[普通指令] 計算負值 res=-M

Result_t vneg_type(Vector_t M)

[普通指令] 計算最大值 res=max(M,N)

Result_t vmax_type(Vector_t M,Vector_t N)

[普通指令] 計算最小值 res=min(M,N)

Result_t vmin_type(Vector_t M,Vector_t N)

...

  • 比較指令 [普通指令] 比較是否相等 res=mask(M == N)
Result_t vceg_type(Vector_t M,Vector_t N)

[普通指令] 比較是否大於或等於 res=mask(M >= N)

Result_t vcge_type(Vector_t M,Vector_t N)

[普通指令] 比較是否大於 res=mask(M > N)

Result_t vcgt_type(Vector_t M,Vector_t N)

[普通指令] 比較是否小於或等於 res=mask(M ⇐ N)

Result_t vcle_type(Vector_t M,Vector_t N)

[普通指令] 比較是否小於 res=mask(M < N)

Result_t vclt_type(Vector_t M,Vector_t N)

...

  • 歸約指令 [普通指令] 歸約加法,M和N內部的元素各自相加,最后組成一個新的結果
Result_t vpadd_type(Vector_t M,Vector_t N)

[普通指令] 歸約最大比較,M和N內部的元素比較得出最大值,最后組成一個新的結果

Result_t vpmax_type(Vector_t M,Vector_t N)

[普通指令] 歸約最小比較,M和N內部的元素比較得出最小值,最后組成一個新的結果

Result_t vpmin_type(Vector_t M,Vector_t N)

...

參考

  1. DEN0018A_neon_programmers_guide
  2. DDI0487A_f_armv8_arm
  3. DEN0013D_cortex_a_series_PG
  4. Coding for NEON - Part 1: Load and Stores
  5. Coding for NEON - Part 2: Dealing With Leftovers
  6. Coding for NEON - Part 3: Matrix Multiplication
  7. Coding for NEON - Part 4: Shifting Left and Right
  8. Coding for NEON - Part 5: Rearranging Vectors

歡迎關注個人公眾號,全部原創,發布技術與感想。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM