【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详细解释
数据类型
对于数据类型的命名,一般遵循这样的规则:
<基本类型>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/bash
export ANDROID_NDK=/opt/env/android-ndk-r14b
rm -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 install
cd ..
将生成的可执行文件main,push到设备端进行运行,最终的运行结果:
cost total time : 112538 microseconds -- common method
cost total time : 44217 microseconds -- neon method
可以看出,使用Neon指令集优化,省下了近60.71%的运行时间。
相关阅读
上一篇: python 通过元类控制类的创建
下一篇: xUtils框架网络获取