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

2020-01-13来源: eefocus关键字:ARM  NEON  编程系列  基本指令集

前言

本系列博文用于介绍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,结果比

[1] [2]
关键字:ARM  NEON  编程系列  基本指令集 编辑:什么鱼 引用地址:http://news.eeworld.com.cn/mcu/ic485490.html 本网站转载的所有的文章、图片、音频视频文件等资料的版权归版权所有人所有,本站采用的非本站原创文章及图片等内容无法一一联系确认版权者。如果本网所选内容的文章作者及编辑认为其作品不宜公开自由传播,或不应无偿使用,请及时通过电子邮件或电话通知我们,以迅速采取适当措施,避免给双方造成不必要的经济损失。

上一篇:ARM NEON 编程系列1 - 导论
下一篇:ARM地址空间

关注eeworld公众号 快捷获取更多信息
关注eeworld公众号
快捷获取更多信息
关注eeworld服务号 享受更多官方福利
关注eeworld服务号
享受更多官方福利

推荐阅读

ARM linux内核在内存中的布局
Kernel Memory Layout on ARM Linux Russell King <rmk@arm.linux.org.uk>      November 17, 2005 (2.6.15)This document describes the virtual memory layout which the Linuxkernel uses for ARM processors.  It indicates which regions arefree for platforms to use, and which are used by generic
发表于 2020-01-19
ARM命令LDREX和STREX实现spinlock
在 include/asm-arm/spinlock.h 下有這麼一段#if __LINUX_ARM_ARCH__ < 6#error SMP not supported on pre-ARMv6 CPUs#endif好啦,前提就是:只有 ARM core 版本 >=6 才可以繼續:all spin lock primitives 到最後都是使用下面這個基本型: static inline void __raw_spin_lock(raw_spinlock_t *lock){    unsigned long tmp;1 
发表于 2020-01-19
ARM用户层发生异常后软硬件协同处理流程
我这里是要简单说一下,在ARM平台的用户层发生异常后的软硬件协同处理流程,是个大致的概况,对宏观了解后,具体细节内容网上有很多,可以自行查询。用户层程序正在执行时,遇到未定义的指令(ARM不是别的指令)或者SWI软件中断指令(产生系统调用),就会产生异常,这里以未定义指令异常为例进行说明:一旦出现未定义指令异常,CPU会自动做如下操作:(1)未定义模式(ARM七种运行模式的一种)下对应的lr(即R14,不同的运行模式有不同的lr寄存器)寄存器保存当前发生异常的指令下一条指令的地址。例如,在用户态有A B C 三条指令顺序执行,指令A发生未定义指令异常,则指令B的地址就会由CPU保存到未定义模式下的lr寄存器中,用于异常返回
发表于 2020-01-19
ARM处理器各个模式之间是如何切换的?
1、ARM处理器各个模式之间是如何切换的?答:除用户模式外的其他6种模式称为特权模式,这些模式中,程序可以访问所有系统资源,也可以任意进行处理器模式的切换。处理器模式可以通过软件控制进行切换(直接设置CPSR寄存器的后五位就可以在6种特权模式之间互相切换),也可以通过外部中断或异常处理过程进行切换(例如,在USR模式下,发生中断后切换到IRQ模式)。2、ARM各个模式之间切换时,上下文的保存哪些是硬件在做?哪些是操作系统在做?答:CPU做的:(1)把返回地址保存到相应模式的lr寄存器中,例如从usr模式切换到irq模式,CPU会将usr模式下的pc值,保存到irq模式下的lr寄存器中。(2)保存CPSR到相应模式的SPSR寄存器中
发表于 2020-01-19
ARM处理器的运行模式和ARM寄存器
一、ARM处理器共有7种运行模式 处理器模式描述用户模式(User,usr)正常程序执行的模式快速中断模式(FIQ,fiq)用于高速数据传输和通道处理外部中断模式(IRQ,irq)用于通常的中断处理特权模式(Supervisor,sve)供操作系统使用的一种保护模式数据访问中止模式(Abort,abt)用于虚拟存储及存储保护未定义指令中止模式(Undefined,und)用于支持通过软件仿真硬件的协处理器系统模式(System,sys)用于运行特权级的操作系统任务usr是普通模式,其他六种是特权模式(Privileged Modes),在这些模式下,程序可以访问所有的系统资源,也可以任意地进行处理器模式的切换。除了usr
发表于 2020-01-18
ARM处理器的运行模式和ARM寄存器
ARM裸机驱动中的main函数调用前的准备工作
硬件方面1.关闭CPU看门狗2 配置CPU的工作时钟3.程序要在SDRAM中运行,因此必须初始化SDRAM软件方面1 函数要运行,需要栈空间,因此必须初始化栈指针SP2 设置main函数的返回地址3 调用main4 清理工作
发表于 2020-01-18
小广播
何立民专栏 单片机及嵌入式宝典

北京航空航天大学教授,20余年来致力于单片机与嵌入式系统推广工作。

电子工程世界版权所有 京ICP证060456号 京ICP备10001474号 电信业务审批[2006]字第258号函 京公海网安备110108001534 Copyright © 2005-2020 EEWORLD.com.cn, Inc. All rights reserved