您的位置 首页 观点

ARM处理器NEON编程及优化技巧——矩阵乘法的实例

ARM的NEON协处理器技术是一个64128-bit的混合SIMD架构,用于加速包括视频编码解码、音频解码编码、3D图像、语音和图像等多媒体和信号处理

ARM的NEON协处理器技能是一个64/128-bit的混合SIMD架构,用于加快包含视频编码解码、音频解码编码、3D图画、语音和图画等多媒体和信号处理运用。本文首要介绍怎么运用NEON的汇编程序来写SIMD的代码,包含怎么开端NEON的开发,怎么高效的运用NEON。首要会重视内存操作,即怎么改变指令来灵敏有用的加载和存储数据。接下来是由于SIMD指令的运用而导致剩余的若干个单元的处理,然后是用一个矩阵乘法的比如来阐明用NEON来进行SIMD优化,终究重视怎么用NEON来优化各式各样的移位操作,左移或许右移以及双向移位等。本节是一个用NEON优化矩阵乘法的实例。

矩阵

本节将介绍怎么用NEON有用的处理一个4×4的矩阵乘法运算,这种类型的运算常常用于3D图形,咱们以为这些矩阵在内存里是依照列为主摆放的,这是依照OPENGL-ES的通用格局。

矩阵乘法算法

咱们首要看一下矩阵乘法的核算办法,核算的打开,用NEON指令来进行子操作过程。

图1. 以列为主的矩阵乘法运算

由于数据是依照列序存储的,因此矩阵乘法便是把榜首个矩阵的每一列乘以第二个矩阵的每一行,然后把乘积成果相加。乘累加成果 作为成果矩阵的一个元素。

图2. 矩阵乘法中的向量乘以标量的运算

假定每列元素在NEON寄存器中表明为一个向量,那么上述的矩阵乘法便是一个向量乘以标量的运算,而后续的累加也相同可以同向量乘以标量的累加指令完结。由于咱们的操作是在榜首个矩阵的列,然后核算列的成果,读列元素和写列元素都是线性的加载和存储操作,不需求interleave的加载和存储操作。

代码

浮点运算版别

首要看一个单精度浮点的矩阵乘法完结。首要加载矩阵元素到NEON寄存器,然后依照列序做乘法,用VLD1做线性的加载数据到NEON寄存器,用VST1把核算成果保存到内存。

vld1.32 {d16-d19}, [r1]!            @ 加载矩阵0的上8个元素 

vld1.32 {d20-d23}, [r1]!            @ 加载矩阵0的下8个元素 

vld1.32 {d0-d3}, [r2]!              @ 加载矩阵1的上8个元素 

vld1.32 {d4-d7}, [r2]!              @ 加载矩阵1的下8个元素 

NEON有32个64位寄存器,因此加载一切的输入矩阵元素到16个64-bit寄存器,咱们依然有16个64位寄存器做后续的处理。

D和Q寄存器

大都的NEON指令有两种办法来拜访寄存器组:

  • 作为32个双字寄存器,64-bit位宽,命名为d0-d31
  • 作为16个四字寄存器,128-bit位宽,命名为q0-q15

图3. NEON寄存器组

这些寄存器中一个Q寄存器是一对D寄存器的别号,如Q0是d0和d1寄存器对的别号,寄存器中的值可以用两种办法拜访。这种完结办法很相似C言语里的union联合的数据结构。关于浮点的矩阵乘法,咱们会常常运用Q寄存器的表达办法,由于常常会处理4个32-bit的单精度浮点,这对应于128-bit的Q寄存器。

代码部分

经过以下4条NEON乘法指令能完结一列4个成果:

vmul.f32    q12, q8, d0[0]        @ 向量乘以标量(MUL),矩阵0的榜首列乘以矩阵1的每列的榜首个元素0 

vmla.f32    q12, q9, d0[1]        @ 累加的向量乘以标量(MAC),矩阵0的第二列乘以矩阵1的每列的第二个元素1 

vmla.f32    q12, q10, d1[0] @ 累加的向量乘以标量(MAC),矩阵0的第二列乘以矩阵1的每列的第二个元素2 

vmla.f32    q12, q11, d1[1] @ 累加的向量乘以标量(MAC),矩阵0的第二列乘以矩阵1的每列的第二个元素3 

榜首条指令是图2中的列元素x0, x1, x2, x3 (寄存器q8)乘以y0 (d0[0]),然后成果保存到q12寄存器。接下来的指令操作相似,便是把榜首个矩阵的其他列乘以第二个矩阵的榜首列的呼应元素,成果累加到寄存器Q12里。需求留意的是标量元素如d1[1]也可以用q0[3]表明,可是或许编译器如GNU汇编器会不能承受这种办法。

假如咱们只需求矩阵乘以向量的运算,如许多3D图画处理中的那样,那么此刻的核算就完毕了,可以把成果向量保存 到内存了,可是为了完结矩阵相乘,还需求完结后面的迭代操作,运用寄存器Q1到Q3的y4到yF的元素。假如界说如下的宏,那么就能简化代码结构了:

.macro mul_col_f32 res_q, col0_d, col1_d 

vmul.f32    \res_q, q8, \col0_d[0]      @向量乘以标量(MUL),矩阵0的榜首列乘以矩阵1的每列的榜首个元素0 

vmla.f32    \res_q, q9, \col0_d[1]      @累加的向量乘以标量(MAC),矩阵0的第二列乘以矩阵1的每列的第二个元素1 

vmla.f32    \res_q, q10, \col1_d[0] @累加的向量乘以标量(MAC),矩阵0的第二列乘以矩阵1的每列的第二个元素2 

