ARM学习系列 ---- ARM NEON

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优化过的库函数就可以了,简单易用。目前你有下列库可以选择:

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:函数的返回类型;

vvector 的缩写,存在于所有内部函数上;

* p*:指示成对操作([值] 表示可能存在值);

* q*:表示饱和操作(AArch64 操作中的 vqtb[l][x] 除外,其中 q 表示 128 位索引和结果操作数);

r:表示舍入操作;

name:基本操作的描述性名称,通常,这是高级 SIMD 指令,但不一定是;

* u*:指示有符号到无符号的饱和度;

n:表示窄幅操作;

q:后缀名称表示对 128 位向量的操作。

x:指示 AArch64 中的高级 SIMD 标量操作,它可以是 bhsd 之一(即 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:矩阵乘法完整代码

  • 3
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Using built-in specs. COLLECT_GCC=./arm-cortexa9_neon-linux-uclibcgnueabihf-gcc COLLECT_LTO_WRAPPER=/opt/toolchains/arm-cortexa9_neon-linux-uclibcgnueabihf-gcc-7.4.0/bin/../libexec/gcc/arm-cortexa9_neon-linux-uclibcgnueabihf/7.4.0/lto-wrapper Target: arm-cortexa9_neon-linux-uclibcgnueabihf Configured with: /home/abu/arm/u-boot-2019.04/.build/arm-cortexa9_neon-linux-uclibcgnueabihf/src/gcc/configure --build=i686-build_pc-linux-gnu --host=i686-build_pc-linux-gnu --target=arm-cortexa9_neon-linux-uclibcgnueabihf --prefix=/home/abu/x-tools/arm-cortexa9_neon-linux-uclibcgnueabihf --with-sysroot=/home/abu/x-tools/arm-cortexa9_neon-linux-uclibcgnueabihf/arm-cortexa9_neon-linux-uclibcgnueabihf/sysroot --enable-languages=c,c++,fortran --with-cpu=cortex-a9 --with-fpu=neon --with-float=hard --with-pkgversion='crosstool-NG 1.24.0' --enable-__cxa_atexit --disable-libmudflap --disable-libgomp --disable-libssp --disable-libquadmath --disable-libquadmath-support --disable-libsanitizer --disable-libmpx --with-gmp=/home/abu/arm/u-boot-2019.04/.build/arm-cortexa9_neon-linux-uclibcgnueabihf/buildtools --with-mpfr=/home/abu/arm/u-boot-2019.04/.build/arm-cortexa9_neon-linux-uclibcgnueabihf/buildtools --with-mpc=/home/abu/arm/u-boot-2019.04/.build/arm-cortexa9_neon-linux-uclibcgnueabihf/buildtools --with-isl=/home/abu/arm/u-boot-2019.04/.build/arm-cortexa9_neon-linux-uclibcgnueabihf/buildtools --disable-lto --with-host-libstdcxx='-static-libgcc -Wl,-Bstatic,-lstdc++ -lm' --enable-threads=posix --enable-target-optspace --disable-plugin --with-libintl-prefix=/home/abu/arm/u-boot-2019.04/.build/arm-cortexa9_neon-linux-uclibcgnueabihf/buildtools --disable-multilib --with-local-prefix=/home/abu/x-tools/arm-cortexa9_neon-linux-uclibcgnueabihf/arm-cortexa9_neon-linux-uclibcgnueabihf/sysroot --enable-long-long Thread model: posix gcc version 7.4.0 (crosstool-NG 1.24.0)
### 回答1: arm-linux-androideabi-4.9是一个基于ARM架构的交叉编译器工具集。它用于在Linux操作系统上生成针对Android操作系统的ARM架构的可执行文件。 交叉编译器工具集是一组软件工具,用于将源代码从一种处理器架构(例如x86)转换为另一种处理器架构(例如ARM)。换句话说,它可以让开发人员在一种平台上开发软件,并将其编译成在另一种平台上运行的可执行文件。 在Android开发中,我们通常将Android操作系统视为目标平台,并希望能够在开发主机上编译和调试我们的应用程序。然而,Android平台使用的是ARM架构,而大多数开发主机使用的是x86架构。因此,我们需要一个交叉编译器工具集来生成ARM架构的可执行文件。 arm-linux-androideabi-4.9是一个常用的交叉编译器工具集版本,它是基于GCC(GNU编译器集合)构建的。它支持C、C++和其他一些编程语言,并提供了一系列的命令行工具,例如编译器、连接器和调试器。通过使用这个工具集,开发人员可以在他们的开发主机上编写、编译和调试ARM架构的应用程序,并将它们部署到Android设备上运行。 总之,arm-linux-androideabi-4.9是一个用于在Linux操作系统上生成ARM架构的Android应用程序的交叉编译器工具集。它是Android开发中的重要工具之一,使开发人员能够在开发主机上进行高效的应用程序开发和调试。 ### 回答2: arm-linux-androideabi-4.9是一个编译工具链的版本号,用于在ARM架构的Android设备上进行开发和编译。其中,arm表示所使用的处理器架构为ARM;linux表示编译工具链用于在Linux系统上进行开发和编译;androideabi表示该编译工具链是用于Android设备上进行应用程序开发和编译的;4.9表示这个编译工具链的版本号为4.9。 编译工具链是用于将源代码转化为可执行文件的一系列工具的集合。在开发Android应用程序时,通常会使用编译工具链来编译C/C++源代码,将其转化为在设备上可以运行的机器码。arm-linux-androideabi-4.9是一个适用于ARM架构的Android设备的编译工具链,其版本号为4.9。 通过使用arm-linux-androideabi-4.9,开发者可以在他们的开发环境中编译C/C++源代码,并生成可在ARM架构的Android设备上运行的二进制文件。这样,开发者就可以针对特定的Android设备,使用这个编译工具链来生成对应的可执行文件,以达到更好的性能和兼容性。 总之,arm-linux-androideabi-4.9是一个针对ARM架构的Android设备开发和编译的工具链版本号,通过使用它可以编译C/C++源代码,并生成可在Android设备上运行的二进制文件。 ### 回答3: arm-linux-androideabi-4.9,是指适用于ARM架构的Linux系统以及Andorid平台的GNU工具链的版本号。这个版本的工具链由GCC(GNU编译器集合)组成,用于编译C、C++等高级语言,并将其转化为适合在ARM架构上运行的机器码。 arm-linux-androideabi-4.9中的"arm"表示这个工具链是为ARM架构设计的,"linux"表示它可以运行在Linux系统上,"androideabi"表示它是专门用于Android平台的编译工具链,"4.9"表示这个版本的GCC系列为4.9。 这个工具链主要包含了编译器(gcc)、链接器(ld)以及一些辅助的工具(如assembler、objcopy等),可以将高级语言的源代码编译为ARM体系结构下的机器码,从而方便在ARM架构的设备上运行。而Android平台对应的工具链会提供一些额外的功能,如对ARM架构下的硬件加速(如NEON指令集)的支持等。 使用这个工具链可以帮助开发者编译和构建ARM架构上的应用程序。在Android开发中,特别是进行底层开发和系统级编程时,我们会使用这个工具链来编译和生成可执行文件,进行调试和测试。 总之,arm-linux-androideabi-4.9是一款用于ARM架构的Linux系统和Android平台上的GCC工具链的版本。它为开发者提供了编译、链接和构建应用程序所需的工具和功能,帮助开发者在ARM架构设备上进行软件开发

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值