当前位置: 首页 > 工具软件 > neon > 使用案例 >

ARM NEON寄存器

姬寂离
2023-12-01

/*
 * 2020/4/2    10:51    yin
 *
 * 如何才能快速到写出高效的指令代码? 这就需要对各个指令比较熟悉,知道各个指令的使用规范和使用场合。
 */

    32bit NEON寄存器是Q0~Q15共计16个

    64bit NEON寄存器是V0~V31共计32个。

    AArch32状态下的NEON寄存器组,Q0~Q15,同时对应D0~D32,一个Q分为两个D,这两个D是可以单独操作的;

    其次就是一个D分解为2个S,注意哦只有D0~D15才可分解,共计32个S寄存器;如果要计算浮点值的话你得先把数据搬运到
    这个寄存器(而不是R0~R15中)才能进行浮点运算。

    可以看到特点是寄存器是连续的,Q0等价于D0+D1等价于S0+S1+S2+S3,这在我们做矢量运算的时候是很方便的(实例中可以见到),
    但是在AArch64状态下的NEON寄存器组可就不一样。

    V0对应AArch32状态下Q0,这里只对应D0,跟AArch32不同的是D1分配到V1中去了,也就是说寄存器不连续了,这里可能有人有疑问,
    假如我要把数据放到V0的上班部分怎么办呢?这个当然考虑到了比如smlal2指令就是专门处理这类问题的

/*
 * SIMD
 */

    Single Instruction Multiple Data,单指令多数据流。反之SISD是单指令单数据。

    以加法指令为例,单指令单数据(SISD)的CPU对加法指令译码后,执行部件先访问内存,取得第一个操作数;之后再一次访问内存,
    取得第二个操作数;随后才能进行求和运算。
    
    而在SIMD型的CPU中,指令译码后几个执行部件同时访问内存,一次性获得所有操作数进行运算。这个特点使SIMD特别适合于多媒体应用等数据密集型运算。

    (1)SISD

         一次指令操作一个数据。如下例子4次指令操作才完成8个寄存器相加:

        addro, r5
        add r1,r6
        addr2, r7
        addr3, r8        

    (2)SIMD(vector mode)

        一次指令可以处理多个数据,但是每个数据处理是顺序执行,如下:

        VADD.F32S24, S8, S16

        //S24=S8+S16
        //S25=S9+S17
        //S26=S10+S18
        //S27=S11+S19

        一个指令,但是数据相加是顺序执行。在ARM上这个也叫"Vector Floating Point(VFP)。"

    (3)SIMD(packed data mode)

        一次指令可以处理多个数据,由于使用大寄存器方式可以同时进行,如下:

        VADD.I16Q10, Q8, Q9

        一个指令将两个64-bit寄存相加,I16表示数据类型int16,64-bit= 4 * 16,每个寄存器里4个16-bit lanes独立相加,但是同时完成

    "在ARM上这个叫做增强型SIMD技术或NEON技术。"

/*
 * NEON
 */

    NEON指令是专门针对大规模到并行运算而设计的。
    
    NEON 技术可加速多媒体和信号处理算法(如视频编码/解码、2D/3D 图形、游戏、音频和语音处理、图像处理技术、电话和声音合成),
    其性能至少为ARMv5 性能的3倍,为 ARMv6 SIMD性能的2倍。

/*
 * NEON 寄存器
 */
    有16个128位四字到寄存器Q0-Q15,32个64位双子寄存器D0-D31,两个寄存器是重叠的,在使用到时候需要特别注意,不小心就会覆盖掉。

    两个寄存器的关系:Qn =D2n和D2n+1,如Q8是d16和d17的组合

/*
 * NEON 数据类型SSSSSS
 */
    注意数据类型针对到时操作数,而不是目标数,这点在写的时候要特别注意,很容易搞错,尤其是对那些长指令宽指令的时候,因为经常Q和D一起操作。

                        8 bit    16 bit        32 bit        64 bit
    无符号整数            U8        U16            U32            U64
    有符号整数            S8        S16            S32            S64
    未指定类型的整数    I8        I16            I32            I64
    浮点数                不可用    不可用        F(或F32)    不可用
    {0,1}上的多项式        P8        P16            不可用        不可用


/*
 * NEON中的正常指令、宽指令、窄指令、饱和指令、长指令
 */
    正常指令(q):生成大小相同且类型通常与操作数向量相同到结果向量

    长指令(l):对双字向量操作数执行运算,生产四字向量到结果。所生成的元素一般是操作数元素宽度到两倍,并属于同一类型。L标记,如VMOVL。

    宽指令(w):一个双字向量操作数和一个四字向量操作数执行运算,生成四字向量结果。W标记,如VADDW。

    窄指令(n):四字向量操作数执行运算,并生成双字向量结果,所生成的元素一般是操作数元素宽度的一半。N标记,如VMOVN。

    饱和指令(q):当超过数据类型指定到范围则自动限制在该范围内。Q标记,如VQSHRUN


