Neon intrinsics 简明教程

Posted 芥末的无奈

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Neon intrinsics 简明教程相关的知识,希望对你有一定的参考价值。

文章目录


前言

本文旨在向 NEON 新手提供入门指导,以便能够快速入门 NEON。NEON 作为一种底层的技术,学习曲线相当陡峭,本教程将扫平你在入门期间的各类疑问,并结合大量习题让你能够真正的入门 NEON。

SIMD & NEON

SIMD(Single Instruction,Multiple Data)即单指令多数据。简而言之,它是对指令集的一种扩展,可以对多个数值进行相同操作。

NEON 是指适用于 Arm Cortex-A系列处理器的一种高级 SIMD(单指令多数据)扩展指令集。

我为什么要学习 NEON,原因有:

  1. 本人熟悉的音频 DSP 算法,可以通过 SIMD 技术进行加速,使其性能提升
  2. ARM 架构在移动端(android/ios 等)设备上已经是统治级别,如果想让这些算法在移动端上有更好的表现,那么就必须学习 NEON
  3. SIMD 技术很酷,通过 NEON 我可以了解到 SIMD 的编程范式。学习一样新东西,本身就很有趣。

NEON intrinsics

那么我们如何才能使用上 NEON 呢?你可以在 C/C++ 代码中嵌入 NEON 汇编代码,这种方式难度非常高,你需要对寄存器、汇编等技术很熟悉,对于新手简直是劝退。幸运的是,你还有 NEON intrinsics 可以选择。

NEON intrinsics 其实就是一组 c 函数,你可以通过调用它们来实现 SIMD,让你的算法更加高效。作为新手,我们以 NEON intrinsics 为入口,一起来窥探 SIMD 奇妙的世界是非常合适的。

经过对 NEON intrinsics 一段时间的学习,大致掌握了 NEON intrinsics 一些基本使用,于是乎赶紧总结一下,防止日后忘记,同时也给各位看官作为学习的参考。

NEON intrinsics 学习资料

推荐几个自己在学习过程中找到的不错的资料。

  1. Learn the architecture - Optimizing C code with Neon intrinsics,推荐第一个读它,内容较少,语言精练。如果你对其中一些知识点不太清楚,没有关系,通读我这篇博客,相信能够轻松解决你的疑问。
  2. Learn the architecture - Neon programmers’ guide,在「Introduction」部分对 SIMD&NEON 原理做了很详细的介绍,对寄存器也做了说明;「NEON Intrinsics」章节介绍了很多 NEON Intrinsics 用法;中文翻译版本在 NEON码农指导 Chapter 1 : Introduction
  3. ARM Compiler toolchain Compiler Reference Version 5.03,其中 「Using NEON Support」非常详细,对 NEON Intrinsics 做了详细的分类和总结,推荐阅读。
  4. ARM NEON for C++ Developers,相当详细地介绍了 NEON Intrinsics 各种用法。
  5. neon-guide,简要的 neon 教程,内容简练。

寄存器

SIMD 提速的原理在于寄存器,在 Introducing NEON Development Article 中提到:

一些现代软件,尤其是多媒体编解码软件和图形加速软件,有大量的少于机器字长的数据参与运算。例如,在音频应用中16位以内数据是频繁的,在图形与视频领域8位以内数据是频繁的。
当在32位微处理器上执行这些操作时,相当一部分计算单元没有被利用,但是依然消耗着计算资源。为了更好的利用这部分闲置的资源,SIMD技术使用一个单指令来并行地在同样类型和大小的多个数据元素上执行相同的操作。通过这种方法,硬件可以在同样时间消耗内用并行的4个8位数值加法运算来替代通常的两个32位数值加法运算。