vmla.f32    \res_q, q11, \col1_d[1] @累加的向量乘以标量(MAC),矩阵0的第二列乘以矩阵1的每列的第二个元素3 

.endm 

那么整个4×4的矩阵乘法代码或许如下:

vld1.32 {d16-d19}, [r1]!            @ 加载矩阵0的上8个元素 

vld1.32 {d20-d23}, [r1]!            @ 加载矩阵0的下8个元素 

vld1.32 {d0-d3}, [r2]!              @ 加载矩阵1的上8个元素 

vld1.32 {d4-d7}, [r2]!              @ 加载矩阵1的下8个元素 

mul_col_f32 q12, d0, d1         @ 矩阵 0 * 矩阵1的榜首列 

mul_col_f32 q13, d2, d3         @ 矩阵 0 * 矩阵1的第二列 

mul_col_f32 q14, d4, d5         @ 矩阵 0 * 矩阵1的第三列 

mul_col_f32 q15, d6, d7         @ 矩阵 0 * 矩阵1的第四列 

vst1.32 {d24-d27}, [r0]!            @ 保存成果的上8个元素 

vst1.32 {d28-d31}, [r0]!            @ 保存成果的下8个元素 

定点算法

定点算法核算往往比浮点核算更快,由于往往定点运算或许需求更少的内存带宽,整数值的乘法也会比浮点算法更为简略。可是定点算法,你需求很细心的挑选表明格局来防止溢出或许饱满,这些会影响你的算法终究的精度。定点算法完结的矩阵乘法和浮点算法相似,在本例中,用Q1.14定点格局,可是根本的完结格局根本相似,仅仅完结中或许需求对成果做一些移位调整。下面是列乘的宏:

.macro mul_col_s16 res_d, col_d 

vmull.s16   q12, d16, \col_d[0] @ 向量乘以标量(MUL),矩阵0的榜首列乘以矩阵1的每列的榜首个元素0 

vmlal.s16   q12, d17, \col_d[1] @ 累加的向量乘以标量(MAC),矩阵0的第二列乘以矩阵1的每列的第二个元素1 

vmlal.s16   q12, d18, \col_d[2] @ 累加的向量乘以标量(MAC),矩阵0的第二列乘以矩阵1的每列的第二个元素2 

vmlal.s16   q12, d19, \col_d[3] @ 累加的向量乘以标量(MAC),矩阵0的第二列乘以矩阵1的每列的第二个元素3 

vqrshrn.s32 \res_d, q12, #14             @ 把成果右移14位,并把累加成果变成Q1.14定点格局,并饱满运算 

.endm 

比较定点和浮点算法的宏,你会发现如下的首要差异:

  • 矩阵元素的值为16位而不是32位,因此用D寄存器来保存4个输入元素
  • 矩阵乘法的成果是把16×16=32位的数据,运用VMULL和VMLAL来吧成果保存到Q寄存器。
  • 终究成果也是16位,因此需求把32位累加器成果来得到16-bit的成果,运用VQRSHRN,饱满处理把32位的成果舍入到16位的narrow右移操作。

把数据从32-bits变成16-bits也能有用的处理内存拜访,加载和存储数据都只需求更少的带宽。

vld1.16 {d16-d19}, [r1]     @ 加载16个元素到矩阵0 

vld1.16 {d0-d3}, [r2]       @ 加载16个元素到矩阵1 

mul_col_s16 d4, d0                      @ 矩阵0乘以矩阵1的列0 

mul_col_s16 d5, d1                      @ 矩阵0乘以矩阵1的列1 

mul_col_s16 d6, d2                      @ 矩阵0乘以矩阵1的列2 

mul_col_s16 d7, d3                      @ 矩阵0乘以矩阵1的列3 

vst1.16 {d4-d7}, [r0]       @ 保存16个成果元素 

指令重排

咱们先展现一下指令重排怎么能进步代码功能。在宏中,接近的乘法指令会写入到相同的方针寄存器,这会让NEON的流水线等候前面的乘法成果完结才干开端下一条指令的履行。假如不运用宏界说,而合理组织指令的次第,把那些相关依靠的指令变成不依靠,这些指令就能并发而不会形成流水线的stall。

vmul.f32    q12, q8, d0[0]              @ rslt col0  = (mat0 col0) * (mat1 col0 elt0) 

vmul.f32    q13, q8, d2[0]              @ rslt col1  = (mat0 col0) * (mat1 col1 elt0) 

vmul.f32    q14, q8, d4[0]              @ rslt col2  = (mat0 col0) * (mat1 col2 elt0) 

vmul.f32    q15, q8, d6[0]              @ rslt col3  = (mat0 col0) * (mat1 col3 elt0) 

vmla.f32    q12, q9, d0[1]              @ rslt col0 += (mat0 col1) * (mat1 col0 elt1) 

vmla.f32    q13, q9, d2[1]              @ rslt col1 += (mat0 col1) * (mat1 col1 elt1) 

... 

... 

用以上的处理办法,矩阵乘法的功能在Cortex-A8处理平台上功能提高了一倍。从文档arm.com/help/index.jsp?topic=/com.arm.doc.set.cortexa/index.html” rel=”nofollow”>Technical Reference Manual for your Cortex core可以看到 各个指令的需求时刻以及推迟,有这些推迟周期,可以更为合理的组织代码次第,提高功能。

声明:本文内容来自网络转载或用户投稿,文章版权归原作者和原出处所有。文中观点,不代表本站立场。若有侵权请联系本站删除(kf@86ic.com)https://www.86ic.net/news/guandian/276107.html

为您推荐

联系我们

联系我们

在线咨询: QQ交谈

邮箱: kf@86ic.com

关注微信
微信扫一扫关注我们

微信扫一扫关注我们

返回顶部