/*
 * NEON指令
 *
 *    V开头的都是A32/T32指令集
 */
    复制指令:

        VMOV:

            两个arm寄存器和d之间

                vmov d0, r0, r1:将r1的内容送到d0到低半部分,r0的内容送到d0到高半部分

                vmov r0, r1, d0:将d0的低半部分送到r0,d0的高半部分内容送到r1

                一个arm寄存器和d之间

                vmov.U32 d0[0], r0:将r0的内容送到d0[0]中,d0[0]指d0到低32位

                vmov.U32 r0, d0[0]:将d0[0]的内容送到r0中

            立即数:

                vmov.U16 d0, #1:将立即数1赋值给d0的每个16位

                vmov.U32 q0, #1:将立即数1赋值给q0的每个32位

                长指令:VMOVL:d赋值给q

                vmovl.U16 q0, d0:将d0的每个16位数据赋值到q0的每个32位数据中

                窄指令:VMOVN:q赋值给d

                vmovn.I32 d0, q0:将q0的每32位数据赋值到q0的每16位数据中

                饱和指令:VQMOVN等,饱和到指定的数据类型

                 vqmovun.S32 d0, q0:将q0到每个32位移动到d0中到每个16位中,范围是0-65535
            

        VDUP:

            VDUP.8 d0, r0:将r0复制到d0中,8位

            VDUP.16 q0, r0:将r0复制到q0中,16位

            VDUP.32 q0, d2[0]:将d2的一半复制到q0中

            VDUP.32 d0, d2[1]:将d2的一半复制到d0中

            注意是vdup可以将r寄存器中的内容复制到整个neon寄存器中,不能将立即数进行vdup,立即数只能用vmov
    

    逻辑运算:

        VADD:按位与;VBIC:位清除;VEOR:按位异或;VORN:按位或非;VORR:按位或


    移位指令:

        VSHL:左移、VSHLL:左移扩展、VQSHL:左移饱和、VQSHLU:无符号左移饱和扩展

        VSHR:右移、VSHRN:右移窄、VRSHR:右移舍入、VQSHRUN:无符号右移饱和舍入


    通用算术指令:

        VABA:绝对值累加、VABD:绝对值相加、VABS:绝对值、VNEG:求反、VADD、VADDW、VADDL、VSUB、VSUBL、VSUBW:加减

        VPADD:将两个向量的相邻元素相加

        如VPADD.I16 {d2}, d0, d1

        VPADDL:VPADDL.S16 d0, d1

        VMAX:最大值,VMIN:最小值

        VMUL、VMULL、VMLA(乘加)、VMLS(乘减)、


    加载存储指令:

        VLD和VST
        
        VREV反转元素指令:
        
        VEXT移位指令:
        
        VTRN转置指令:可以用于矩阵的转置
        
        VZIP指令:压缩,类似交叉存取
        
        VUZP指令:解压操作,类似交叉存取
        
        VTBL查表指令:从d0,d1中查找d3中的索引值,如果找到则取出,没有找到则为0,存入d2中

/*
 * 需要注意的地方
 */
    load数据的时候,第一次load会把数据放在cache里面,只要不超过cache的大小,下一次load同样数据的时候,则会比第一次load要快很多,
    会直接从cache中load数据,这样在汇编程序设计的时候是非常需要考虑的问题。

    如:求取一个图像的均值,8*8的窗口,先行求和,然后列求和出来均值,这时候会有两个函数,数据会加载两遍,如果按照这样去优化的话则优化不了多少。
    如果换成上面这种思路,先做行16行,然后再做列,这样数据都在cache里面,做列的时候load数据会很快。

    在做neon乘法指令的时候会有大约2个clock的阻塞时间,如果你要立即使用乘法的结果,则就会阻塞在这里,在写neon指令的时候需要特别注意。
    乘法的结果不能立即使用,可以将一些其他的操作插入到乘法后面而不会有时间的消耗。

    如:vmul.u16 q1, d3, d4

        vadd.u32 q1, q2, q3

    此时直接使用乘法的结果q1则会阻塞,执行vadd需要再等待2个clock的时间

    使用饱和指令的时候,如乘法饱和的时候,在做乘法后会再去做一次饱和,所以时间要比直接做乘法要慢。

        如:vmul.u16 q1, d3, d4

            vqmul.u32 q1, q2, q3

    后一个的时间要比第一个的时间要久。

    在对16位数据进行load或者store操作的时候,需要注意的是字节移位。比如是16位数据,则load 8个16位数据,如果指定寄存器进行偏移,此时需要特别注意。

        例如:vld1.64 {d0}, [r0], r1


