【CPU】ARM底层汇编之neon指令集优化(附实例)

编程入门 行业动态 更新时间:2024-10-10 23:19:02

【CPU】ARM<a href=https://www.elefans.com/category/jswz/34/1768082.html style=底层汇编之neon指令集优化(附实例)"/>

【CPU】ARM底层汇编之neon指令集优化(附实例)

当在ARM芯片上进行一些例如图像处理等计算的时候,常常会因为计算量太大造成计算帧率较低的情况。因而,需要选择一种更加简单快捷的计算方式以获得处理速度上的提升。ARM NEON就是一个不错的选择。


Neon指令优化

NEON是一种SIMD(Single Instruction Multiple Data)指令,也就是说,NEON可以把若干源(source)操作数(operand)打包放到一个源寄存器中,对他们执行相同的操作,产生若干目的(dest)操作数,这种方式也叫向量化(vectorization)

可能你对这个描述还不够清晰,简单来说,就是:NEON指令优化的精髓就在于同时在不同通道内进行并行运算。通常可用于图像等矩阵数据的循环优化

更简单的说,就是,将Neon寄存器分为多个通道,每个通道存储一个数据。一条对Neon寄存器的计算指令,实际上,是对各通道的数据分别的计算指令。即寄存器位宽,直接影响到数据的通道数。

例如:在ARMv7的NEON unit中,register file总大小是1024-bit,可以划分为16个128-bit的Q-register(Quadword register)或者32个64-bit的D-register(Dualword register),也就是说,最长的寄存器位宽是128-bit。那么,假设我们采用32-bit单精度浮点数float来做浮点运算,那么可以把最多128/32=4个浮点数打包放到Q-register中做运算,即4个4个参与计算,从而提高吞吐量,减少loop次数。


Neon指令的使用

主流支持目标平台为ARM CPU的编译器基本都支持NEON指令。可以通过在代码中嵌入NEON汇编来使用NEON,但是更加常见的方式是通过类似C函数的NEON Instrinsic来编写NEON代码。本文统一采用后者。

硬件平台

本文的例子都是基于ARMV7架构平台。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只有单精度浮点计算能力

头文件和编译选项

在使用NEON Instrinsic来进行编写NEON代码前,需要引入头文件:

#include <arm_neon.h>

同时,在编译的时候,需要指定编译参数。如果使用CMakeLists.txt,可以指定:

set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -mfpu=neon")

关于编译选项,可以参考:ARM平台NEON指令的编译和优化

NEON Instrinsic详细解释

  • ARM NEON Intrinsics
  • ARM汇编笔记(3)——NEON intrinsics 函数
数据类型

对于数据类型的命名,一般遵循这样的规则:

<基本类型>x<lane个数>x<向量个数>_t

其中,向量个数如果省略表示只有一个。

基本类型int8,int16,int32,int64,uint8,uint16,uint32,uint64,float16,float32

lane个数表示并行处理的基本类型数据的个数。

按照上述的规则,比如:

float32x4_t 
指令函数

对于指令函数的命名,一般遵循这样的规则:

v<指令名>[后缀]_<数据基本类型简写>

其中,后缀如果没有,表示64位并行;如果后缀是q,表示128位并行;如果后缀是l,表示长指令,输出数据的基本类型位数是输入的2倍;如果后缀是n,表示窄指令,输出数据的基本类型位数是输入的一半

数据基本类型简写:s8,s16,s32,s64,u8,u16,u32,u64,f16,f32。

按照上述的规则,比如:

vadd_u16:两个uint16x4相加为一个uint16x4
vaddq_u16:两个uint16x8相加为一个uint16x8
vaddl_u16:两个uint8x8相加为一个uint16x8
指令名

Neon的指令名主要分为:算术和位运算指令、数据移动指令、访存指令
算术和位运算指令最简单,包括add(加法),sub(减法),mul(乘法)这些基本指令。

