ARM NEON在矩阵&向量计算中的加速

一、概述

NEON是ARM上使用的一种SIMD(Single Instruction Multiple Data – 单指令多数据)指令集。可实现64位/128位的并行计算。简单理解就是一个计算指令,可以指定4个Float和4个Float并行计算(也可以是其他数据类型,但是必须包含在64位/128位内),得到4个Float结果。而不是一次只能一个Float和一个Float的计算。

比如在RGB颜色转灰色时,计算公式为:Gray = R*0.299 + G*0.587 + B*0.114,计算过程是由3个float乘法,2个加发组成,共有5个计算指令;如果直接使用NEON指令,就是可以直接通过一个指令计算完成,提升80%的理论性能。

矩阵计算就更为明显,在4×4的矩阵和4个元素的向量相乘时,有16个float乘法和12个加法计算;NEON可以4个指令直接计算,提升的性能更明显。

当然,这种计算需要是一种矩阵或者像素计算密集型的场景,比如RGB图片转黑白色,不通过GPU加速,而是通过CPU计算的场景;有多个3D模型,每帧需要为每个3D模型进行矩阵计算等等。

二、NEON在矩阵&向量中的计算示例

向量的点积运算示例(这里向量以4个元素为例,前3个元素通常表示3D空间的xyz坐标,第4个元素w用于齐次坐标;也可以表示颜色的RGBA)。两个向量分别是:,,向量的点积计算公式:。对应的NEON加速代码如下:

2a9f5ab6977328f8bdb1086b0f2dc6d5.png

类似vdupq_n_f32、vld1q_f32、vmlaq_f32、vadd_f32、vget_lane_f32等等APIs,都是ARM NEON的intrinsics指令,C格式的API。并且这些APIs都定义在arm_neon.h头文件中。ARM NEON指令有两种实现方式,一种就是示例中的Intrinsics指令,另外一种就是直接使用NEON的汇编指令,嵌入到C语言代码中。我们这里只是以Intrinsics指令为例,汇编指令在原理上一样。

三、示例代码中APIs的说明

3.1 ARM NEON向量寄存器

向量寄存器用来存放向量数据,每个向量元素的类型必须相同。这个向量寄存器有128位,AArch64有32个这个寄存器,AArch32/Armv7有16个这个寄存器。

每个寄存器可以表示2个double float类型数据(每个数据占用64位),4个float类型数据(每个数据占用32位),8个short类型数据(每个数据占用16位),16个byte类型数据(每个数据占用8位)。数据类型可以是整形,也可以是浮点数,只要占用位数对齐,类型统一即可。

ca89fec4cc3e0ca4b933427a28746545.png

3.2 示例说明

在计算时,第一步是要把C代码中定义的数据(数组的形式存在,在运行栈中,或者在堆中)加载到向量寄存器中,第二步通过寄存器进行并行计算,第三步把结果写入到指定寄存器,第四步寄存器结果写入C代码对应的变量中(即C语言的栈或者堆中)。

8c2d37e728347055e53f4891ef271f5b.png

第一步:vld1q_f32的意思就是把” A + k”地址指向的内容加载到向量寄存器。f32的意思是,一个值是32位。这个命令是从指定地址,连续复制数据到寄存器,并填满寄存器。比如,这里一个数据是32位,一个寄存器128位,也就是这个命令会连续填充4个f32值。说明:这里是多对(“K”个)向量进行点积计算。

第二步:vmlaq_f32意思是把两个寄存器中,并行4个通道的4个f32分别对应相乘,同时把结果和保存结果的寄存器对应通道进行累加。

第三步:vget_high_f32、vget_low_f32是取寄存器的高位和低位(按照f32的type,分别有2个通道),vadd_f32就是获取高位2通道和低位2通道分别相加,存到一个float32x2_t数据格式用(f32类型,2通道)。vpadd_f32中的p是pairwise,意思是将参数两个向量的相邻数据进行计算,这里就是r自己的2个相邻通道相加。

