ARM学习系列 ---- ARM NEON
1 NEON概述
1.1 简介
NEON是指适用于Arm Cortex-A系列处理器的一种高级SIMD(单指令多数据)扩展指令集,可执行并行数据处理。
1.2 发展历史
arm v6 SIMD扩展 | arm v7-a NEON |
---|---|
(1)利用arm通用寄存器 (2)支持8/16bit整数 (3)同时计算2x16/4x8操作数 | (1)32个64bit NEON寄存器 (2)支持8/16/32/64bit整数 (3)支持单精度浮点 (4)最多同时支持16个8bit操 作数 |
1.3 SIMD概念
处理大型数据集时,一个主要的性能限制因素是执行数据处理指令所花费的 CPU 时间量。此 CPU 时间取决于处理整个数据集所需的指令数。指令的数量取决于每条指令可以处理的数据项数。
1.3.1 单指令单数据(SISD)
大多数 Arm 指令是单指令单数据 (SISD)。每条指令对单个数据源执行其指定的操作。因此,处理多个数据项需要多个指令。例如,要执行四个加法运算,需要四条指令来添加四对寄存器中的值:
ADD w0, w0, w5
ADD w1, w1, w6
ADD w2, w2, w7
ADD w3, w3, w8
这种方法相对较慢,并且很难看出不同寄存器之间的关系。为了提高性能和效率,媒体处理通常被卸载到专用处理器,如图形处理单元(GPU)或媒体处理单元,它们可以通过单个指令处理多个数据值。
如果要处理的值小于最大位大小,则 SISD 指令会浪费额外的潜在带宽。例如,将 8 位值相加时,需要将每个 8 位值加载到单独的 64 位寄存器中。对较小的数据大小执行大量单独的操作并不能有效地使用计算机资源,因为处理器、寄存器和数据路径都是为 64 位计算而设计的。
1.3.2 单指令多数据(SIMD)
单指令多数据 (SIMD) 指令同时对多个数据项执行相同的操作。这些数据项作为单独的通道打包在更大的寄存器中。
例如,以下指令将四对单精度(32 位)值相加。但是,在这种情况下,这些值被打包到两对128位寄存器中的单独通道中。然后,第一个源寄存器中的每个通道被添加到第二个源寄存器中的相应通道中,然后存储在目标寄存器中的同一通道中:
ADD V10.4S, V8.4S, V9.4S
// This operation adds two 128-bit (quadword) registers, V8 and V9,
// and stores the result in V10.
// Each of the four 32-bit lanes in each register is added separately.
// There are no carries between the lanes.
该单条指令同时对大型寄存器中的所有数据值进行操作:
使用单个 SIMD 指令执行这四个操作比使用四个单独的 SISD 指令更快。
该图显示了 128 位寄存器,每个寄存器包含四个 32 位值,但对于 Neon 寄存器,也可以采用其他组合:
-
可以使用 Neon 寄存器的所有 128 位同时操作两个 64 位、四个 32 位、8 个 16 位或 16 个 8 位整数数据元素。
-
可以使用 Neon 寄存器的下部 64 位同时操作两个 32 位、4 个 16 位或 8 个 8 位整数数据元素(在这种情况下,Neon 寄存器的上部 64 位未使用)。
请注意,图中所示的加法操作对于每个车道都是真正独立的。车道 0 的任何溢出或携带都不会影响车道 1,这是一个完全独立的计算。
2 ARM V8架构
Arm v8-A是一个非常重要的架构变化,它支持64位执行模式 “AArch64” ,并且带来了全新的64位指令集 “A64” 。同时,为了兼容Arm v7-A (32位架构)指令集,也引入了 “AArch32” 的概念。大部分Arm v7-A代码可以运行在Arm v8-A AArch32执行模式下。
2.1 寄存器
Arm v8-A AArch64有31个64位通用寄存器,每一个通用寄存器具有64位(X0-X30)或是32位模式(W0-W30)。其寄存器视图如下:
Arm v8-A AArch64有32个128位寄存器,也能当作32位Sn寄存器或是64位Dn寄存器使用。其寄存器视图如下:
2.2 指令集
Arm v8-A AArch32指令集是由A32(Arm指令,32 位固定长度指令集)和T32(Thumb指令集,16 位固定长度指令集;Thumb2指令集, 16/32位长度指令集)指令集组成。它是Arm v7 Cortex-A指令集的超集,因此Arm v8-A AArch32能后向兼容Arm v7-A以便运行早期软件。同时,为了维持与A64指令集的一致性,AArch32指令集又新增了NEON除法,加密指令扩展。
与AArch32指令集相比,AArch64指令集A64(32位固定长度)发生了很大变化,比如,它们具有完全不同的指令格式。但是在功能上来说,AArch64指令集基本上实现了AArch32指令集的全部功能,另外添加了NEON双精度浮点的支持。
2.3 NEON指令格式
AArch64 NEON指令格式通用描述如下:
{ < p r e f i x > } < o p > { < s u f f i x > } V d . < T > , V n . < T > , V m . < T > \{<prefix>\}<op>\{<suffix>\} Vd.<T>, Vn.<T>, Vm.<T> {<prefix>}<op>{<suffix>}Vd.<T>,Vn.<T>,Vm.<T>
<prefix>
——前缀,如S/U/F/P 分别表示有符号整数/无符号整数/浮点数/布尔数据类型
<op>
——操作符。例如ADD,AND等。
<suffix>
——后缀,通常是有以下几种:
P:将向量按对操作,例如ADDP
V:跨所有的数据通道操作,例如FMAXV
2:在宽指令/窄指令中操作数据的高位部分。例如ADDHN2,SADDL2。
ADDHN2:两个128位矢量相加,得到64位矢量结果,并将结果存到NEON寄存器的高64位部分。
SADDL2: 两个NEON寄存器的高64位部分相加,得到128-位结果。
<T>
——数据类型,通常是8B/16B/4H/8H/2S/4S/2D等。B代表8bit数据类型;H代表16位数据宽度;S代表32位数据宽度,可以是32位整数或单精度浮点;D代表64位数据宽度,可以是64位整数或双精度浮点。
下面列出具体的NEON指令例子:
UADDLP V0.8H, V0.16B
FADD V0.4S, V0.4S, V0.4S
3 NEON编程基础
NEON 技术通常有下列四种方式:
-
调用NEON优化过的库函数
-
使用编译器自动矢量化选项
-
使用NEON intrinsics指令
-
手写NEON汇编
3.1 调用函数库
用户只需要在程序中直接调用NEON优化过的库函数就可以了,简单易用。目前你有下列库可以选择:
-
Arm Compute library —— 一系列经过Arm CPU和GPU优化过的底层函数库。用于图像处理、机器学习和计算机视觉。可以参考如下链接:https://developer.Arm.com/technologies/compute-library
-
Ne10开源库——由Arm主导开发的,目前提供了比较通用的数学函数,部分图像处理函数,以及FFT函数。Project Ne10:Arm Architecture @ GitHub 的开放式优化软件库项目
3.2 自动矢量化
在GCC编译器选项中有自动矢量化编译选项可以帮助现有的代码编译生成NEON代码。GNU GCC提供一系列的选项,有的能提升性能,有的能降低生成可执行文件的代码大小。对于每一行代码,有很多种汇编指令可以选择。编译器在寄存器、堆栈空间、代码大小、编译时间、便于调试、指令执行时间等许多选项中必须有所取舍,这样才能生成最优的映像文件。
3.3 NEON intrinsics
NEON intrinsics可以视作在NEON指令上面封装了一层接口。当用户在C程序中调用NEON intrinsics接口时,编译器会自动生成相关的NEON指令。NEON intrinsics可以跨Arm v7-A/v8-A运行。只要编程一次,就可以借助编译器生成相应的NEON代码。如果用户在代码中使用了Arm v8-A AArch64特有的NEON指令,只要如下例所示,用宏定义(__aarch64__
)将这部分代码分隔即可。
下面是NEON intrinsics的两个示例。
3.3.1 RGB 去隔行示例
考虑一个 24 位 RGB 图像,其中图像是一个像素数组,每个像素都有一个红色、蓝色和绿色元素。在内存中,这可能显示为:
RGB数据是交错的,访问和操作三个单独的颜色通道会给程序员带来问题。在简单的情况下,可以通过将模数3来操作交错的RGB值单色通道数值。但是,对于更复杂的操作(如傅里叶变换),提取和拆分通道更有意义。
我们在内存中有一个 RGB 值数组,我们希望对它们进行去插错,并将这些值放在单独的颜色数组中。执行此操作的 C 过程可能如下所示:
void rgb_deinterleave_c(uint8_t *r, uint8_t *g, uint8_t *b, uint8_t *rgb, int len_color) {
/*
* Take the elements of "rgb" and store the individual colors "r", "g", and "b".
*/
for (int i=0; i < len_color; i++) {
r[i] = rgb[3*i];
g[i] = rgb[3*i+1];
b[i] = rgb[3*i+2];
}
}
但有一个问题。使用优化级别 -O3(非常高的优化)使用 Arm Compiler 6 进行编译并检查反汇编,发现没有使用 Neon 指令或寄存器。每个单独的 8 位值都存储在单独的 64 位通用寄存器中。考虑到全宽 Neon 寄存器的宽度为 128 位,在示例中,每个寄存器可以容纳 16 个 8 位值,重写解决方案以使用 Neon 内部函数应该会给我们带来良好的结果。
void rgb_deinterleave_neon(uint8_t *r, uint8_t *g,
uint8_t *b, uint8_t *rgb, int len_color)
{
/*
* Take the elements of "rgb" and store the individual colors "r", "g", and "b"
*/
int num8x16 = len_color / 16;
uint8x16x3_t intlv_rgb;
for (int i=0; i < num8x16; i++) {
intlv_rgb = vld3q_u8(rgb+3*16*i);
vst1q_u8(r+16*i, intlv_rgb.val[0]);
vst1q_u8(g+16*i, intlv_rgb.val[1]);
vst1q_u8(b+16*i, intlv_rgb.val[2]);
}
}
在此示例中,我们使用了以下类型和内部函数:
代码元素 | 这是什么? | 我们为什么要使用它? |
---|---|---|
uint8x16_t | 包含16 个8位无符号整数的数组。 | 一个uint8x16_t 适合128位寄存器。我们可以确保即使在C代码中也没有浪费的寄存器位。 |
uint8x16x3_t | 具有三个uint8x16_t 元素的结构。 | 循环中当前颜色值的临时保留区域。 |
vld3q_u8(…) | 通过加载 3*16 字节内存的连续区域来返回uint8x16x3_t 的函数。加载的每个字节都以交替模式放置在三个uint8x16_t 数组之一。 | 在最低级别,这种内在保证了LD3指令的生成,LD3指令以交替模式将给定地址的值加载到三个Neon寄存器中。 |
vst1q_u8(…) | 将uint8x16_t 存储在给定地址的函数。 | 它存储一个完整的128位寄存器,充满字节值。 |
可以使用以下命令在 Arm 计算机上编译和反汇编上述完整源代码:
gcc -g -o3 rgb.c -o exe_rgb_o3
objdump -d exe_rgb_o3 > disasm_rgb_o3
3.3.2 矩阵乘法示例
矩阵乘法是在许多数据密集型应用程序中执行的操作。它由一组以简单方式重复的算术运算组成:
矩阵乘法过程如下:
-
A- 在第一个矩阵中取一行
-
B- 使用第二个矩阵中的列执行此行的点积
-
C- 将结果存储在新矩阵的相应行和列中
对于 32 位浮点数的矩阵,乘法可以写为:
void matrix_multiply_c(float32_t *A, float32_t *B, float32_t *C,
uint32_t n, uint32_t m, uint32_t k)
{
for (int i_idx=0; i_idx < n; i_idx++) {
for (int j_idx=0; j_idx < m; j_idx++) {
C[n*j_idx + i_idx] = 0;
for (int k_idx=0; k_idx < k; k_idx++) {
C[n*j_idx + i_idx] += A[n*k_idx + i_idx]*B[k*j_idx + k_idx];
}
}
}
}
我们假设了内存中矩阵的列主布局。也就是说,n x m
矩阵 M
表示为数组M_array
其中 Mij
= M_array[n*j + i]
。
此代码不是最佳的,因为它没有充分利用 Neon。我们可以开始通过使用内部函数来改进它,但是让我们先解决一个更简单的问题,先看看小的、固定大小的矩阵,然后再转到更大的矩阵。
下面的代码使用内部函数将两个 4x4
矩阵相乘。由于我们要处理的数值数量很少且固定,所有这些值都可以同时放入处理器的 Neon 寄存器中,因此我们可以完全展开循环。
void matrix_multiply_4x4_neon(float32_t *A, float32_t *B, float32_t *C)
{
// these are the columns A
float32x4_t A0;
float32x4_t A1;
float32x4_t A2;
float32x4_t A3;
// these are the columns B
float32x4_t B0;
float32x4_t B1;
float32x4_t B2;
float32x4_t B3;
// these are the columns C
float32x4_t C0;
float32x4_t C1;
float32x4_t C2;
float32x4_t C3;
A0 = vld1q_f32(A);
A1 = vld1q_f32(A+4);
A2 = vld1q_f32(A+8);
A3 = vld1q_f32(A+12);
// Zero accumulators for C values
C0 = vmovq_n_f32(0);
C1 = vmovq_n_f32(0);
C2 = vmovq_n_f32(0);
C3 = vmovq_n_f32(0);
// Multiply accumulate in 4x1 blocks, i.e. each column in C
B0 = vld1q_f32(B);
C0 = vfmaq_laneq_f32(C0, A0, B0, 0);
C0 = vfmaq_laneq_f32(C0, A1, B0, 1);
C0 = vfmaq_laneq_f32(C0, A2, B0, 2);
C0 = vfmaq_laneq_f32(C0, A3, B0, 3);
vst1q_f32(C, C0);
B1 = vld1q_f32(B+4);
C1 = vfmaq_laneq_f32(C1, A0, B1, 0);
C1 = vfmaq_laneq_f32(C1, A1, B1, 1);
C1 = vfmaq_laneq_f32(C1, A2, B1, 2);
C1 = vfmaq_laneq_f32(C1, A3, B1, 3);
vst1q_f32(C+4, C1);
B2 = vld1q_f32(B+8);
C2 = vfmaq_laneq_f32(C2, A0, B2, 0);
C2 = vfmaq_laneq_f32(C2, A1, B2, 1);
C2 = vfmaq_laneq_f32(C2, A2, B2, 2);
C2 = vfmaq_laneq_f32(C2, A3, B2, 3);
vst1q_f32(C+8, C2);
B3 = vld1q_f32(B+12);
C3 = vfmaq_laneq_f32(C3, A0, B3, 0);
C3 = vfmaq_laneq_f32(C3, A1, B3, 1);
C3 = vfmaq_laneq_f32(C3, A2, B3, 2);
C3 = vfmaq_laneq_f32(C3, A3, B3, 3);
vst1q_f32(C+12, C3);
}
我们选择将固定大小的 4x4
矩阵相乘,原因如下:
-
一些应用特别需要
4x4
矩阵,例如图形或相对论物理学。 -
Neon 矢量寄存器保存四个 32 位值,因此将程序与架构相匹配将使其更容易进行优化。
-
我们可以采用这个
4x4
内核,并将其用于更通用的内核。
让我们总结一下这里使用的内部函数:
代码元素 | 这是什么? | 我们为什么要使用它? |
---|---|---|
float32x4_t | 由4个 32 位浮点数组成的数组。 | 一个uint32x4_t 适合128位寄存器。我们可以确保即使在C代码中也没有浪费的寄存器位。 |
vld1q_f32(…) | 将4个 32 位浮点数加载到float32x4_t 中的函数。 | 要获得矩阵值,我们需要从A和B获得矩阵值。 |
vfmaq_lane_f32(…) | 使用融合乘法累加指令的函数。将float32x4_t 值乘以另一个float32x4_t 元素,然后将结果加第三个float32x4_t ,然后再返回结果。 | 由于矩阵行对列点积是一组乘法和加法,因此此操作非常自然地适合。 |
vst1q_f32(…) | 将float32x4_t 存储在给定地址的函数。 | 在计算结果后存储结果。 |
现在我们可以乘以4x4
矩阵,我们可以通过将较大的矩阵视为4x4
矩阵块来乘以它们。这种方法的一个缺陷是,它仅适用于在两个维度中都是四倍的矩阵大小,但是通过用零填充任何矩阵,您可以使用此方法而无需更改它。
下面列出了更一般的矩阵乘法的代码。内核的结构变化很小,添加循环和地址计算是主要变化。与在 4x4 内核中一样,我们对 B 列使用了唯一的变量名称,即使我们可以使用一个变量并重新加载。这充当编译器的提示,为这些变量分配不同的寄存器,这将使处理器能够在等待加载另一列的同时完成一列的算术指令。
void matrix_multiply_neon(float32_t *A, float32_t *B, float32_t *C,
uint32_t n, uint32_t m, uint32_t k)
{
/*
* Multiply matrices A and B, store the result in C.
* It is the user's responsibility to make sure the matrices are compatible.
*/
int A_idx;
int B_idx;
int C_idx;
// these are the columns of a 4x4 sub matrix of A
float32x4_t A0;
float32x4_t A1;
float32x4_t A2;
float32x4_t A3;
// these are the columns of a 4x4 sub matrix of B
float32x4_t B0;
float32x4_t B1;
float32x4_t B2;
float32x4_t B3;
// these are the columns of a 4x4 sub matrix of C
float32x4_t C0;
float32x4_t C1;
float32x4_t C2;
float32x4_t C3;
for (int i_idx=0; i_idx<n; i_idx+=4 {
for (int j_idx=0; j_idx<m; j_idx+=4){
// zero accumulators before matrix op
c0=vmovq_n_f32(0);
c1=vmovq_n_f32(0);
c2=vmovq_n_f32(0);
c3=vmovq_n_f32(0);
for (int k_idx=0; k_idx<k; k_idx+=4){
// compute base index to 4x4 block
a_idx = i_idx + n*k_idx;
b_idx = k*j_idx k_idx;
// load most current a values in row
A0=vld1q_f32(A+A_idx);
A1=vld1q_f32(A+A_idx+n);
A2=vld1q_f32(A+A_idx+2*n);
A3=vld1q_f32(A+A_idx+3*n);
// multiply accumulate 4x1 blocks, i.e. each column C
B0=vld1q_f32(B+B_idx);
C0=vfmaq_laneq_f32(C0,A0,B0,0);
C0=vfmaq_laneq_f32(C0,A1,B0,1);
C0=vfmaq_laneq_f32(C0,A2,B0,2);
C0=vfmaq_laneq_f32(C0,A3,B0,3);
B1=v1d1q_f32(B+B_idx+k);
C1=vfmaq_laneq_f32(C1,A0,B1,0);
C1=vfmaq_laneq_f32(C1,A1,B1,1);
C1=vfmaq_laneq_f32(C1,A2,B1,2);
C1=vfmaq_laneq_f32(C1,A3,B1,3);
B2=vld1q_f32(B+B_idx+2*k);
C2=vfmaq_laneq_f32(C2,A0,B2,0);
C2=vfmaq_laneq_f32(C2,A1,B2,1);
C2=vfmaq_laneq_f32(C2,A2,B2,2);
C2=vfmaq_laneq_f32(C2,A3,B3,3);
B3=vld1q_f32(B+B_idx+3*k);
C3=vfmaq_laneq_f32(C3,A0,B3,0);
C3=vfmaq_laneq_f32(C3,A1,B3,1);
C3=vfmaq_laneq_f32(C3,A2,B3,2);
C3=vfmaq_laneq_f32(C3,A3,B3,3);
}
//Compute base index for stores
C_idx = n*j_idx + i_idx;
vstlq_f32(C+C_idx, C0);
vstlq_f32(C+C_idx+n,Cl);
vstlq_f32(C+C_idx+2*n,C2);
vstlq_f32(C+C_idx+3*n,C3);
}
}
}
编译和反汇编此函数,并将其与我们的 C 函数进行比较,如下所示:
-
给定矩阵乘法的算术指令更少,因为我们利用的是具有完整寄存器打包的高级 SIMD 技术。纯 C 代码通常不会这样做。
-
FMLA
而不是FMUL
指令。由内部函数指定。 -
减少循环迭代。如果使用得当,内部函数允许循环轻松展开。
-
但是,由于内存分配和数据类型(例如,
float32x4_t
)的初始化,存在不必要的加载和存储,这些类型在纯 C 代码中未使用。
可以使用以下命令在 Arm 计算机上编译和反汇编上述完整源代码:
gcc -g -o3 matrix.c -o exe_matrix_o3
objdump -d exe_ matrix _o3 > disasm_matrix_o3
3.3.4程序约定
程序约定是针对特定编程语言的一组准则。
3.3.4.1 宏
为了使用内部函数,必须支持高级 SIMD 体系结构,并且在任何情况下都可能启用某些特定指令,也可能不启用这些指令。当定义了以下宏并等于 1 时,相应的功能可用:
__ARM_NEON:编译器支持高级 SIMD,始终为 1 表示 AArch64
__ARM_NEON_FP:支持NEON浮点运算,始终为 1 表示 AArch64
__ARM_FEATURE_CRYPTO:提供加密说明。因此,可以使用加密 Neon 内部函数。
__ARM_FEATURE_FMA:融合的乘法累加指令可用。因此,可以使用使用这些的NEON内部函数。
此列表并非详尽无遗,有关更多宏的详细信息,请参阅Arm C 语言扩展公文。
3.3.4.2类型
arm_neon.h
中有三大类数据类型,它们遵循以下模式:_base_
是指基本数据类型,_W_
是基本类型的宽度。_L_
是向量数据类型(例如标量数组)中标量数据类型实例的数量,_N_
是向量数组类型(例如标量数组的结构)中矢量数据类型实例的数量。
baseW_t:标量数据类型
baseWxL_t:矢量数据类型
baseWxLxN_t:向量数组数据类型
通常,_W_
和 _L_
是这样的,矢量数据类型的长度为 64 位或 128 位,因此完全适合 Neon 寄存器。_N_
对应于那些同时在多个寄存器上运行的指令。
3.3.4.3 功能函数
根据 Arm C 语言扩展,arm_neon.h
中的函数原型遵循通用模式。在最一般的层面上,这是:
ret v[p][q][r]name[u][n][q][x][_high][_lane | laneq][_n][_result]_type(args)
请注意,某些字母和名称会过载,但按上述顺序排列:
ret:函数的返回类型;
v:vector
的缩写,存在于所有内部函数上;
* p*:指示成对操作([值]
表示可能存在值
);
* q*:表示饱和操作(AArch64 操作中的 vqtb[l][x]
除外,其中 q
表示 128 位索引和结果操作数);
r:表示舍入操作;
name:基本操作的描述性名称,通常,这是高级 SIMD 指令,但不一定是;
* u*:指示有符号到无符号的饱和度;
n:表示窄幅操作;
q:后缀名称表示对 128 位向量的操作。
x:指示 AArch64 中的高级 SIMD 标量操作,它可以是 b
、h
、s
或 d
之一(即 8、16、32 或 64 位);
_high:在 AArch64 中,用于涉及 128 位操作数的加宽和缩小操作。对于加宽 128 位操作数,“高
”是指源操作数的前 64 位。对于缩小范围,它是指目标操作数的前 64 位;
_n:指示作为参数提供的标量操作数;
_lane:指示从矢量的通道获取的标量操作数。_laneq
表示从 128 位宽度的输入向量的通道中获取的标量操作数。(左|右
表示只出现左
或右
)。
type:缩写形式的主操作数类型;
args:函数的参数。
3.4 NEON汇编
NEON手写汇编主要有两种方式:独立汇编文件和内嵌汇编。
3.4.1 独立汇编文件
独立汇编文件可以用“.S”作为文件后缀,也可以用“.s”作为文件后缀。区别在于.S文件会经过C/C++预处理器处理,这样我们可以利用宏定义等C语言特性。
手写NEON汇编文件时,我们需要注意寄存器的保存。对于Arm v7/v8我们需要保存下列寄存器:
下面是Arm v7-A/v8-A NEON 汇编的一个例程:
//在头文件中定义
void add_float_neon2(float* dst, float* src1, float* src2, int count);
下面是手写汇编代码,保存到.S文件中
// Arm v7-A/Arm v8-A AArch32版本
.text
.syntax unified
.align 4
.global add_float_neon2
.type add_float_neon2, %function
.thumb
.thumb_func
add_float_neon2:
.L_loop:
vld1.32 {q0}, [r1]!
vld1.32 {q1}, [r2]!
vadd.f32 q0, q0, q1
subs r3, r3, #4
vst1.32 {q0}, [r0]!
bgt .L_loop
bx lr
// Arm v8-A AArch64版本
.text
.align 4
.global add_float_neon2
.type add_float_neon2, %function
add_float_neon2:
.L_loop:
ld1 {v0.4s}, [x1], #16
ld1 {v1.4s}, [x2], #16
fadd v0.4s, v0.4s, v1.4s
subs x3, x3, #4
st1 {v0.4s}, [x0], #16
bgt .L_loop
ret
3.4.2 内嵌汇编
顾名思义,内嵌汇编是和C代码紧密结合在一起的一种方式。我们可以直接把汇编代码内嵌在C/C++代码中,我们可以在需要NEON的地方即时添加。
优点:
-
过程调用规则简单,不需要自己手动保存寄存器。
-
可以使用 C/C++ 变量和函数,因此它能非常容易地整合到 C/C++ 代码
缺点:
-
内嵌汇编有一套复杂的语法规则
-
NEON代码内嵌在C/C++代码中,不易于移植到其他平台
//Arm v7-A/Arm v8-A AArch32
void add_float_neon3(float* dst, float* src1, float* src2, int count)
{
asm volatile (
"1: \n"
"vld1.32 {q0}, [%[src1]]! \n"
"vld1.32 {q1}, [%[src2]]! \n"
"vadd.f32 q0, q0, q1 \n"
"subs %[count], %[count], #4 \n"
"vst1.32 {q0}, [%[dst]]! \n"
"bgt 1b \n"
: [dst] "+r" (dst)
: [src1] "r" (src1), [src2] "r" (src2), [count] "r" (count)
: "memory", "q0", "q1"
);
}
//Arm v8-A AArch64
void add_float_neon3(float* dst, float* src1, float* src2, int count)
{
asm volatile (
"1: \n"
"ld1 {v0.4s}, [%[src1]], #16 \n"
"ld1 {v1.4s}, [%[src2]], #16 \n"
"fadd v0.4s, v0.4s, v1.4s \n"
"subs %[count], %[count], #4 \n"
"st1 {v0.4s}, [%[dst]], #16 \n"
"bgt 1b \n"
: [dst] "+r" (dst)
: [src1] "r" (src1), [src2] "r" (src2), [count] "r" (count)
: "memory", "v0", "v1"
);
}
3.5 相关资源链接
ArmC语言扩展:Arm C 语言扩展
内部函数查询网站:内在 – 手臂开发人员 (arm.com)
内部函数文档: 适用于 Armv7 和 Armv8 架构的高级 SIMD 架构扩展 (Neon) 内部函数
Introducing Neon for Armv8-A:
https://developer.arm.com/documentation/102474/0100/
Ne10项目开源库: https://github.com/projectNe10/
Arm NEON编程快速上手指南 - 知乎 (zhihu.com)
3.5 附录1:矩阵乘法完整代码