实际编程中经常要在不同NEON数据类型间转移数据,有时还要按lane来get/set向量值,NEON intrinsics也提供了这类操作。

  • dup[后缀]n<数据基本类型简写>:用同一个标量值初始化一个向量全部的lane;
  • set[后缀]lane<数据基本类型简写>:对指定的一个lane进行设置
  • get[后缀]lane<数据基本类型简写>:获取指定的一个lane的值
  • mov[后缀]_<数据基本类型简写>:数据间移动

NEON访存指令可以将内存读到NEON数据类型中去,或者将NEON数据类型写进内存。可以支持一次读写多向量数据类型。

  • ld<向量数>[后缀]_<数据基本类型简写>:读内存
  • st<向量数>[后缀]_<数据基本类型简写>:写内存

实例

实例内容:对于1280 * 720 * 3的图片数据,需要对每个像素点进行同样的加法和乘法运算,比较非Neon和Neon两种方式的耗时。
源码:

# include <iostream>
# include <chrono>
# include <random>
# include <arm_neon.h>int main(int argc, char const *argv[])
{float *data_tmp = new float[1080 * 720 * 3];std::default_random_engine e;std::uniform_real_distribution<float> u(0, 255);for(int i = 0; i < 1080 * 720 * 3; ++i) {*(data_tmp + i) = u(e);}float *data = data_tmp;float *data_res1 = new float[1080 * 720 * 3];std::chrono::microseconds start_time = std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::system_clock::now().time_since_epoch());for(int i = 0; i < 1080 * 720 * 3; ++i) {*data_res1 = ((*data) + 3.4 ) / 3.1;++data_res1;++data;}std::chrono::microseconds end_time = std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::system_clock::now().time_since_epoch());std::cout << "cost total time : " << (end_time - start_time).count() << " microseconds  -- common method" << std::endl;data = data_tmp;float *data_res2 = new float[1080 * 720 * 3];start_time = std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::system_clock::now().time_since_epoch());float32x4_t A = vdupq_n_f32(3.4);float32x4_t B = vdupq_n_f32(3.1);for(int i = 0; i < 1080 * 720 * 3 / 4; ++i) {float32x4_t C = (float32x4_t){*data, *(data + 1), *(data + 2), *(data + 3)};float32x4_t D = vmulq_f32(vaddq_f32(C, A), B);vst1q_f32(data_res2, D);data = data + 4;data_res2 = data_res2 + 4;}end_time = std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::system_clock::now().time_since_epoch());std::cout << "cost total time : " << (end_time - start_time).count() << " microseconds  -- neon method" << std::endl;return 0;
}

编写CMakeLists.txt,用于项目编译:

cmake_minimum_required(VERSION 3.0)
project(main)set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++0x")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -mfpu=neon")
add_definitions("-Wall -g")add_executable(${PROJECT_NAME} main.cpp )install(TARGETS ${PROJECT_NAME}RUNTIME DESTINATION ${PROJECT_SOURCE_DIR})

在同级目录下编写main.sh,进行项目编译:

#/bin/bashexport ANDROID_NDK=/opt/env/android-ndk-r14brm -r build
mkdir build && cd build cmake -DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK/build/cmake/android.toolchain.cmake \-DANDROID_ABI="armeabi-v7a" \-DANDROID_PLATFORM=android-22 \..make && make installcd ..

将生成的可执行文件main,push到设备端进行运行,最终的运行结果:

cost total time : 112538 microseconds  -- common method
cost total time : 44217 microseconds  -- neon method

可以看出,使用Neon指令集优化,省下了近60.71%的运行时间。


相关阅读

  • ARM底层汇编优化之NEON优化 - 概述(基础入门 )
  • ARM NEON编程初探——一个简单的BGR888转YUV444实例详解
  • ARM NEON 编程系列2 - 基本指令集

更多推荐

【CPU】ARM底层汇编之neon指令集优化(附实例)

本文发布于:2024-02-07 10:49:45,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/34/1756436.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:底层   实例   指令集   CPU   ARM

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!