Arm NEON programming quick referenceLearn the architecture - Neon programmers’ guide 中对 ARM 架构的寄存器做了较为详细的介绍。总结起来为:

  • Armv7-A/AArch32
    • 有 16 个 32bit 的通用寄存器(R0-R15)
    • 有 32 个 64bit 的 NEON 寄存器(D0-D31);它们也可以被看成是 16 个 128bit NEON 寄存器(Q0-Q15);每个 Q 寄存器对应两个 D 寄存器,匹配关系如下图
  • Armv8/AArch64
    • 有 31 个 64bit 的通用寄存器(X0-X30);此外还有上一个特殊的寄存器,该寄存器的名字取决于当前运行环境
    • 有 32 个 128bit 的 NEON 寄存器(V0-V31);它们也可以被看成是 32bit 的 Sn 寄存器或者 64bit 的 Dn 寄存器

向量数据类型

在 NEON 中有非常多向量数据类型,具体的列表你可以在 Vector data types 中找到。

它们有着统一的命名范式规则:

 <type><size>x<number of lanes>_t
  • type,向量中存放数据的类型,包括:
    • int,有符号整形
    • uint,无符号整形
    • float,浮点
    • poly,关于这种类型的介绍,请参考 这里
  • size,即 type 的 bit 长度,例如 float32,表示 32 bit 的 float 类型、int64 表示 64bit 的 int,以此类推
  • number of lanes,通道数,即有多少个数据,例如 float32x4_t,有 4 个 float32

其实这些向量数据类型,你可以认为就是一个数组,类比到 c++ 中的std::array,例如

int16x8_t   < == > std::array<int16_t, 8>
uint64x2_t  < == > std::array<int64_t, 2>
float32x4_t < == > std::array<float, 4>

这些数据类型是为了填满一个寄存器的,所以它们总的 bit 长度要么是 64 或者 128。假设一个 float32x4_t 的向量,其值为 0, 1, 2, 3,那么它们在寄存器总存放的顺序如下图:

你可以像获取数组中的值一样来获取这些向量里的值,例如:

float32x4_t a1.0, 2.0, 3.0f, 4.0;
printf("%lf %lf %lf %lf\\n", a[0], a[1], a[2], a[3]);

至于 Lanes ,我们可以理解为数组下标,在后面的 NEON intrinsics 函数介绍中你经常会看到 lanes 这个词。

NENO intrinsics 命名方式

前面提到 NENO intrinsics 其实就是一堆的 C 函数,作为新手,我第一次看到这些函数的时候是有点懵的,因为它们的命名方式过于抽象了,需要经过一些查询才能大致得知其意思。其大致符合这样的规则:

<opname><flags>_<type>

举几个例子:

  • vmul_s16,将两个 s16 的向量相乘
  • vaddl_u8,将两个 u8 的向量相加

Program-conventions 介绍了更加详细的规则,令人眼花缭乱。了解命名规则有助于我们快速理解 intrinsic 的含义,但作为新人我觉得不必要过于纠结,我们完全可以通过对 intrinsics doc 进行查询,快速的掌握这些神奇函数的性质。至于命名规则,你用熟了、看多了,自然也能猜到一二。

NEON Intrinsics 查询

你可以登入 Intrinsics 进行查询。那么如何看懂查询的结果呢?这里说一下自己的经验。

对于一个函数,我们在意的内容包括:

  • 输入是什么?即参数有哪些。
  • 输出时什么?即返回的数据类型是怎么样的。
  • 函数的行为是怎样的?即函数做了哪些操作。