第四步:vget_lane_f32比较简单,就是获取第一个参数寄存器中指定通道的值。这里就是第0通道的值。并写会到一个float值中。

四、点积的推广

这里的点积相对比较复杂,考虑到了一些通用性。这里使用了一个for循环,当只是计算两个4元素向量的点积时,可以把for循环去掉,vmlaq_f32由vmull_f32替换即可。vmull_f32的原型:Result_t vmull_type(Vector_t N, Vector_t M),Result_t可以是float32x4_t,M和N就是left_vec和right_vec。

如果进行叉乘,则不需要进行第三步,直接返回一个float32x4_t的类型数据即可。

如果计算矩阵(4×4)和向量(4通道)相乘,就是计算点积4次,并且结果分别放到float32x4_t类型的4个通道中。

如果是矩阵(4×4)相乘则是4个叉乘。

这四种情况可以自己根据上方点积的计算方式,独立写出。

五、数据类型和函数指令说明

其实NEON Intrinsics指令中,对使用的变量类型、函数定义做了扩展,便于记忆和理解。

  1. 比如下方的数据类型:

b5fc5865a6f1ed957c1f28dd0f80476c.png

A. int是数据类型,可以是int/uint/float/poly等等。

B. 后边几个数字由‘x’号链接,第一个数字就是每个元素的大小,这里是bit,而非Byte,可以是8/16/32/64。

C. 第二个数字是通道。比如表示颜色的RGBA,就是4通道,每个通道可以用一个byte表示(这里其实就是int8类型)。表示3D空间坐标,可以是xyz,就是3通道。如果是一个2D平面,就是一个xy,2通道了。

D. 最后一个数字表示有多少个。比如一个3D空间坐标xyz,一个四边形有4个顶点,这里就可以表示4(这个值通常是一个2的次幂数)。

这里可以根据实际情况选择自己的数据类型。不过要注意,这里要和128位对齐,符合自己实际数据对齐逻辑,不能超出。

2.函数也有类似的表达方式,例如:

cfb8affbd9353ad3dfdf666b8ba8acb1.png

3f2b51074829cf516b067d1fda3a7089.png

v表示的AArch32/Armv7的指令

p表示pairwise计算。这里表示的是a和b向量的相邻数据进行两两和操作,如下方的操作方式:

6349f4310282b31043d45d7db42e697b.png

add就是加法,加减乘除普通计算,还有一些操作,比如加载、存储、移位、逻辑计算、类型转换等等。

q表示试用128位的向量计算器,不然就使用64位向量寄存器。

s8就是数类型了,可以是:u8、s8、u16、s16、u32、s32、f32、f64。

更多的内容可以在底部参考资料中,找到相关内容。

通过数据类型和函数类型,我们就可以根据实际情况,结合这些函数,封装我们自己的加速代码逻辑,达到优化的目的。

六、总结

这里只是对点积计算方式进行了解析,同时对于其他情况的推广。其实对于int、char等类型可以类比计算。对像素、向量、矩阵等等的计算会成倍提升(理论性能提升16、8、4、2倍不等,根据实际类型确定)。特别是在移动端,图形计算、图形处理领域,CPU性能遇到瓶颈,进行性能优化时,NEON指令是一个不错的优化点。

参考资料:

https://zhuanlan.zhihu.com/p/441686632

https://zhuanlan.zhihu.com/p/431971424

https://blog.csdn.net/yutianzuijin/article/details/79944292

手机投屏之WFD简介

手机主流存储器件的分析与发展

AMD高保真超分算法1.0解密

5c71db8f83318a86c30f44aa25f10073.gif

长按关注内核工匠微信

Linux内核黑科技| 技术文章| 精选教程

版权声明:本文为博主作者:内核工匠原创文章,版权归属原作者,如果侵权,请联系我们删除!

原文链接:https://blog.csdn.net/feelabclihu/article/details/134086435

共计人评分,平均

到目前为止还没有投票!成为第一位评论此文章。

(0)
乘风的头像乘风管理团队
上一篇 2024年4月22日
下一篇 2024年4月22日

相关推荐