NEON(Advanced SIMD)是ARM架构中用于向量化计算的指令集,广泛应用于手机、嵌入式设备和 Apple Silicon等基于ARM的平台上。NEON提供了一种高效的方式同时处理多个数据元素,在多媒体处理、信号处理和机器学习中非常重要。
NEON 是一种 SIMD(Single Instruction Multiple Data)技术,可以使用单条指令同时对多个数据元素进行操作,其特点如下:
- 数据并行:一次处理 4 个 32 位浮点数或 8 个 16 位整数。
- 支持整数和浮点运算。
- 提供了专用的 128 位向量寄存器(每个寄存器可以存储多个数据元素)。
- 为什么使用 NEON 指令集?
- 性能提升:通过并行处理数据,大幅提升计算密集型任务的性能。
- 节省能耗:减少指令数,降低处理时间。
- 应用场景广泛:适合图像处理(滤波器、卷积)、信号处理(FFT、卷积编码)、矩阵计算等。
- 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 位浮点向量寄存器的数据到内存。
⸻
- 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
⸻
- 常用指令
以下是一些常用的 NEON 指令,分为四类:
加载与存储 • vld1q_f32(float32_t* ptr):从内存加载 4 个 32 位浮点数到 128 位寄存器。 • vst1q_f32(float32_t* ptr, float32x4_t val):将 128 位寄存器的值存储到内存。
算术操作 • vaddq_f32(a, b):加法,两个向量相加。 • vsubq_f32(a, b):减法,两个向量相减。 • vmulq_f32(a, b):乘法,两个向量相乘。 • vdivq_f32(a, b):除法(部分平台支持)。
逻辑与比较 • vandq_u8(a, b):按位与操作。 • vorrq_u8(a, b):按位或操作。 • veorq_u8(a, b):按位异或操作。 • vcgeq_f32(a, b):比较是否大于等于。
数据处理 • vdupq_n_f32(value):将标量复制到每个向量元素。 • vcombine_f32(a, b):将两个 64 位向量合并为一个 128 位向量。 • vextq_f32(a, b, n):提取 a 和 b 的元素形成新的向量。
⸻
- 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
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;
}
⸻
- 性能测试(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
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 指令集的核心:高效向量化数据处理。
重要基础:理解寄存器布局、指令命名规则以及典型数据操作。
实践技巧:逐步优化代码并衡量性能收益。