vaddq_f32 为例,查询结果如下图。我们对照着该图做

  • Arguments,参数是两个 float32x4_t,分别是 ab
  • Return Type,返回一个 float32x4_t
  • Description,描述了该函数的行为:“Floating-point Add (vector). 这条指令将两个源SIMD&FP寄存器中相应的向量元素相加,将结果写入向量中,并将向量写入目标SIMD&FP寄存器。这条指令中所有的值都是浮点值。”
  • Instruction Group,所属类别
  • This intrinsic compiles to the following instructions,该函数将被编译成如下指令:FADD Vd.4S,Vn.4S,Vm.4S。即对 Vm 和 Vn 寄存器中 4 个 float 做 FADD 操作,然后将结果存放在 Vd 中
  • Argument Preparation,参数 a 放在 Vn 寄存器,参数 b 放在 Vm 寄存器中
  • Architectures,该函数在 v7、A32、A64 架构下可用
  • Operation,即该指令的具体操作,你通过这部分内容可以大致的了解指令的算法流程,它类似伪代码,并不难理解。在遇到一些奇怪的指令时,仅仅通过 Description 可能无法知晓它的作用,这时候你可以来看 Operation。

三种处理方式:Long/Wide/Narrow

NEON 指令通常有 Normal、Long、Wide 和 Narrow 之分。

  • Normal,指令的输入与输出数据有相同 bit 宽度,例如 vaddq_f32,结果为 float32x4_t,输出为 float32x4_t,都是 128-bit。
  • Long,指令对 64-bit 数据进行操作,产生 128-bit 向量结果,结果宽度是输入的两倍,并且类型相同。此类指令在 NEON Intrinsics 中通过 “l” 来标识,例如 vaddl_s32,输入为 int32x2_t,输出为 int64x2_t
  • Wide,指令对一个 128-bit 向量和一个 64-bit 向量进行操作,产生一个 128-bit 向量结果。结果和第一输入向量是第二输入向量的两倍宽度。此类指令在 NEON Intrinsics 中通过 “w” 来标识,例如 vaddw_s32,输入为 int64x2_tint32x2_t,输出为 int64x2_t
  • Narrow,指令对 128-bit 向量进行操作,产生一个 64-bit 的结果,结果宽度是输入的一半。此类指令在 NEON Intrinsics 中通过 “n” 来标识,例如 vaddhn_s32,输入为 int32x4_t ,输出为 int16x4_t

NENO intrinsics 手册

ARM Compiler toolchain Compiler Reference Version 5.03 中对 intrinsic 做了详细的分类。本章将对各个类别的函数举例说明,帮助大家理解。

所有代码你可以直接在 Compiler Explorer 在线编辑器中运行,选择 ‘arm64’ 编译器且引入 <arm_neon.h> 即可。

Addition 向量加法

Vector add: vaddq_type. Vr[i]:=Va[i]+Vb[i]

c = a + b

  • vaddq_f32
float32x4_t a1.0, 2.0, 3.0f, 4.0;
float32x4_t b1.0, 2.0, 3.0f, 4.0;
float32x4_t c = vaddq_f32(a, b); // c: 2, 4, 6, 8
  • vadd_u64
uint64x1_t a1;
uint64x1_t b2;
uint64x1_t c = vadd_u64(a, b); // c: 3

Vector long add: vaddl_type. Vr[i]:=Va[i]+Vb[i]

Long 方式处理。Va, Vb 的通道数相同, 返回值时一个输入的两倍宽向量

  • vaddl_s32
int32x2_t a1, 2;
int32x2_t b1, 2;
int64x2_t c = vaddl_s32(a, b); // c: 2, 4

Vector wide add: vaddw_type. Vr[i]:=Va[i]+Vb[i]

Wide 方式处理。Va,Vb 的通道数相同,Va 是 Vb 的两倍宽,返回值宽度与 Va 相同

  • vaddw_s32
int64x2_t a1, 2;
int32x2_t b1, 2;
int64x2_t c = vaddw_s32(a, b);

Vector halving add: vhaddq_type. Vr[i]:=(Va[i]+Vb[i])>>1

Va 与 Vb 相加,并将结果右移一位(相当于整数除 2),即 c = (a + b) >> 1

  • vhadd_s32
int32x2_t a1, 2;
int32x2_t b2, 3;

// a + b = 3, 5
// (a + b)/2 = 1, 2
int32x2_t c = vhadd_s32(a, b);

