中文
注册
我要评分
文档获取效率
文档正确性
内容完整性
文档易理解
在线提单
论坛求助
鲲鹏小智

NEON指令加速

NEON是一种基于SIMD思想的技术,能够基于单条指令对多个数据同时进行操作,其使用的 NEON指令 类似于Intel CPU下的MMX/SSE/AVX指令,通过向量化的计算方式优化应用程序性能,通常应用于图像处理、音视频处理、数据并行处理等需要大量计算场景。

NEON技术依赖于128位NEON寄存器的硬件支持,NEON寄存器是一种向量寄存器,一个寄存器中可存储多个数据元素,但要求其具有相同的数据类型。

以下是ARMv8-A中AArch64架构下的寄存器:

  • 64位寄存器:(通用寄存器)

    ARMv8有31个64位通用寄存器,1个特殊寄存器。因此可以看成31个64位的X寄存器或者31个32位的W寄存器(X寄存器的低32位)。

  • 128位寄存器:(向量寄存器)

    ARMv8有32个128位的V寄存器,同样也可以看成是32个32位的S寄存器或者32个64位的D寄存器。

使用编译器能力自动向量化加速

原理

编译器支持自动向量化功能,其会自动利用NEON属性,编译时将代码向量化。启用自动向量化功能前需要打开相应的编译选项,且并非所有代码均可向量化,其需要符合一定的编码方式和规律,以提供更多的提示信息给编译器,进一步触发编译器进行代码的向量化。

支持该特性的编译器有:GCC、LLVM、适用于嵌入式和Linux项目的ARM编译器。

修改方式:

  • 自动向量化编译选项使能
    • GCC编译器使用-O3会自动使能-ftree-vectorize选项,在-O1和-O2下需要添加-ftree-vectorize选项才能进行向量化。在-O0模式下,即使添加-ftree-vectorize也无法进行向量化。
    • armcc编译器使用-vectorize选项来使能向量化编译,一般选择更高的优化等级如-O2或者-O3就能使能-vectorize选项。在-O1模式下需要使用-vectorize选项使能向量化编译,在-O0模式下,即使添加-vectorize选项编译器同样无法进行向量化。

    在Armv8-a的AArch64架构下才支持双浮点计算的向量化,其他架构下非必需时避免使用双浮点的数据类型,该类型会阻止编译器做向量化。各架构下支持的数据类型如下:

    -

    Armv7-A/R

    Armv8-A/R

    Armv8-A

    -

    -

    aarch32

    aarch64

    Floating-point

    32-bit

    16-bit/32-bit

    16-bit/32-bit/64-bit

    Integer

    8-bit/16-bit/32-bit

    8-bit/16-bit/32-bit/64-bit

    8-bit/16-bit/32-bit/64-bit

    arch命令下可查看CPU硬件架构是AArch64还是AArch32。

  • 编码方式上触发代码向量化
    1. 循环次数在已知时要直接传递常数,而不使用变量,让编译器预先明确循环迭代次数。循环次数是2的指数倍时,需告知编译器,以便尽可能的向量化。在循环次数非2的指数倍时,也可将循环分解进行构造。
      void vecAdd(int *vecA, int *vecB, int *vecC, int len) 
      {
          int i;
          // 告诉编译器len是4的整数倍
          for (i = 0; i < len * 4; i++) {  
              vecC[i] = vecA[i] + vecB[i];
          }
       }
    2. 在控制循环结束的条件中,尽量使用 "<"来进行条件判断,而不使用"<="或"!=",使用 "<"能使编译器识别到在该变量值之前循环结束,这有助于编译器进行向量化。
    3. 使用restrict关键字

      为指针添加__restrict或__restrict__关键字,提示编译器,对象已经被指针所引用,不能通过除该指针外所有其他直接或间接的方式修改该对象的内容,编译器以此获知当前对象无其他依赖,可并行操作和向量化。但使用前必须确保确实没有指针访问区域重叠的现象,否则计算结果可能会出错。

      void vecAdd(int *__restrict__ vecA, int *__restrict__ vecB, int *__restrict__ vecC, int len)
      {
          int i;
          for (i = 0; i < len *4; i++) {
              vecC[i] = vecA[i] + vecB[i];
          }
      }
    4. 避免循环依赖(即某次循环的结果会被前一次循环的结果影响)。
    5. 在满足需求情况下,使用尽可能小的数据类型,以便向量化后,NEON寄存器一次能处理更多数据,提升向量化后代码性能。
    6. 避免在循环中出现条件判断,尽量少用break跳出循环。
    7. 编写简单的代码,编译器更容易理解与自动向量优化。(向量化程度取决于编译器所理解编码人员代码意图的程度。)
    8. 用数组下标来替代指针访问元素。
    9. 构造结构体时,可尽量保持结构体内变量的数据类型一致,便于数据加载时向量化。

      如下为像素点数据结构体做4字节对齐,采用以下方式可进行向量化:

      struct aligned_pixel {
          char r;
          char g;
          char b;
          char not_used;  /* Padding used to keep r aligned to a 32-bit word */
      }screen[10];

      若只改变结构体内单个元素变量类型进行数据对齐,导致结构体内变量数据类型不同,则无法进行自动向量化:

      struct pixel {
          char r;
          short g; /* Green channel contains more information */
          char b;
      }screen[10];

使用NEON intrinsic加速提升性能

原理

NEON intrinsic函数是一系列C函数调用,编译器可将其替换为适当的NEON指令或NEON指令序列。NEON intrinsic函数几乎提供与编写NEON汇编指令相同的功能,但是将寄存器分配等工作留给编译器,以便开发人员可以专注于算法开发。与使用NEON汇编指令编码相比,NEON intrinsic方式的代码有更好的可维护性。ARM编译器、GCC和LLVM编译器都支持NEON intrinsic。

修改方式

在使用NEON intrinsic函数时需要增加头文件#include <arm_neon.h>,详细的NEON intrinsic函数列表和使用方法,可参考NEON Intrinsic Reference:https://developer.arm.com/architectures/instruction-sets/simd-isas/neon/intrinsics

搜索结果
找到“0”个结果

当前产品无相关内容

未找到相关内容,请尝试其他搜索词