• 大前端CPU优化技术--NEON自动向量化


    前言

    ARM NEON技术是一种高级的单指令多数据的架构扩展的实现。它是一种64位和128位混合的SIMD技术,主要应用场景是音视频处理,图像视觉计算,信号处理应用等需要密集计算的场景。

    NEON技术是作为ARM核心的一部分实现的,但是它拥有自己的执行流水线,以及和ARM核心寄存器库不通用的寄存器库。上篇文章中介绍NEON四种调用技术有提到编译器自动向量化技术,可将 C/C++ 代码自动转换为 NEON 指令,这可以大幅减少初学者的入门成本并可以借鉴编译器的优化为后面的指令优化作参考,目前大多数编译器都具有自动向量化的功能,将 C/C++ 代码自动替换为 SIMD 指令。

    本文将从入门成本低的编译器优化手段揭开NEON自动向量化的面纱。

    编译器支持

    GCC和ARM编译工具链提供使能自动向量化的NEON技术的功能,但由于C和c++标准不包括并发性方面,你可能需要为编译器提供额外的信息以使NEON功能得到更大的发挥,所需的代码需改全部是代码语言标准的一部分,所以不会对代码的跨平台移植造成影响。当编译器能确定程序员的意图时,编译器能很好的进行矢量化编译,前提是你必须对编译器指定恰当的参数,下面就分各种情况来说明NEON的使能方式:

    编译器

    GCC,LLVM和ARM编译工具链提供自动向量化的NEON技术的功能,具体如下:

    Arm 编译工具

    armcc编译器

    基于LLVM-clang,最初为高性能计算设计,针对linux下的用户应用程序开发
    LLVM-clang开源,基于LLVM架构的C/C++/Objective-C编译器前端
    GCCGNU编译器套件,开源

    编译器配置

    自动向量化默认不会被启用,开发者需要向编译器提供允许自动向量化的配置来对自动向量化功能进行使用。

    Arm cc编译器中使能自动向量化

            --vectorize          使能矢量化
            --cpu 7-A/Cortex-A8  指定支持NEON功能的处理器架构
            -O2/-O3              指定优化级别
            -Otime               指定优化类型,时间优先

    LLVM 开启自动向量化:

    Android NDK 从 r13 开始以 clang 为默认编译器,本节通过 cmake 调用Android NDK r19c 工具链展示 clang 的自动向量化方法。使用 Android NDK 工具链使能自动向量化配置参数如下:

            -fvectorize  启用自动向量化(-O2 及以上优化等级默认启用)

            -O 自动向量化仅在 -O1 及以上优化等级生效 ,-O1、-O2 等

    在 CMake 中配置自动向量化方式如下:

    1. # method 1
    2. set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -O1 -fvectorize")
    3. set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O1 -fvectorize")
    4. # method 2
    5. set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -O2")
    6. set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O2")

    GCC编译工具自动矢量化

    GCC编译器支持自动矢量化设置,如下:
            -ftree-vectorize
            -mfpu=neon
            -mcpu            指定支持NEON功能的处理器架构

    注:若优化级别设定为-O3则意味着-ftree-vectorize被设定;如果你没有指定-mcpu选项,GCC编译器会指定为内部默认处理器架构。这样的话编译出的程序可能会运行缓慢或者直接不能运行。

    1. # AArch32
    2. arm-none-linux-gnueabihf-gcc -mcpu=cortex-a53 -mfpu=neon -ftree-vectorize -O2 myfile.c
    3. # AArch64
    4. aarch64-none-linux-gnu-gcc -mcpu=cortex-a53 -ftree-vectorize -O2 myfile.c

    自动向量化 

    自动向量化示例

     原函数进行有无向量化操作后,生成的汇编指令如下:

    1. void add (int * __restrict pa, int * __restrict pb, unsigned int n, int x)
    2.         {
    3.             unsigned int i;
    4.             for (i = 0; i < (n & ~3); i++)
    5.                 pa[i] = pb [i] + x;
    6.          }

      注:没有向量优化的代码全是ARM指令实现的,而进行向量优化的代码中有NEON指令用来加速数据的load,add,save等关键耗时操作。 编译器通过矢量化加载 (ldr -> vld1)、求和 (add -> vadd)以及保存 (str -> vst1)等指令,将每次循环中处理的数据变为 4 个,循环次数精简为之前的 1/4。

    自动向量化核心

    从编译技术上来说,自动向量化一般包含两部分:循环向量化(Loop vectorization)超字并行向量化(SLP,Superword-Level Parallelism vectorization,又称Basic block vectorization)

    上面的函数里将代码优化成下面的函数即是自动向量化的核心处理,

    循环向量化:将循环进行展开,增加循环中的执行代码来减少循环次数。如以下代码将循环次数精简到之前的1/4,将pa[i] = pb [i] + x展开增加减少3次循环代价

    SLP 向量化:编译器将多个标量运算绑定到一起,使其成为向量运算。下图将四次标量运算替换为一次向量运算。

    向量化规则

    在C/C++语言中没有指定有关于NEON编程的语法,所以编译器不能安全地生成并行代码,开发者可能需要向编译器提供额外的信息以获取到最好的向量化收益。

    基于一定的编程优化准则,可以更好的协助编译器完成自动向量化的工作,获得理想的性能状态。

    向量化是以确保优化后的代码与非向量化的代码有相同的结果的方式进行的。在某些情况下,为了避免出现不正确的结果,可能需要手动调整代码,使其更适合于自动向量化。有时候开发者会尝试手动优化代码,这也可能导致无法向量化,比如循环展开、开启编译器优化等。

    向量化的规则包括以下几个方面:

    • 简短的代码最容易实现优化
    • 避免在循环中使用带break语句的条件判断
    • 尽量使循环次数是2的幂且循环次数是固定常量
    • 循环内部的函数应该是内联函数
    • 尽量使用带索引的数组而不是指针且间接寻找是不支持向量化的
    • 使用 restrict 关键字来告诉编译器指针不引用内存的重叠区域
    • 避免循环之间的依赖性
    • 浮点数据处理自动向量化条件苛刻

    向量化性能瓶颈

    编译器自动向量化过程和生成代码的性能受到一些因素的影响:

    • 循环的组织方式
    • 数据的结构
    • 循环的迭代次数
    • 数组的类型
    • 多级内存的使用

    大多数应用程序都需要反复的优化以便让编译器优化出性能最佳的代码,但理想总是显得过于丰满,受限于环境,代码等影响,往往无法获取最高性能。

    总结

    自动向量化是一种快速简单的使用NEON加速的方法,减少开发者的学习和入手成本,因为用C/C++代码编写既增强了代码阅读性也增加了可移植性。但其本身优化条件比较苛刻,需要代码具备很好的并行性,对一些复杂代码很难起作用。

    所以想要榨干编译器,获取极致的优化性能就不能完全依赖编译器的不确定性,可将编译器优化作为参考,通过intrinsic甚至neon汇编进行编程才是终极之道。

  • 相关阅读:
    Python实现的人工智能冬奥会对话系统
    想在golang里用好泛型还挺难的
    Redis(一)——初识Redis
    数据结构 第二章作业 线性表 西安石油大学
    Linux14 NAT网络配置原理 查看网络ip和网关 修改ip地址 指定ip方法 主机名与hosts映射 主机名解析过程
    STL的学习之一
    Js逆向教程-11常见混淆AA和JJ
    网络编程_sgu(620-635)
    苍穹外卖项目(黑马)学习笔记DAY10
    一、VUE杂谈一
  • 原文地址:https://blog.csdn.net/jh1988abc/article/details/126268371