Vector rounding halving add: vrhaddq_type. Vr[i]:=(Va[i]+Vb[i]+1)>>1

Va 与 Vb 相加,并加上 1,然后右移一位。即整数除以 2 并向上取整,即 c = (a + b + 1) >> 1

  • vrhadd_s32
int32x2_t a1, 2;
int32x2_t b2, 3;
int32x2_t c = vrhadd_s32(a, b);

VQADD: Vector saturating add

向量饱和加法,当计算结果可表示的最大值或者小于表示的最小值时,计算结果取值为这个最大值或最小值。

  • vqadd_s8
int8x8_t a127, 127;
int8x8_t b0, 1;
int8x8_t c = vqadd_s8(a, b); // c127, 127, ....

int8x8_t e-128, -128;
int8x8_t d0, -1;
int8x8_t f = vqadd_s8(e, d); // f-128, -128, ....

Vector add high half: vaddhn_type.Vr[i]:=Va[i]+Vb[i]

Narrow 方式处理。Va 与 Vb 向量相加,去结果的高位存放在 Vr 中

int32x4_t a0x7ffffffe, 0x7ffffffe, 0, 0;
int32x4_t b0x00000001, 0x00000002, 0, 0;
// 0x7ffffffe + 0x00000001 = 0x7fffffff => 取高位 => 0x7fff
// 0x7ffffffe + 0x00000002 = 0x80000000 => 取高位 => 0x8000
int16x4_t c = vaddhn_s32(a, b);//c32767 -32768 0 0

Vector rounding add high half: vraddhn_type.

向量相加,取最高位的一半作为结果,并做四舍五入

int32x4_t a0x7ffffffe, 0x7ffffffe, 0, 0;
int32x4_t b0x00000001, 0x00000002, 0, 0;
// 0x7ffffffe + 0x00000001 + 0x00008000 = 0x80007fff => 取高位 => 0x8000
// 0x7ffffffe + 0x00000002 + 0x00008000 = 0x80008000 => 取高位 => 0x8000
int16x4_t c = vraddhn_s32(a, b);//c-32768 -32768 0 0

Multiplication 向量乘法

Vector multiply: vmulq_type. Vr[i] := Va[i] * Vb[i]

向量相乘,c = a*b

  • vmul_f32
float32x2_t a1.0f, 2.0f;
float32x2_t b2.0f, 3.0f;
float32x2_t c = vmul_f32(a, b); // c3.0f, 6.0f

Vector multiply accumulate: vmlaq_type. Vr[i] := Va[i] + Vb[i] * Vc[i]

向量乘加,即 d = a + b*c

  • vmla_f32
float32x2_t a1.0f, 2.0f;
float32x2_t b2.0f, 3.0f;
float32x2_t c4.0f, 5.0f;
float32x2_t d = vmla_f32(a, b, c); //c9, 17

Vector multiply accumulate long: vmlal_type. Vr[i] := Va[i] + Vb[i] * Vc[i]

Long 方式处理,Va 是 Vb/Vc 两倍宽,输出宽度与 Va 一致

  • vmlal_s32
int64x2_t a1, 2;
int32x2_t b2, 3;
int32x2_t c4, 5;
int64x2_t d = vmlal_s32(a, b, c); //c9, 17

Vector multiply subtract: vmlsq_type. Vr[i] := Va[i] - Vb[i] * Vc[i]

向量乘减,即 d = a - b*c

  • vmls_f32
float32x2_t a1.0f, 2.0f;
float32x2_t b2.0f, 3.0f;
float32x2_t c4.0f, 5.0f;
float32x2_t d = vmls_f32(a, b, c);// c-7, -13

Vector multiply subtract long

向量乘减,Long 方式处理

  • vmlsl_s32
int64x2_t a1, 2;
int32x2_t b2, 3;
int32x2_t c4, 5;
int64x2_t d = vmlsl_s32(a, b, c);// c-7, -13

