• Introducing NEON Development Article翻译


    # Introducing NEON Development Article

    介绍NEON的开发文章

    链接: https://pan.baidu.com/s/1iHmmvnlLziSPPNMgaYGEoA  密码: 5asd

    出版时间:2009年。

    目录:

    第一章 介绍 NEON

    1.1 什么是SIMD

    1.2 什么是NEON

    1.3 NEON架构概况

    1.4 面向NEON的开发

    Chapter 1 第一章

    Introducing NEON 介绍NEON

    这里引入NEON技术,它首先在ARM Cortex-A8处理器里实现。除了介绍NEON架构,还给出单指令多数据(SIMD)的概念,从比较高的层次介绍了怎么利用它。包括这些小节:

    1. SIMD是什么?

    2. NEON是什么?

    3. NEON架构概况

    4. 面向NEON的开发

    1.1 SIMD是什么?

    典型的用处:多媒体编解码,图形加速,超大规模数据处理。音频处理常见的16比特数据,图形和视频处理常见的8比特数据。

    当在32位微处理器上做这些操作时,有些计算单元没有被用到,但是仍然耗电。为了充分利用资源,SIMD技术用单个指令执行同时执行多个同样的操作(需要同样类型、同样尺寸的数据)。这样,硬件本来要做32位加法的,改为同时做48位加法,并且时间开销和做一个8位加法是一样的。

    1.1.1 ARM SIMD指令

    ARMv6架构引入了少量SIMD指令,是把多个16位或8位的数据打包为32位,然后放在32位通用寄存器上处理。这样看来可以有2倍或4倍加速。举个例子,UADD8 R0, R1, R2,这条指令是说把R1R2这两个寄存器里的值相加,其中R0R1都看做4个通道(lane)的向量,具体相加是逐通道(lane)相加,不同通道互不干扰:

    1.2 NEON是什么?

    ARMv7架构引入了Advanced SIMD extension(高级SIMD扩展),作为ARMv7-AARMv7-R的扩展。它怎么扩展了SIMD的概念呢?定义了在64D向量寄存器和128Q向量寄存器上的操作。



    ARM处理器上实现的ASIMD扩展又叫做NEON,在所有的ARM Cortex-A系列上都支持。



    NEON指令被作为ARMThumb指令流来执行,相比于使用外部加速器,这样做能简化开发、调试和集成。NEON指令能做的事情包括这些方面:

    - 内存访问

    - NEON向量寄存器和通用寄存器之间的数据相互拷贝

    - 数据类型转换

    - 数据处理



    举例:VADD.I16 Q0, Q1, Q2 指令意思是,每个lane16bit,每个对应lane位做加法,一共有8个这样的lane同时执行;第一个8lane放在Q0向量寄存器里,第二个8lane放在Q1向量寄存器里,结果存放在Q0向量寄存器里:

    1.3 NEON架构概况

    ARM架构定义ASIMD扩展作为第1011代的架构的协处理器的一部分,而这些协处理器也用于向量浮点扩展(Vector Floating Point extensionVFP)。实际上不必同时实现VFPNEON,通常来说支持VFP的系统只需要稍微修改(甚至不用修改)就支持NEON



    在为特定处理器优化NEON代码时,您可能必须考虑该处理器如何集成NEON技术的实现定义方面。啥意思呢?假设有AB两个ARM处理器,你给A处理器写的NEON优化代码,在B处理器上跑,得到的时间开销很可能跟A上的不一样,即使AB两个处理器的每个指令的指令周期数是完全一样的。 (个人理解为,AB两个处理器频率有差别,因而相同的指令周期对应的实际耗时仍不一样)



    关于ASIMD扩展的更多细节需要翻看 ARM Architecture Reference Manual ARMv7-A and ARMv7-R edition,里面列出了指令和编码。具体请到 http://infocenter.arm.com 查找。



    1.3.1 支持的数据类型

    NEON指令支持8位、16位、32位、64位的有符号和无符号整数。

    NEON也支持32位的单精浮点类型,和8位、16位的多项式(?polynomial,不太懂)

    有一个NEON指令,VCVT,用来做NEON数据类型转换,包括这些类型到32位单精度浮点的转换:

    - 32位整数

    - 定点的

    - 半精度的浮点数(如果处理器实现了半精度扩展的话,也就是fp16



    1.3.2 NEON寄存器

    NEON寄存器组(register bank),由3264位的寄存器组成。如果ASIMDVFPv3都被处理器所支持,那么VFPv3的浮点寄存器和ASIMD的向量寄存器是重合的。(ASIMD就是NEON的意思)。 这种情况下,VFPv3被实现为VFPv3-D32,意思是支持32个双精度浮点寄存器。啥好处呢?恢复VFP上下文的机制,和恢复NEON上下文的机制,共享使用,实现一个就可以了。



    NEON向量寄存器可以整块使用,也可以切开来用,不过不是随便切,是单次或多次折半那种。所有的NEON向量寄存器组,因而有多种叫法:

    - 16128位的Q寄存器,Q0-Q15

    - 3264位的D寄存器,D0-D31

    其中NEOND0-D31向量寄存器,和VFPv3D0-D31浮点寄存器是完全等同的。Q0-Q15的向量寄存器,每一个Q寄存器对应到一对(挨着的两个,而不是随意两个)D向量寄存器。具体写代码的时候,完全等同的寄存器,不用手动切换区分。下图展示了上面说的等同映射关系和整体情况:

    1.3.3 NEON指令

    NEON指令从功能上来说很少,为啥呢?因为只提供了数据读取、数据存储、数据处理的功能,并且是集成到ARMThumb指令集里头的。标准的ARMThumb指令管理了所有的程序流控制。(这说的很绕,其实就是说,NEON相当于是ARM汇编的扩展,可以在内联汇编或单个汇编文件里写NEON,也可以用intrinsic这种类似API调用的方式来写NEON)。



    很多NEON指令都是可以处理多种数据类型的(功能相同,向量长度相同,由于数据类型长度不一样,因而lane数量也不一样),具体表现为在指令后面紧跟着处理的每个lane的比特数,例如:

    VADD.I16 q0, q1, q2

    意思是是把q0, q1, q2都看做有8lane的、每个lane16bit的向量寄存器,然后q1q2的对应lane相加,结果放到q0的对应lane上。



    还有一些NEON指令,它们的输入、输出的向量寄存器大小不一样。比如:

    VMULL.S16 Q0, D2, D3

    意思是每个lane宽度是16bit的情况下,对应lane相乘,得到32位(而不是16位)的结果;D2D3都是416bitlane,而Q0则是432bitlane



    为了提升性能和代码密度,NEON指令里的数据加载、数据写入(内存和向量寄存器之间,普通寄存器和向量寄存器之间)支持一次多写多个数据。还包括在多写多个数据时,交叉存放的指令。(初学者看到交叉存放肯定懵逼,稍微画个图):



    假设内存里的uchar数组data保存了RGBRGBRGB...顺序的数据,被vld3_u8指令读取,会存放在3个分别是RGB通道数据的向量寄存器;如果是三次调用vld1_u8,偏移量分别是0816,则目标向量寄存器里读进去的,仍然是RGB交错的数据;vld3_u8是“交错读取”的指令,对应的写入指令vst3_u8是它的逆过程(从3个向量寄存器v1.val[0], v1.val[1], v2.val[2]交错的取数据写入到连续的内存空间)。

    1.4 面向NEON开发

    为了使用NEON这样的新特性,你得用新一点的编译工具,比如GNU的工具,或RealView的编译工具(当前时间2021-04-05 14:28:05Android NDK已经切换到Clang了,2333,编译器发展日新月异)。

    1.4.1 汇编器

    在汇编里写NEON是很直接的一种方式。正是因为NEON指令集和汇编在设计上的连贯性,使得在汇编里写起NEON往往比预期的要容易。



    GNURVCT汇编器使用同样的指令形式,但指令形式之外的语法就不一样了。区别包括:

    - 汇编指令(assembler directives)

    - 标签形式(format of labels)

    - 注释写法(comment indicators)

    1-1显示的是用GNU汇编器(Gas)执行NEON指令写成的函数,例1-2则给出了RVCT格式的写法。



    1-1 Simple NEON Assembler example for Gas

    -----------------------------------------------------------------------------------------------------------------------

    .text

    .arm

    .global double_elements

    double_elements:

    vadd.i32 q0, q0, q0

    bx lr

    .end

    -----------------------------------------------------------------------------------------------------------------------

    Gas汇编器汇编例1-1的代码时,需要传入 -mfpu=neon 命令行参数,意思是开启NEON指令。例如:

    arm-neon-linux-gnueabi-as -mfpu=neon asm.s



    1-2 Simpe NEON assembler example for RVCT

    -----------------------------------------------------------------------------------------------------------------------

    AREA RO, CODE, READONLY

    ARM

    EXPORT double_elements

    double_elements

    VADD.I32 Q0, Q0, Q0

    BX LR

    END

    -----------------------------------------------------------------------------------------------------------------------

    RVCT汇编1-2的代码时,需要指定目标处理器为支持NEON指令集的处理器,例如:

    armasm --cpu=Cortex-A8 asm.s







    1.4.2 Intrinsic(不太好翻译,通常也不翻译)

    Intrinsic函数和数据类型,统称为intrinsics(加了s的复数形式),提供了和内联函数类似的功能,并且提供了额外的特征比如类型检查、自动寄存器分配。每个Intrinsic函数看起来是C/C++调用,但在编译阶段会被底层指令所替代。这意味着什么?意味着你可以用(相对)high-level的语言来表达low-level的体系结构行为(说的这么绕,不就是用看似函数的写法表达底层NEON汇编指令吗?)



    Intrinsic函数,和用NEON汇编,有啥区别?除了能用high-level的类似函数调用的方式来访问指令,还有一点区别是,编译器可以优化Intrinsic而汇编则不被优化。(也就是通常写的asm volatile或者单独放在.s为后缀的汇编文件中)。使用Intrinsic意味着开发者不需要考虑寄存器分配和连锁问题(interlock issues,没懂),因为编译器会处理这些事情。



    GCCRVCT支持相同的NEON intrinsic语法,使得C/C++代码在两种工具链之间有很好的兼容性。要使用NEON intrinsics,需要包含头文件 arm_neon.h 。例1-3CIntrinsic实现了和前面汇编例子同样的功能:



    1-3 Example NEON Intrinsics

    -----------------------------------------------------------------------------------------------------------------------

    #include <arm_neon.h>

    uint32x4_t double_elements(uint32x4_t input)

    {

    return (vaddq_u32(input, input));

    }

    -----------------------------------------------------------------------------------------------------------------------

    注意,GCCRVCT两个工具链都能正确识别例1-3的代码,区别仅仅在于给汇编器传入的参数不一样,其中:

    GCC编译NEON Intrinsics

    必须制定-mfpu=neon命令行参数:

    arm-none-linux-gnueabi-gcc -mfpu=neon intrinsic.c

    此外你可能还需要考虑你的toolchain的其他参数,有可能需要手动增加传参-mfloat-abi=softfp,表示的是NEON变量必须被送给通用寄存器(general purpose registers)。(没动。。)

    完整的被支持的Intrinsics可以在gnu在线手册里找到:

    http://gcc.gnu.org/onlinedocs/gcc/ARM-NEON-Intrinsics.html





    NEON Intrinsics with RVCT

    RVCT要编译NEON intrinsic的话,需要在命令行参数里传入支持NEON指令的CPU,例如:

    armcc –cpu=Cortex-A9 intrinsic.c

    完整的受支持的Intrinsic函数和向量数据类型,请看RealView Compilation Tools Compiler Reference Guide,在线网址为:

    http://infocenter.arm.com



    1.4.3 自动向量化

    编译器也可以对C/C++代码执行自动向量化。也就是说,不写NEON汇编,也不写NEON Intrinsics,(满足条件下)也能达到NEON一样的高性能。这样一来,代码的可移植性就更强了,包括不同的工具、不同的平台,都友好。

    1-4 NEON向量化

    ------------------------------------------------------------------------------------------------------------------------

    void add_ints(int * __restrict pa, int * __restrict pb, unsigned int n, int x)

    {

    unsigned int I;

    for(i=0; i<(n&~3); i++) {

    pa[i] = pb[i] + x;

    }

    }

    ------------------------------------------------------------------------------------------------------------------------

    1-4给出的例子意在说明,编译器可以安全的、最佳地向量化的一个小函数。为啥呢?首先papb都是用__restrict修饰,意思是说papb不会有相互重叠的内存部分。同时,循环次数表示的是磨掉了后两位二进制,使得循环次数一定是4的倍数。这些因素使得编译器可以安全地向量化NEON的读写操作。



    编译例1-4的代码

    和先前类似,GCCRVCT都能识别上述代码,但是要开启编译时自动向量化展开的特性,传入的编译参数是不一样的。对于GCC来说是:

    arm-none-linux-gnueabi-gcc -mfpu=neon -ftree-vectorize -c vectorized.c

    同时,你可能还需要传入-mfloat-abi=softfp,表示说NEON变量必须被放在通用寄存器上。

    此外,你还可以传入 -ftree-vectorizer-verbose=1 表示让编译器输出“它已经做了向量化的代码”,以及输出“它无法做向量化的代码,以及为什么不能向量化”。有些版本的GCC还支持大于1的值;利用这个参数,可以观察和尝试优化更多的代码。

    对于RVCT来说,编译器开启自动向量化的参数里,必须指定支持NEON指令的CPU,以及传入-O2或更高的优化选项,以及传入-Otime--vectorize参数,例如:

    armcc --cpu=Cortex-A9 -O3 -Otime --vectorize -c vectorized.c



    注意!当指定--vectorize,必须还同时指定了-Otime-O2-O3的优化,才能开启自动向量化。

    此外,传入--fpmode=fast会给输入排序,可能导致浮点精度差异。这个参数默认是没开启的。

    再有就是传入--remarks命令行参数给RVCT编译器,会提供更多详情,对于NEON向量化来说包括:

    - 哪些代码被向量化了

    - 哪些代码没有被向量化,以及为什么没有被向量化



    1.4.4 使用NEON优化的库

    在你的系统里,使用NEON技术的最简单方式,是直接用NEON优化过的库。一个具体的例子是,使用OpenMAX(open media acceleration),可以从 http://www.arm.com 下载。(没听说过T_T



    1-5计算两个16位整数的点积,是通过调用OpenMAX的函数omxSP_DotProd_S16( )做到的。这个函数使用NEON向量化操作实现的。



    1-5 OpenMAX example

    ----------------------------------------------------------------------------------------------------------------------

    #include <omxSP.h>

    OMX_S16 sources1[] = {42, 23, 983, 7456, 124, 11111, 4554, 10002};

    OMX_S16 source2[] = {242, 423, 9832, 746, 1124, 1411, 2254, 1298};

    OMX_S32 source_dotproduct(void)

    {

    OMX_INT len = sizeof(source1)/sizeof(OMX_S16);

    return omxSP_DotProd_S16(source1, source2, len);

    }

    ----------------------------------------------------------------------------------------------------------------------

    Greatness is never a given, it must be earned.
  • 相关阅读:
    IntelJ idea下lombok 不生效的问题(@Builder等注解不生效的问题)解决,lombok Plugin插件安装
    Lombok使用
    Windows系统配置.bat启动spring boot项目jar
    SpringBoot2.X + SpringCache + redis解决乱码问题
    mac安装Redis可视化工具-Redis Desktop Manager
    带你轻松了解C# Lock 关键字
    Nginx + Docker 多阶段构建的部署学习
    WPF在Gmap.net中将Marker动起来
    WPF使用 Gmap.NET 绘制极坐标运动轨迹
    使用MySql对IdentityServer4进行持久化
  • 原文地址:https://www.cnblogs.com/zjutzz/p/14623817.html
Copyright © 2020-2023  润新知