老学庵

天行健,君子以自强不息;地势坤,君子以厚德载物!

0%

NEON指令集

  NEON(Advanced SIMD)是ARM架构中用于向量化计算的指令集,广泛应用于手机、嵌入式设备和 Apple Silicon等基于ARM的平台上。NEON提供了一种高效的方式同时处理多个数据元素,在多媒体处理、信号处理和机器学习中非常重要。

  NEON 是一种 SIMD(Single Instruction Multiple Data)技术,可以使用单条指令同时对多个数据元素进行操作,其特点如下:

  • 数据并行:一次处理 4 个 32 位浮点数或 8 个 16 位整数。
  • 支持整数和浮点运算。
  • 提供了专用的 128 位向量寄存器(每个寄存器可以存储多个数据元素)。
  1. 为什么使用 NEON 指令集?
  • 性能提升:通过并行处理数据,大幅提升计算密集型任务的性能。
  • 节省能耗:减少指令数,降低处理时间。
  • 应用场景广泛:适合图像处理(滤波器、卷积)、信号处理(FFT、卷积编码)、矩阵计算等。
  1. NEON 向量化指令的基本结构

NEON 指令是对多个数据元素同时操作的指令,分为以下几部分:

命名规则

NEON 指令的命名一般由 操作类型、数据类型 和 后缀 构成。

<操作符> <数据类型><后缀>

•   操作符:描述具体操作,如 vadd(向量加法)、vmul(向量乘法)。
•   数据类型:
•   q:表示 128 位宽度的寄存器(如 float32x4_t)。
•   d:表示 64 位宽度的寄存器(如 float32x2_t)。
•   后缀:指明数据类型或操作的特殊要求。
•   s:标量(Scalar)。
•   u:无符号整数(Unsigned)。
•   i:有符号整数(Integer)。
•   f:浮点数(Floating point)。

示例 • vaddq_f32:加法,作用于 128 位(q)浮点型(f32)向量。 • vmulq_u16:乘法,作用于 128 位无符号 16 位整数。 • vld1q_f32:加载数据到 128 位浮点向量寄存器。 • vst1q_f32:存储 128 位浮点向量寄存器的数据到内存。

  1. NEON 数据类型

在 ARM 的 NEON 指令集中,ARM 的 NEON 向量寄存器是 128 位宽,可以存储以下数据类型:

数据类型 元素宽度 元素个数(128 位寄存器) 8 位整数(有/无符号) 8 位 16 16 位整数(有/无符号) 16 位 8 32 位整数(有/无符号) 32 位 4 64 位整数(有/无符号) 64 位 2 32 位浮点数 32 位 4 64 位浮点数 64 位 2

  1. 常用指令

以下是一些常用的 NEON 指令,分为四类:

  1. 加载与存储 • vld1q_f32(float32_t* ptr):从内存加载 4 个 32 位浮点数到 128 位寄存器。 • vst1q_f32(float32_t* ptr, float32x4_t val):将 128 位寄存器的值存储到内存。

  2. 算术操作 • vaddq_f32(a, b):加法,两个向量相加。 • vsubq_f32(a, b):减法,两个向量相减。 • vmulq_f32(a, b):乘法,两个向量相乘。 • vdivq_f32(a, b):除法(部分平台支持)。

  3. 逻辑与比较 • vandq_u8(a, b):按位与操作。 • vorrq_u8(a, b):按位或操作。 • veorq_u8(a, b):按位异或操作。 • vcgeq_f32(a, b):比较是否大于等于。

  4. 数据处理 • vdupq_n_f32(value):将标量复制到每个向量元素。 • vcombine_f32(a, b):将两个 64 位向量合并为一个 128 位向量。 • vextq_f32(a, b, n):提取 a 和 b 的元素形成新的向量。

  1. NEON 实例代码

以下是一个简单的例子,演示如何使用 NEON 实现两个浮点数组的加法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <arm_neon.h>
#include <iostream>

void neon_vector_add(const float* a, const float* b, float* result, size_t n) {
size_t i = 0;
for (; i + 4 <= n; i += 4) { // 每次处理 4 个元素
float32x4_t va = vld1q_f32(&a[i]); // 加载数组 a 的数据
float32x4_t vb = vld1q_f32(&b[i]); // 加载数组 b 的数据
float32x4_t vc = vaddq_f32(va, vb); // 执行向量加法
vst1q_f32(&result[i], vc); // 存储结果到 result
}

// 处理剩余的标量元素
for (; i < n; ++i) {
result[i] = a[i] + b[i];
}
}

int main() {
constexpr size_t n = 8;
float a[n] = {1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0};
float b[n] = {8.0, 7.0, 6.0, 5.0, 4.0, 3.0, 2.0, 1.0};
float result[n] = {0};

neon_vector_add(a, b, result, n);

std::cout << "Result: ";
for (size_t i = 0; i < n; ++i) {
std::cout << result[i] << " ";
}
std::cout << std::endl;

return 0;
}

  1. 性能测试(Benchmark)

将上述代码与普通的标量实现进行对比,可以测量 NEON 的性能提升:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <chrono>

void scalar_vector_add(const float* a, const float* b, float* result, size_t n) {
for (size_t i = 0; i < n; ++i) {
result[i] = a[i] + b[i];
}
}

void benchmark() {
constexpr size_t n = 1000000;
float* a = new float[n];
float* b = new float[n];
float* result = new float[n];

for (size_t i = 0; i < n; ++i) {
a[i] = i * 1.0f;
b[i] = i * 0.5f;
}

// 标量实现
auto start = std::chrono::high_resolution_clock::now();
scalar_vector_add(a, b, result, n);
auto end = std::chrono::high_resolution_clock::now();
std::cout << "Scalar Time: "
<< std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count()
<< " ms" << std::endl;

// NEON 实现
start = std::chrono::high_resolution_clock::now();
neon_vector_add(a, b, result, n);
end = std::chrono::high_resolution_clock::now();
std::cout << "NEON Time: "
<< std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count()
<< " ms" << std::endl;

delete[] a;
delete[] b;
delete[] result;
}

int main() {
benchmark();
return 0;
}

总结 + NEON 指令集的核心:高效向量化数据处理。

  • 重要基础:理解寄存器布局、指令命名规则以及典型数据操作。

  • 实践技巧:逐步优化代码并衡量性能收益。