Vector saturating doubling multiply high

ab 的相乘,将结果加倍(*2),将最终结果的最高位一半放入向量中,并将向量写入目标寄存器。

  • vqdmulh_s32
int32x2_t a0x00020000, 0x00035000;
int32x2_t b0x00010000, 0x00015000;
// (0x00020000 * 0x00010000)*2 = 0x400000000,  >> 32 = 0x00000004
// (0x00035000 * 0x00015000)*2 = 0x8b2000000,  >> 32 = 0x00000008
int32x2_t c = vqdmulh_s32(a, b); // c4, 8

Vector saturating rounding doubling multiply high

  • vqrdmulh_s32,其中 0x800000001<<31,这个值怎么来的,请参考 vqrdmulh_s32 的 Operation 部分。
int32x2_t a0x00010000, 0x00035000;
int32x2_t b0x00020000, 0x00015000;
// (0x00020000 * 0x00010000)*2 + 0x80000000 = 0x480000000, >> 32 = 0x00000004
// (0x00035000 * 0x00015000)*2 + 0x80000000 = 0x932000000, >> 32 = 0x00000009
int32x2_t c = vqrdmulh_s32(a, b); // c4, 9

Vector saturating doubling multiply accumulate long

d = a + (b*c*2) ,Long 方式处理

  • vqdmlal_s32
int64x2_t a1, 2;
int32x2_t b3, 4;
int32x2_t c5, 6;
int64x2_t d = vqdmlal_s32(a, b, c); // c31,50

Vector saturating doubling multiply subtract long

d = a - (b*c*2) ,Long 方式处理

  • vqdmlsl_s32
int64x2_t a1, 2;
int32x2_t b3, 4;
int32x2_t c5, 6;
int64x2_t d = vqdmlsl_s32(a, b, c); // c-29,-46

Vector long multiply

c = a*b,Long 方式处理

  • vmull_s32
int32x2_t a1, 2;
int32x2_t b3, 4;
int64x2_t c = vmull_s32(a, b);// c3, 8

Vector saturating doubling long multiply

c = 2*a*b,Long 方式处理

  • vqdmull_s32
int32x2_t a1, 2;
int32x2_t b3, 4;
int64x2_t c = vqdmull_s32(a, b);

Subtraction 向量减法

通过对 AdditionMultiplication 指令学习,你会发现有很多很多指令是在某个基础指令上的变种,这些变种指令操作与基础指令大同小异,后面将不再对变种指令做讲解,让我们把注意力放在更重要的指令上。

Vector subtract

向量相减,即 c = a - b

  • vsubq_f32
float32x4_t a4,3,2,1;
float32x4_t b1,2,3,4;
float32x4_t c = vsubq_f32(a, b); //c3, 1, -1, -3

Vector long subtract: vsubl_type. Vr[i]:=Va[i]-Vb[i]

向量相减,Long 方式处理。

  • vsubl_s32
int32x2_t a4, 3;
int32x2_t b1, 2;
int64x2_t c= vsubl_s32(a, b);//c3,1

Vector wide subtract: vsubw_type. Vr[i]:=Va[i]-Vb[i]

向量相减,Wide 方式处理

  • vsubw_s32
int64x2_t a4,3;
int32x2_t b1, 2;
int64x2_t c= vsubw_s32(a, b);//c3,1

Vector saturating subtract

向量饱和减法

  • vqsub_s32
int32x2_t a0x7fffffff, 0x7fffffff;
int32x2_t以上是关于Neon intrinsics 简明教程的主要内容,如果未能解决你的问题,请参考以下文章

NEON Intrinsics 练习题

NEON Intrinsics 练习题

NEON指南-4-Neon intrinsics chromium case study

Neon 在 Intrinsics 中的校验和代码实现

Neon Intrinsics各函数介绍

Neon Intrinsics各函数介绍