/*
 * 常用的编译器选项配置
 */

    自动向量化选项

     armcc编译器使用–vectorize选项来使能向量化编译,一般选择更高的优化等级如-O2或者-O3就能使能–vectorize选项。

     gcc编译器的向量化选项-ftree-vectorize来使能向量化选项,使用-O3会自动使能-ftree-vectorize选项。

    选择处理器类型

     armcc编译器使–cpu 7-A或者–cpu Cortex-A8来指定指令集架构和CPU类型。

     gcc编译器的处理器选项-mfpu=neon和-mcpu来指定cpu类型。如-mcpu=cortex-a5

    选择NEON和VFP类型

     gcc选择用-mfpu=vfpv3-fp16来指定为vfp协处理,而-mfpu=neon-vfpv4等就能指定为NEON+VFP结构。

    选择浮点处理器和ABI接口类型

     -mfloat-abi=soft使用软件浮点库,不是用VFP或者NEON指令;-mfloat-abi=softfp使用软件浮点的调用规则,而可以使用VFP和NEON指令,
                        编译的目标代码和软件浮点库链接使用;

     -mfloat-abi=hard使用VFP和NEON指令,并且改变ABI调用规则来产生更有效率的代码,如用vfp寄存器来进行浮点数据的参数传递,
                        从而减少NEON寄存器和ARM寄存器的拷贝。

/*
 * 常用的CPU类型编译器选项
 */
    CPU类型     CPU类型选项     FP选项     FP + SIMD选项     备注
    Cortex-A5     -mcpu=cortex-a5     -mfpu=vfpv3-fp16
    -mfpu=vfpv3-d16-fp16     -mfpu=neon-fp16     -d16表明只有前16个浮点寄存器可用
    Cortex-A7     -mcpu=cortex-a7     -mfpu=vfpv4
    -mfpu=vfpv4-d16     -mfpu=neon-vfpv4     -fp16表明支持16bit半精度浮点操作
    Cortex-A8     -mcpu=cortex-a8     -mfpu=vfpv3     -mfpu=neon     
    Cortex-A9     -mcpu=cortex-a9     -mfpu=vfpv3-fp16
    -mfpu=vfpv3-d16-fp16     -mfpu=neon-fp16     
    Cortex-A15     -mcpu=cortex-a15     -mfpu=vfpv4     -mfpu=neon-vfpv4     

/*
 * NEON汇编和EABI程序调用规范
 */
GNU assembler (gas) and ARM Compiler toolchain assembler(armasm)都支持NEON指令的汇编。但必须遵循
    ARMEmbedded Application Binary Interface (EABI)EABI的规范,即NEON寄存器的S0-S15 (D0-D7, Q0-Q3)用于传递参数和返回值,
    被调用函数内可以直接使用,不用保存;D16-D31 (Q8-Q15)则有调用函数来保存,被调用函数内可以不保存的随意使用;而S16-S31(D8-D15, Q4-Q7)
    则必须由被调用函数内部保存。对于调用传参规范则有,对于软件浮点,参数有R0~R3和堆栈stack传递,而硬件浮点,可以通过NEON寄存器来传递参数。


/*
 * neon使用方法
 */

(1)汇编

     通过汇编直接时钟neon指令:

    .text

    .arm

    .global double_elements

    double_elements:

    vadd.i32 q0,q0,q0

    bx  lr

    .end

    arm-hisiv300-linux-as -mfloat-abi=softfp-mfpu=neon-vfpv4 neon_as.S
    arm-hisiv300-linux-as -mfloat-abi=softfp-mfpu=neon-vfpv4 neon_as.S

    该种方式效率最高,但是难度大,移植性差

(2)使用arm提供的Intrinsics函数

    可以认为是内联函数,但是在编译时编译器会将函数转化为neon指令。调用该函数需要包含头文件arm_neon.h,该头文件包含了neon各种操作函数,
    具体可以该头文件arm-hisiv300-linux/lib/gcc/arm-hisiv300-linux-uclibcgnueabi/4.8.3/include/arm_neon.h或参考
    文档《neon_programmers_guide.pdf》附录D。

        简单例子:

        #include <arm_neon.h>

        uint32x4_t double_elements(uint32x4_tinput)

        {

                 return(vaddq_u32(input,input));

        }

        arm-hisiv300-linux-gcc -mfloat-abi=softfp-mfpu=neon-vfpv4 -c neon_in.c

(3)自动化向量(实际验证未通过)

         该方式需要对指针参数添加__restrict(用于限定和约束指针,表明指针是访问一个数据对象的唯一且初始的方式)。


/*
 * neon应用例子
 */
    在《neon_programmers_guide.pdf》文档中举了好多个neon使用的例子:

    A.交换RGB颜色通道

    B.处理非对齐数组

    C.矩阵计算

    D.向量积

    E.转换色彩深度

    F.中值滤波

    G.FIR滤波

/*
 * 官方参考文档
 */

    Neon资料比价少,在arm官网上可以查到如下几个资料,第一个是详细说明。

    《neon_programmers_guide.pdf》

    《introducing_neon.pdf》

    《neon_support_in_compilation_tools.pdf》

 类似资料: