ARM NEON SIMD 编程实战:从 int16/int32/float32 到音频异常检测---AI算子研发
日期 :2026-06-11
适用平台 :AArch64 (ARMv8-A+) / Android NDK / Linux ARM
目录
[什么是 SIMD 与 NEON](#什么是 SIMD 与 NEON)
[NEON 数据类型总览](#NEON 数据类型总览)
[int16 / int32 / float32 / float16 / bfloat16 对比](#int16 / int32 / float32 / float16 / bfloat16 对比)
[NEON Intrinsics 速查表](#NEON Intrinsics 速查表)
[实战案例:neon_get_mean_std_impl 逐行解析](#实战案例:neon_get_mean_std_impl 逐行解析)
[实战案例:int16 NEON 均值/方差计算](#实战案例:int16 NEON 均值/方差计算)
[实战案例:NEON 宏模板------批量向量运算](#实战案例:NEON 宏模板——批量向量运算)
[实战案例:卷积/池化/ReLU 的 NEON 加速](#实战案例:卷积/池化/ReLU 的 NEON 加速)
[float16 与 bfloat16 的 NEON 支持](#float16 与 bfloat16 的 NEON 支持)
性能优化技巧与踩坑指南
1. 什么是 SIMD 与 NEON
SIMD (Single Instruction, Multiple Data)是一种数据级并行技术:一条指令同时处理多个数据元素。
ARM NEON 是 ARM 架构内置的 SIMD 扩展,从 ARMv7 开始引入,AArch64 下称为 Advanced SIMD :
特性
AArch32 (ARMv7)
AArch64 (ARMv8-A)
寄存器宽度
128-bit (Q0-Q15)
128-bit (V0-V31)
寄存器数量
16 个 Q 寄存器
32 个 V 寄存器
float16 硬件支持
需要 VFPv4
原生支持
bfloat16 支持
无
ARMv8.6-A+ (BF16 扩展)
水平归约
需多条指令
vaddvq / vmaxvq 原生
NEON 编程的三种方式
方式
说明
适用场景
Intrinsics
C 函数形式调用 NEON 指令
✅ 推荐,可读性好,编译器可优化
内联汇编
直接写 NEON 汇编
极致优化,可移植性差
自动向量化
编译器 -O2 -ftree-vectorize 自动
简单循环,效果不可控
本文全部使用 NEON Intrinsics 方式。
2. NEON 数据类型总览
NEON 用向量寄存器(128-bit)承载不同精度的数据:
128-bit 寄存器的拆分方式:
┌─────────────────────────────────────────────────────────────────┐
│ 128-bit (q 寄存器) │
├─────────────────────────────────────────────────────────────────┤
│ float32x4 │ f32 │ f32 │ f32 │ f32 │ 4 × float32 │
├───────────┼───────┼───────┼───────┼───────┤ │
│ float16x8 │ f16 │ f16 │ f16 │ f16 │ 8 × float16 │
│ │ f16 │ f16 │ f16 │ f16 │ │
├───────────┼───────┼───────┼───────┼───────┤ │
│ int32x4 │ i32 │ i32 │ i32 │ i32 │ 4 × int32 │
├───────────┼───────┼───────┼───────┼───────┤ │
│ int16x8 │ i16 │ i16 │ i16 │ i16 │ 8 × int16 │
│ │ i16 │ i16 │ i16 │ i16 │ │
├───────────┼───────┼───────┼───────┼───────┤ │
│ int8x16 │ i8 × 16 │
└─────────────────────────────────────────────────────────────────┘
NEON 类型命名规则
<type><size>x<number_of_lanes>[_t]
示例:
float32x4_t → 4 个 float32(128-bit)
int16x4_t → 4 个 int16 (64-bit,d 寄存器)
int16x8_t → 8 个 int16 (128-bit,q 寄存器)
float16x8_t → 8 个 float16 (128-bit)
bfloat16x8_t → 8 个 bfloat16(128-bit,ARMv8.6-A+)
带 q 前缀的 intrinsics 操作 128-bit,不带 q 的操作 64-bit:
操作宽度
类型示例
Intrinsics 前缀
64-bit (d 寄存器)
int16x4_t, float32x2_t
vadd_s16, vld1_f32
128-bit (q 寄存器)
int16x8_t, float32x4_t
vaddq_s16, vld1q_f32
3. int16 / int32 / float32 / float16 / bfloat16 对比
3.1 数据格式位布局
int16 (Q15 定点):
┌──┬─────────────────────────────┐
│S │ 15 bit │ 范围: [-32768, 32767]
└──┴─────────────────────────────┘
int32:
┌──┬─────────────────────────────────────────────────────────────┐
│S │ 31 bit │ 范围: [-2^31, 2^31-1]
└──┴─────────────────────────────────────────────────────────────┘
float32 (IEEE 754):
┌──┬──────────────┬──────────────────────────────────────────────┐
│S │ Exp (8bit) │ Mantissa (23bit) │ 动态范围: ±3.4×10^38
└──┴──────────────┴──────────────────────────────────────────────┘
float16 (IEEE 754 half):
┌──┬────────┬──────────────────────┐
│S │Exp(5b) │ Mantissa (10bit) │ 动态范围: ±65504
└──┴────────┴──────────────────────┘
bfloat16 (Brain Float):
┌──┬──────────────┬──────────────┐
│S │ Exp (8bit) │ Mantissa(7b) │ 动态范围: 与 float32 相同
└──┴──────────────┴──────────────┘
3.2 五种数据类型全面对比
属性
int16 (Q15)
int32
float32
float16
bfloat16
位宽
16 bit
32 bit
32 bit
16 bit
16 bit
符号
有符号
有符号
有符号
有符号
有符号
精度
1 LSB ≈ 3×10⁻⁵
1 LSB ≈ 4.7×10⁻¹⁰
~7 位有效数字
~3.3 位有效数字
~2.4 位有效数字
动态范围
固定 -32768,32767
固定 -2³¹,2³¹-1
±3.4×10³⁸
±65504
±3.4×10³⁸
NEON 并行数 (128-bit)
8 路
4 路
4 路
8 路
8 路
NEON 向量类型
int16x8_t
int32x4_t
float32x4_t
float16x8_t
bfloat16x8_t
乘法溢出风险
高(需 vmlal 扩展)
高
无(自动指数)
中
中
水平归约
vaddvq_s16
vaddvq_s32
vaddvq_f32
需 AArch64
需 ARMv8.6-A
典型用途
音频 PCM
中间累加器
ML 推理/音频处理
移动端推理
训练梯度
3.3 精度与范围的直观对比
精度(可区分的最小变化):
int16 : ████████████████████████████████ 1/65536 ≈ 1.5e-5
float16: ██████████████████████ ~0.001 (在 1.0 附近)
bfloat16:████████████ ~0.008 (在 1.0 附近)
float32: ████████████████████████████████ ~6e-8 (在 1.0 附近)
动态范围(可表示的最大值 / 最小正值):
int16 : ████ 65536
float16: ██████████████ 65504 / 6e-8 ≈ 10^12
bfloat16:██████████████████████████████ 3.4e38 / 1.2e-38 ≈ 10^76
float32: ████████████████████████████████ 3.4e38 / 1.2e-38 ≈ 10^76
关键结论 :
int16 精度高但范围小,适合音频 PCM(满量程 0 dBFS)
float32 精度和范围都好,是通用计算的首选
float16 范围够但精度低,适合推理(不适合训练)
bfloat16 范围与 float32 相同但精度更低,专为 ML 训练设计
4. NEON Intrinsics 速查表
4.1 加载与存储
Intrinsics
操作
说明
vld1q_f32(ptr)
加载
从内存加载 4×float32 到 128-bit 寄存器
vld1q_s32(ptr)
加载
加载 4×int32
vld1q_s16(ptr)
加载
加载 8×int16
vld1_f32(ptr)
加载
加载 2×float32 到 64-bit 寄存器
vld1_s16(ptr)
加载
加载 4×int16 到 64-bit 寄存器
vst1q_f32(ptr, val)
存储
存储 4×float32 到内存
vst1q_s32(ptr, val)
存储
存储 4×int32
vst1q_s16(ptr, val)
存储
存储 8×int16
vld1q_f16(ptr)
加载
加载 8×float16(需 ARMv8.2-A FP16)
vld1q_bf16(ptr)
加载
加载 8×bfloat16(需 ARMv8.6-A BF16)
4.2 算术运算
Intrinsics
操作
说明
vaddq_f32(a, b)
加法
4 路 float32 并行加法
vaddq_s32(a, b)
加法
4 路 int32 并行加法
vaddq_s16(a, b)
加法
8 路 int16 并行加法
vsubq_f32(a, b)
减法
4 路 float32 并行减法
vsubq_s16(a, b)
减法
8 路 int16 并行减法
vmulq_f32(a, b)
乘法
4 路 float32 并行乘法
vmulq_s16(a, b)
乘法
8 路 int16 并行乘法(注意溢出!)
vmlaq_f32(acc, a, b)
乘累加
acc += a * b,4 路 float32
vmlal_s16(acc, a, b)
扩展乘累加
acc += a * b,int16→int32 扩展,防溢出
vdivq_f32(a, b)
除法
4 路 float32(需 ARMv8-A)
4.3 比较与选择
Intrinsics
操作
说明
vmaxq_f32(a, b)
最大值
逐 lane 取 max
vminq_f32(a, b)
最小值
逐 lane 取 min
vabsq_f32(v)
绝对值
逐 lane 取绝对值
vabs_s16(v)
绝对值
4 路 int16 绝对值(64-bit)
vmaxvq_f32(v)
水平最大
4 个 lane 求最大值(AArch64)
vminvq_f32(v)
水平最小
4 个 lane 求最小值(AArch64)
4.4 类型转换
Intrinsics
操作
说明
vcvtq_f32_s32(v)
转换
int32 → float32
vcvtq_s32_f32(v)
转换
float32 → int32(截断)
vcvt_f32_f16(v)
转换
float16 → float32(64-bit→128-bit)
vcvt_f16_f32(v)
转换
float32 → float16(128-bit→64-bit)
vreinterpretq_f32_s32(v)
重解释
bit 不变,类型重新解释
vmovl_s16(v)
扩展
int16x4 → int32x4(高位补符号位)
vqmovn_s32(v)
饱和窄化
int32x4 → int16x4(饱和截断)
4.5 归约(Reduction)--- AArch64 专有
Intrinsics
操作
说明
vaddvq_f32(v)
水平求和
4 个 float32 lane 求和 → 标量
vaddvq_s32(v)
水平求和
4 个 int32 lane 求和 → 标量
vaddvq_s16(v)
水平求和
8 个 int16 lane 求和 → 标量
vaddv_s16(v)
水平求和
4 个 int16 lane 求和 → 标量
vmaxvq_f32(v)
水平最大
4 个 float32 求最大 → 标量
vmaxv_u16(v)
水平最大
4 个 uint16 求最大 → 标量
4.6 常量构造
Intrinsics
操作
说明
vdupq_n_f32(val)
广播
4 个 lane 都设为 val
vdupq_n_s32(val)
广播
4 个 int32 lane 都设为 val
vdup_n_s16(val)
广播
4 个 int16 lane 都设为 val(64-bit)
vmovq_n_f32(val)
广播
同 vdupq_n_f32,别名
veorq_s32(a, b)
异或
可用于清零:veorq(v, v)
4.7 float16 与 bfloat16 专用
Intrinsics
操作
说明
vaddq_f16(a, b)
加法
8 路 float16 并行(ARMv8.2-A FP16)
vmulq_f16(a, b)
乘法
8 路 float16 并行
vfmaq_f16(acc, a, b)
乘累加
8 路 float16 FMA
vcvtq_f32_f16(v)
转换
4×f16 → 4×f32
vcvt_f16_f32(v)
转换
4×f32 → 4×f16
vaddq_bf16(a, b)
加法
8 路 bfloat16(ARMv8.6-A BF16)
vmulq_bf16(a, b)
乘法
8 路 bfloat16
5. 实战案例:neon_get_mean_std_impl 逐行解析
5.1 背景
在音频异常检测中,需要对滑动窗口内的样本计算 均值(mean) 和 标准差(std) ,用于 sigma 异常点检测。传统标量实现需要两遍遍历:第一遍求 sum 和 sum²,第二遍计算方差。NEON 可以在一遍循环中同时完成。
5.2 完整代码
来源:detect_zero_padding_signal_fp_neon.c
static inline float32x4_t neon_get_mean_std_impl(const float* _a, int n)
{
// ① 初始化 4 个累加器,全部置零
float32x4_t res = vdupq_n_f32(0); // 结果向量 [unused, max, sum, sum²]
float32x4_t v_sy0 = vdupq_n_f32(0); // Σx 累加器
float32x4_t v_syy0 = vdupq_n_f32(0); // Σx² 累加器
float32x4_t v_max = vdupq_n_f32(_a[0]); // |x|_max 累加器
// ② 主循环:每次处理 8 个 float32(2 个 NEON 寄存器)
for (int k = 0; k + 7 < n; k += 8) {
// 加载 4 个 float32
float32x4_t _a0 = vld1q_f32(_a + k); // _a[k..k+3]
float32x4_t _a1 = vld1q_f32(_a + k + 4); // _a[k+4..k+7]
// 累加求和: Σx += _a0 + _a1
v_sy0 = vaddq_f32(v_sy0, _a0);
v_sy0 = vaddq_f32(v_sy0, _a1);
// 累加平方和: Σx² += _a0² + _a1²
v_syy0 = vaddq_f32(v_syy0, vmulq_f32(_a0, _a0));
v_syy0 = vaddq_f32(v_syy0, vmulq_f32(_a1, _a1));
// 更新绝对值最大值
v_max = vmaxq_f32(v_max, vabsq_f32(_a0));
v_max = vmaxq_f32(v_max, vabsq_f32(_a1));
}
// ③ 水平归约:将向量结果合并到标量
res[0] = 0; // 保留字段
res[1] = vmaxvq_f32(v_max); // 全局 |x|_max
res[2] += vaddvq_f32(v_sy0); // 全局 Σx
res[3] += vaddvq_f32(v_syy0); // 全局 Σx²
return res;
}
5.3 数据流图解
输入数组 _a[0..n-1]:
┌────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬ ... ─┐
│a[0]│a[1]│a[2]│a[3]│a[4]│a[5]│a[6]│a[7]│a[8]│a[9]│ │
└────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴───────┘
│ │ │
▼ ▼ ▼
第 0 轮 (k=0) 第 1 轮 (k=8) ...
┌────────────────┐ ┌────────────────┐
│ vld1q_f32 │ │ vld1q_f32 │
│ → _a0[0..3] │ │ → _a0[8..11] │
│ → _a1[4..7] │ │ → _a1[12..15] │
└───────┬────────┘ └───────┬────────┘
│ │
┌─────┼─────┬─────┐ │
▼ ▼ ▼ ▼ ▼
┌───────────────────────────────────┐
│ v_sy0 += _a0 + _a1 (累加 Σx) │
│ v_syy0 += _a0² + _a1² (累加 Σx²)│
│ v_max = max(|_a0|,|_a1|) │
└───────────────────────────────────┘
│
▼ 循环结束后
┌───────────────────────────────────┐
│ vaddvq_f32(v_sy0) → Σx 标量 │
│ vaddvq_f32(v_syy0) → Σx² 标量 │
│ vmaxvq_f32(v_max) → max|x| 标量│
└───────────────────────────────────┘
│
▼ 调用者计算
┌───────────────────────────────────┐
│ mean = Σx / n │
│ std = √(Σx²/n - mean²) │
└───────────────────────────────────┘
5.4 调用示例
// 在 zpd_detect() 中调用
const float* _a = audio + idx - windows;
float32x4_t ret = neon_get_mean_std_impl(_a, n);
if (ret[0]) continue; // 保留字段,可用于错误标志
float smax = ret[1]; // 窗口内最大绝对值
float sy = ret[2]; // Σx
float syy = ret[3]; // Σx²
if (smax < maxmax) continue; // 信号太弱,跳过
float mean = sy / (float)n;
float std = sqrt((syy / (float)n) - mean * mean);
// 5-sigma 异常检测
if (audio[idx] > mean + 5 * std || audio[idx] < mean - 5 * std)
sigma3Iy = 1;
5.5 关键设计要点
每次处理 8 个元素(2×4 路) :隐藏指令延迟,流水线更饱满
一遍循环完成 3 个统计量 :sum、sum²、max,减少内存遍历次数
水平归约延迟到最后 :vaddvq/vmaxvq 只在循环后调用一次
结果打包到 float32x4_t :避免多返回值的开销
6. 实战案例:int16 NEON 均值/方差计算
6.1 背景
在 detect_zero_padding_signal_s16.c 中,音频以 int16 存储,需要在 int16/int32 精度下完成统计。
6.2 代码
/* ---- y sigma ---- */
{
int k = 0;
int smax = 0;
int sy = 0;
int syy = 0;
const int16_t* _a = audio + idx - windows;
// 初始化累加器
int32x4_t v_sy0 = vdupq_n_s32(0); // Σx 累加器 (int32 宽度)
int16x4_t v_max = vdup_n_s16(0); // max|x| 累加器 (int16 宽度)
// 主循环:每次 8 个 int16
for (; k + 7 < n; k += 8) {
int16x4_t _a0 = vld1_s16(_a + k); // 加载 4×int16
int16x4_t _a1 = vld1_s16(_a + k + 4); // 加载 4×int16
// int16 → int32 扩展后累加(防溢出!)
v_sy0 = vaddw_s16(v_sy0, _a0); // Σx += _a0(int16 扩展到 int32)
v_sy0 = vaddw_s16(v_sy0, _a1);
// 更新绝对值最大值
v_max = vmax_s16(v_max, vabs_s16(_a0)); // max = max(max, |a0|)
v_max = vmax_s16(v_max, vabs_s16(_a1));
}
// 水平归约
smax = vmaxv_u16(v_max); // 全局 max|x|
if (smax < maxmax) continue;
sy += vaddvq_s32(v_sy0); // 全局 Σx
mean = (sy / n);
// 第二遍:计算 Σ|x - mean|
int32x4_t v_syy0 = vdupq_n_s32(0);
int16x4_t v_mean = vdup_n_s16((int16_t)mean);
for (k = 0; k + 7 < n; k += 8) {
int16x4_t _a0 = vld1_s16(_a + k);
int16x4_t _a1 = vld1_s16(_a + k + 4);
int16x4_t _b0 = vsub_s16(_a0, v_mean); // x - mean
int16x4_t _b1 = vsub_s16(_a1, v_mean);
v_syy0 = vaddw_s16(v_syy0, vabs_s16(_b0)); // Σ|x-mean|
v_syy0 = vaddw_s16(v_syy0, vabs_s16(_b1));
}
syy += vaddvq_s32(v_syy0);
std = (int16_t)(syy / n);
}
6.3 关键差异:vaddw 扩展累加
vaddw_s16(int32x4_t acc, int16x4_t a) 的工作方式:
acc: [ i32_0 ] [ i32_1 ] [ i32_2 ] [ i32_3 ]
+ + + +
a: [ i16_0 ] [ i16_1 ] [ i16_2 ] [ i16_3 ]
↓ 符号扩展 ↓ ↓ ↓
[ i32扩展 ] [ i32扩展 ] [ i32扩展 ] [ i32扩展 ]
= = = =
结果: [ new_i32_0] [ new_i32_1] [ new_i32_2] [ new_i32_3]
→ int16 自动扩展到 int32 后再累加,避免溢出
6.4 int16 vs float32 统计计算对比
方面
int16 版本
float32 版本
累加器宽度
int32 (防溢出)
float32 (本身)
扩展累加
vaddw_s16
vaddq_f32
绝对值
vabs_s16
vabsq_f32
平方
需 vmull_s16 扩展
vmulq_f32
方差公式
MAD(平均绝对偏差)
方差(Σx²/n - mean²)
水平归约
vaddvq_s32
vaddvq_f32
精度
受 int16 范围限制
~7 位有效数字
7. 实战案例:NEON 宏模板------批量向量运算
7.1 可展开的宏模板
arm_neon_acc.h 中定义了通用的 NEON 运算宏,支持任意展开宽度:
// UNROLL = 每次处理几个 float(必须是 4 的倍数)
#define NEON_ADD_UNROLL_FP32(a, b, c, n, UNROLL) \
do { \
if ((n) % (UNROLL)) return -1; \
for (ssize_t _i = 0; _i < (n); _i += (UNROLL)) { \
for (ssize_t _j = 0; _j < (UNROLL); _j += 4) { \
float32x4_t _va = vld1q_f32((a) + _i + _j); \
float32x4_t _vb = vld1q_f32((b) + _i + _j); \
vst1q_f32((c) + _i + _j, vaddq_f32(_va, _vb)); \
} \
} \
} while (0)
7.2 UNROLL 展开示意
UNROLL = 8 时,每次外循环处理 8 个 float,内循环 2 次 NEON 指令:
外循环 i=0:
┌──────────────────────────────┬──────────────────────────────┐
│ 内循环 j=0: vld1q + vld1q │ 内循环 j=4: vld1q + vld1q │
│ + vaddq + vst1q (4 个) │ + vaddq + vst1q (4 个) │
└──────────────────────────────┴──────────────────────────────┘
← 处理 a[0..3], b[0..3] → ← 处理 a[4..7], b[4..7] →
外循环 i=8:
← 处理 a[8..11], b[8..11] → ← 处理 a[12..15], b[12..15] →
7.3 使用示例
// 向量加法: c = a + b
float a[64], b[64], c[64];
NEON_ADD_UNROLL_FP32(a, b, c, 64, 4); // UNROLL=4: 每次 4 个
NEON_ADD_UNROLL_FP32(a, b, c, 64, 8); // UNROLL=8: 每次 8 个,更好
NEON_ADD_UNROLL_FP32(a, b, c, 64, 16); // UNROLL=16: 每次 16 个,最优
// 向量减法: c = a - b
NEON_SUB_UNROLL_FP32(a, b, c, 64, 8);
8. 实战案例:卷积/池化/ReLU 的 NEON 加速
8.1 1D 卷积 (conv1d_f32)
static inline int conv1d_f32(const float* input, const float* kernel,
const float* bias, float* output,
int input_len, int kernel_len,
int stride, int padding) {
int out_len = (input_len + 2 * padding - kernel_len) / stride + 1;
for (int o = 0; o < out_len; o++) {
float sum = 0.0f;
int start = o * stride - padding;
#if defined(__ARM_NEON)
int k = 0;
float32x4_t vsum = vdupq_n_f32(0.0f);
// NEON 主循环:每次处理 4 个 kernel 元素
for (; k + 3 < kernel_len; k += 4) {
int idx = start + k;
// 处理边界:越界位置填 0
float in0 = (idx >= 0 && idx < input_len) ? input[idx] : 0.0f;
float in1 = (idx+1 >= 0 && idx+1 < input_len) ? input[idx+1] : 0.0f;
float in2 = (idx+2 >= 0 && idx+2 < input_len) ? input[idx+2] : 0.0f;
float in3 = (idx+3 >= 0 && idx+3 < input_len) ? input[idx+3] : 0.0f;
float32x4_t vin = {in0, in1, in2, in3};
float32x4_t vker = vld1q_f32(&kernel[k]);
vsum = vmlaq_f32(vsum, vin, vker); // vsum += vin * vker
}
// 水平归约
sum = vaddvq_f32(vsum);
// 标量处理尾部
for (; k < kernel_len; k++) {
int idx = start + k;
float in_val = (idx >= 0 && idx < input_len) ? input[idx] : 0.0f;
sum += in_val * kernel[k];
}
#else
// 标量 fallback
for (int k = 0; k < kernel_len; k++) {
int idx = start + k;
float in_val = (idx >= 0 && idx < input_len) ? input[idx] : 0.0f;
sum += in_val * kernel[k];
}
#endif
if (bias) sum += *bias;
output[o] = sum;
}
return out_len;
}
8.2 Conv1d + ReLU 融合
// 关键:ReLU 只需一行改动
// 标量版本:output[o] = sum > 0.0f ? sum : 0.0f;
// NEON 融合版本(在主循环中):
float32x4_t vzero = vdupq_n_f32(0.0f);
vsum = vmaxq_f32(vsum, vzero); // 逐 lane ReLU
8.3 Mean Pooling
static inline int meanpool_f32(const float* input, float* output,
int input_len, int pool_size, int stride) {
int out_len = (input_len - pool_size) / stride + 1;
float inv_pool = 1.0f / (float)pool_size;
for (int o = 0; o < out_len; o++) {
float sum = 0.0f;
int start = o * stride;
#if defined(__ARM_NEON)
int k = 0;
float32x4_t vsum = vdupq_n_f32(0.0f);
for (; k + 3 < pool_size; k += 4) {
float32x4_t vdata = vld1q_f32(&input[start + k]);
vsum = vaddq_f32(vsum, vdata);
}
sum = vaddvq_f32(vsum);
for (; k < pool_size; k++)
sum += input[start + k];
#else
for (int k = 0; k < pool_size; k++)
sum += input[start + k];
#endif
output[o] = sum * inv_pool;
}
return out_len;
}
8.4 Max Pooling
float32x4_t vmax = vdupq_n_f32(-1e-10);
for (; k + 3 < pool_size; k += 4) {
float32x4_t vdata = vld1q_f32(&input[start + k]);
vmax = vmaxq_f32(vmax, vdata); // 逐 lane 取 max
}
float max_val = vmaxvq_f32(vmax); // 水平取 max
8.5 ReLU (in-place)
static inline int relu_f32(float* data, int len) {
float32x4_t vzero = vdupq_n_f32(0.0f);
int i = 0;
for (; i + 3 < len; i += 4) {
float32x4_t vdata = vld1q_f32(&data[i]);
vdata = vmaxq_f32(vdata, vzero); // ReLU: max(x, 0)
vst1q_f32(&data[i], vdata);
}
// 标量尾部
for (; i < len; i++)
if (data[i] < 0.0f) data[i] = 0.0f;
return 0;
}
9. float16 与 bfloat16 的 NEON 支持
9.1 float16 (IEEE 754 Half-Precision)
前提 :ARMv8.2-A + FP16 扩展(大多数 2018 年后的手机 SoC 支持)
#include <arm_neon.h>
// 编译选项:-march=armv8.2-a+fp16
// 加载 8 个 float16
float16x8_t v = vld1q_f16(half_ptr);
// 8 路 float16 乘法
float16x8_t result = vmulq_f16(a, b);
// 8 路 float16 FMA: acc += a * b
float16x8_t acc = vfmaq_f16(acc, a, b);
// float16 ↔ float32 转换
// 4×f16 → 4×f32(输入 64-bit,输出 128-bit)
float32x4_t f32 = vcvt_f32_f16(vget_low_f16(v));
// 4×f32 → 4×f16
float16x4_t f16 = vcvt_f16_f32(f32_val);
9.2 bfloat16 (Brain Float 16)
前提 :ARMv8.6-A + BF16 扩展(Cortex-A77+, Cortex-X1+)
// 编译选项:-march=armv8.6-a+bf16
// 加载 8 个 bfloat16
bfloat16x8_t v = vld1q_bf16(bf16_ptr);
// 8 路 bfloat16 乘法
bfloat16x8_t result = vmulq_bf16(a, b);
// bfloat16 → float32 转换(需先到 float32 才能做复杂运算)
// bf16 的主要价值在 ML 训练中保持与 f32 相同的指数范围
9.3 float16 vs bfloat16 在 NEON 中的差异
方面
float16
bfloat16
尾数精度
10 bit (~3.3 位十进制)
7 bit (~2.4 位十进制)
指数范围
±65504
±3.4×10³⁸ (同 f32)
NEON 并行数
8 路
8 路
FMA 支持
✅ vfmaq_f16
✅ vfmaq_bf16 (ARMv8.6+)
硬件可用性
广泛 (2018+)
较新 (2020+)
典型场景
移动端推理
ML 训练/推理
9.4 实用建议
// 推理场景的混合精度模式
// 1. 输入 float32,权重 float16
float16x8_t w_f16 = vld1q_f16(weight_half);
float32x4_t w_lo = vcvt_f32_f16(vget_low_f16(w_f16)); // 低 4 个
float32x4_t w_hi = vcvt_f32_f16(vget_high_f16(w_f16)); // 高 4 个
// 然后用 float32 做计算...
// 2. 输出截断回 float16
float16x4_t out_lo = vcvt_f16_f32(result_lo);
float16x4_t out_hi = vcvt_f16_f32(result_hi);
float16x8_t out = vcombine_f16(out_lo, out_hi);
vst1q_f16(output_ptr, out);
10. 性能优化技巧与踩坑指南
10.1 循环展开策略
推荐展开方式:每次处理 2×NEON_WIDTH
float32: 2×4 = 8 个/轮 → 减少循环开销,隐藏指令延迟
int16: 2×8 = 16 个/轮 → 同上
float16: 2×8 = 16 个/轮 → 同上
10.2 常见陷阱
陷阱
说明
解决方案
int16 乘法溢出
vmulq_s16 结果仍是 int16
用 vmlal_s16 扩展到 int32
对齐要求
vld1q 不要求对齐,但对齐更快
用 __attribute__((aligned(16)))
尾部处理
n 不是 4/8 的倍数时需标量处理
循环条件用 k + 3 < n
水平归约开销
vaddvq 有 3-4 cycle 延迟
只在循环外调用一次
float16 硬件缺失
模拟非常慢
运行时检测 getauxval(AT_HWCAP2)
#ifdef __ARM_NEON 位置
必须在编译时确定
NDK 默认启用,CMake 加 -mfpu=neon
10.3 编译选项
# CMakeLists.txt
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -O2 -march=armv8-a")
# 如果需要 float16 支持
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -march=armv8.2-a+fp16")
# 如果需要 bfloat16 支持
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -march=armv8.6-a+bf16")
10.4 性能对比参考
操作
标量
NEON (float32)
加速比
向量加法 (1K)
1.0x
3.5-4.0x
~4x
向量乘法 (1K)
1.0x
3.5-4.0x
~4x
mean+std (1K)
1.0x
3.0-3.5x
~3.5x
1D Conv (k=3)
1.0x
2.5-3.0x
~3x
ReLU (1K)
1.0x
3.5-4.0x
~4x
注:实际加速比取决于数据规模、内存带宽、CPU 微架构等因素。以上为典型值。
附录 A:NEON 类型速查(按数据类型索引)
float32
类型: float32x4_t (128-bit), float32x2_t (64-bit)
加载: vld1q_f32, vld1_f32
存储: vst1q_f32, vst1_f32
加法: vaddq_f32
减法: vsubq_f32
乘法: vmulq_f32
乘累加: vmlaq_f32 (acc += a*b), vfmaq_f32 (FMA)
除法: vdivq_f32
绝对值: vabsq_f32
最大值: vmaxq_f32
最小值: vminq_f32
水平求和: vaddvq_f32
水平最大: vmaxvq_f32
广播: vdupq_n_f32 / vmovq_n_f32
int16
类型: int16x8_t (128-bit), int16x4_t (64-bit)
加载: vld1q_s16, vld1_s16
存储: vst1q_s16, vst1_s16
加法: vaddq_s16, vadd_s16
减法: vsubq_s16, vsub_s16
乘法: vmulq_s16 (注意溢出!)
扩展乘累加: vmlal_s16 (int16→int32)
扩展加法: vaddw_s16 (int32 += int16)
绝对值: vabs_s16 (64-bit), vabsq_s16 (128-bit)
最大值: vmax_s16, vmaxq_s16
水平求和: vaddvq_s16, vaddv_s16
窄化: vqmovn_s32 (int32→int16, 饱和)
扩展: vmovl_s16 (int16→int32)
int32
类型: int32x4_t (128-bit), int32x2_t (64-bit)
加载: vld1q_s32
存储: vst1q_s32
加法: vaddq_s32
减法: vsubq_s32
乘法: vmulq_s32
乘累加: vmlaq_s32
水平求和: vaddvq_s32
广播: vdupq_n_s32
float16
类型: float16x8_t (128-bit), float16x4_t (64-bit)
加载: vld1q_f16
存储: vst1q_f16
加法: vaddq_f16
乘法: vmulq_f16
乘累加: vfmaq_f16
绝对值: vabsq_f16
最大值: vmaxq_f16
最小值: vminq_f16
转换→f32: vcvt_f32_f16 (64→128)
转换←f32: vcvt_f16_f32 (128→64)
合并: vcombine_f16(lo, hi) → float16x8_t
拆分: vget_low_f16, vget_high_f16
bfloat16
类型: bfloat16x8_t (128-bit), bfloat16x4_t (64-bit)
加载: vld1q_bf16
存储: vst1q_bf16
加法: vaddq_bf16
乘法: vmulq_bf16
乘累加: vfmaq_bf16
转换→f32: vcvt_f32_bf16
转换←f32: vcvt_bf16_f32
前提: ARMv8.6-A + bf16 扩展
附录 B:源文件索引
文件
数据类型
NEON 使用
detect_zero_padding_signal_fp_neon.c
float32
float32x4_t, vld1q_f32, vaddq_f32, vmulq_f32, vmaxq_f32, vabsq_f32, vaddvq_f32, vmaxvq_f32
detect_zero_padding_signal_s16.c
int16
int16x4_t, int32x4_t, vld1_s16, vaddw_s16, vsub_s16, vabs_s16, vmax_s16, vaddvq_s32, vmaxv_u16
detect_zero_padding_signal_int.c
int32
int32x4_t, vld1q_s32, vaddq_s32
arm_neon_acc.h
float32
通用宏模板 + conv/pool/relu 实现
版权声明 :本文代码片段来自 公司 内部项目,仅供技术学习交流使用。
irect/7bfd99cabe094412918fbd584d652b45.png)
目录
[什么是 SIMD 与 NEON](#什么是 SIMD 与 NEON)
[NEON 数据类型总览](#NEON 数据类型总览)
[int16 / int32 / float32 / float16 / bfloat16 对比](#int16 / int32 / float32 / float16 / bfloat16 对比)
[NEON Intrinsics 速查表](#NEON Intrinsics 速查表)
[实战案例:neon_get_mean_std_impl 逐行解析](#实战案例:neon_get_mean_std_impl 逐行解析)
[实战案例:int16 NEON 均值/方差计算](#实战案例:int16 NEON 均值/方差计算)
[实战案例:NEON 宏模板------批量向量运算](#实战案例:NEON 宏模板——批量向量运算)
[实战案例:卷积/池化/ReLU 的 NEON 加速](#实战案例:卷积/池化/ReLU 的 NEON 加速)
[float16 与 bfloat16 的 NEON 支持](#float16 与 bfloat16 的 NEON 支持)
性能优化技巧与踩坑指南
1. 什么是 SIMD 与 NEON
SIMD (Single Instruction, Multiple Data)是一种数据级并行技术:一条指令同时处理多个数据元素。
ARM NEON 是 ARM 架构内置的 SIMD 扩展,从 ARMv7 开始引入,AArch64 下称为 Advanced SIMD :
特性
AArch32 (ARMv7)
AArch64 (ARMv8-A)
寄存器宽度
128-bit (Q0-Q15)
128-bit (V0-V31)
寄存器数量
16 个 Q 寄存器
32 个 V 寄存器
float16 硬件支持
需要 VFPv4
原生支持
bfloat16 支持
无
ARMv8.6-A+ (BF16 扩展)
水平归约
需多条指令
vaddvq / vmaxvq 原生
NEON 编程的三种方式
方式
说明
适用场景
Intrinsics
C 函数形式调用 NEON 指令
✅ 推荐,可读性好,编译器可优化
内联汇编
直接写 NEON 汇编
极致优化,可移植性差
自动向量化
编译器 -O2 -ftree-vectorize 自动
简单循环,效果不可控
本文全部使用 NEON Intrinsics 方式。
2. NEON 数据类型总览
NEON 用向量寄存器(128-bit)承载不同精度的数据:
128-bit 寄存器的拆分方式:
┌─────────────────────────────────────────────────────────────────┐
│ 128-bit (q 寄存器) │
├─────────────────────────────────────────────────────────────────┤
│ float32x4 │ f32 │ f32 │ f32 │ f32 │ 4 × float32 │
├───────────┼───────┼───────┼───────┼───────┤ │
│ float16x8 │ f16 │ f16 │ f16 │ f16 │ 8 × float16 │
│ │ f16 │ f16 │ f16 │ f16 │ │
├───────────┼───────┼───────┼───────┼───────┤ │
│ int32x4 │ i32 │ i32 │ i32 │ i32 │ 4 × int32 │
├───────────┼───────┼───────┼───────┼───────┤ │
│ int16x8 │ i16 │ i16 │ i16 │ i16 │ 8 × int16 │
│ │ i16 │ i16 │ i16 │ i16 │ │
├───────────┼───────┼───────┼───────┼───────┤ │
│ int8x16 │ i8 × 16 │
└─────────────────────────────────────────────────────────────────┘
NEON 类型命名规则
<type><size>x<number_of_lanes>[_t]
示例:
float32x4_t → 4 个 float32(128-bit)
int16x4_t → 4 个 int16 (64-bit,d 寄存器)
int16x8_t → 8 个 int16 (128-bit,q 寄存器)
float16x8_t → 8 个 float16 (128-bit)
bfloat16x8_t → 8 个 bfloat16(128-bit,ARMv8.6-A+)
带 q 前缀的 intrinsics 操作 128-bit,不带 q 的操作 64-bit:
操作宽度
类型示例
Intrinsics 前缀
64-bit (d 寄存器)
int16x4_t, float32x2_t
vadd_s16, vld1_f32
128-bit (q 寄存器)
int16x8_t, float32x4_t
vaddq_s16, vld1q_f32
3. int16 / int32 / float32 / float16 / bfloat16 对比
3.1 数据格式位布局
int16 (Q15 定点):
┌──┬─────────────────────────────┐
│S │ 15 bit │ 范围: [-32768, 32767]
└──┴─────────────────────────────┘
int32:
┌──┬─────────────────────────────────────────────────────────────┐
│S │ 31 bit │ 范围: [-2^31, 2^31-1]
└──┴─────────────────────────────────────────────────────────────┘
float32 (IEEE 754):
┌──┬──────────────┬──────────────────────────────────────────────┐
│S │ Exp (8bit) │ Mantissa (23bit) │ 动态范围: ±3.4×10^38
└──┴──────────────┴──────────────────────────────────────────────┘
float16 (IEEE 754 half):
┌──┬────────┬──────────────────────┐
│S │Exp(5b) │ Mantissa (10bit) │ 动态范围: ±65504
└──┴────────┴──────────────────────┘
bfloat16 (Brain Float):
┌──┬──────────────┬──────────────┐
│S │ Exp (8bit) │ Mantissa(7b) │ 动态范围: 与 float32 相同
└──┴──────────────┴──────────────┘
3.2 五种数据类型全面对比
属性
int16 (Q15)
int32
float32
float16
bfloat16
位宽
16 bit
32 bit
32 bit
16 bit
16 bit
符号
有符号
有符号
有符号
有符号
有符号
精度
1 LSB ≈ 3×10⁻⁵
1 LSB ≈ 4.7×10⁻¹⁰
~7 位有效数字
~3.3 位有效数字
~2.4 位有效数字
动态范围
固定 -32768,32767
固定 -2³¹,2³¹-1
±3.4×10³⁸
±65504
±3.4×10³⁸
NEON 并行数 (128-bit)
8 路
4 路
4 路
8 路
8 路
NEON 向量类型
int16x8_t
int32x4_t
float32x4_t
float16x8_t
bfloat16x8_t
乘法溢出风险
高(需 vmlal 扩展)
高
无(自动指数)
中
中
水平归约
vaddvq_s16
vaddvq_s32
vaddvq_f32
需 AArch64
需 ARMv8.6-A
典型用途
音频 PCM
中间累加器
ML 推理/音频处理
移动端推理
训练梯度
3.3 精度与范围的直观对比
精度(可区分的最小变化):
int16 : ████████████████████████████████ 1/65536 ≈ 1.5e-5
float16: ██████████████████████ ~0.001 (在 1.0 附近)
bfloat16:████████████ ~0.008 (在 1.0 附近)
float32: ████████████████████████████████ ~6e-8 (在 1.0 附近)
动态范围(可表示的最大值 / 最小正值):
int16 : ████ 65536
float16: ██████████████ 65504 / 6e-8 ≈ 10^12
bfloat16:██████████████████████████████ 3.4e38 / 1.2e-38 ≈ 10^76
float32: ████████████████████████████████ 3.4e38 / 1.2e-38 ≈ 10^76
关键结论 :
int16 精度高但范围小,适合音频 PCM(满量程 0 dBFS)
float32 精度和范围都好,是通用计算的首选
float16 范围够但精度低,适合推理(不适合训练)
bfloat16 范围与 float32 相同但精度更低,专为 ML 训练设计
4. NEON Intrinsics 速查表
4.1 加载与存储
Intrinsics
操作
说明
vld1q_f32(ptr)
加载
从内存加载 4×float32 到 128-bit 寄存器
vld1q_s32(ptr)
加载
加载 4×int32
vld1q_s16(ptr)
加载
加载 8×int16
vld1_f32(ptr)
加载
加载 2×float32 到 64-bit 寄存器
vld1_s16(ptr)
加载
加载 4×int16 到 64-bit 寄存器
vst1q_f32(ptr, val)
存储
存储 4×float32 到内存
vst1q_s32(ptr, val)
存储
存储 4×int32
vst1q_s16(ptr, val)
存储
存储 8×int16
vld1q_f16(ptr)
加载
加载 8×float16(需 ARMv8.2-A FP16)
vld1q_bf16(ptr)
加载
加载 8×bfloat16(需 ARMv8.6-A BF16)
4.2 算术运算
Intrinsics
操作
说明
vaddq_f32(a, b)
加法
4 路 float32 并行加法
vaddq_s32(a, b)
加法
4 路 int32 并行加法
vaddq_s16(a, b)
加法
8 路 int16 并行加法
vsubq_f32(a, b)
减法
4 路 float32 并行减法
vsubq_s16(a, b)
减法
8 路 int16 并行减法
vmulq_f32(a, b)
乘法
4 路 float32 并行乘法
vmulq_s16(a, b)
乘法
8 路 int16 并行乘法(注意溢出!)
vmlaq_f32(acc, a, b)
乘累加
acc += a * b,4 路 float32
vmlal_s16(acc, a, b)
扩展乘累加
acc += a * b,int16→int32 扩展,防溢出
vdivq_f32(a, b)
除法
4 路 float32(需 ARMv8-A)
4.3 比较与选择
Intrinsics
操作
说明
vmaxq_f32(a, b)
最大值
逐 lane 取 max
vminq_f32(a, b)
最小值
逐 lane 取 min
vabsq_f32(v)
绝对值
逐 lane 取绝对值
vabs_s16(v)
绝对值
4 路 int16 绝对值(64-bit)
vmaxvq_f32(v)
水平最大
4 个 lane 求最大值(AArch64)
vminvq_f32(v)
水平最小
4 个 lane 求最小值(AArch64)
4.4 类型转换
Intrinsics
操作
说明
vcvtq_f32_s32(v)
转换
int32 → float32
vcvtq_s32_f32(v)
转换
float32 → int32(截断)
vcvt_f32_f16(v)
转换
float16 → float32(64-bit→128-bit)
vcvt_f16_f32(v)
转换
float32 → float16(128-bit→64-bit)
vreinterpretq_f32_s32(v)
重解释
bit 不变,类型重新解释
vmovl_s16(v)
扩展
int16x4 → int32x4(高位补符号位)
vqmovn_s32(v)
饱和窄化
int32x4 → int16x4(饱和截断)
4.5 归约(Reduction)--- AArch64 专有
Intrinsics
操作
说明
vaddvq_f32(v)
水平求和
4 个 float32 lane 求和 → 标量
vaddvq_s32(v)
水平求和
4 个 int32 lane 求和 → 标量
vaddvq_s16(v)
水平求和
8 个 int16 lane 求和 → 标量
vaddv_s16(v)
水平求和
4 个 int16 lane 求和 → 标量
vmaxvq_f32(v)
水平最大
4 个 float32 求最大 → 标量
vmaxv_u16(v)
水平最大
4 个 uint16 求最大 → 标量
4.6 常量构造
Intrinsics
操作
说明
vdupq_n_f32(val)
广播
4 个 lane 都设为 val
vdupq_n_s32(val)
广播
4 个 int32 lane 都设为 val
vdup_n_s16(val)
广播
4 个 int16 lane 都设为 val(64-bit)
vmovq_n_f32(val)
广播
同 vdupq_n_f32,别名
veorq_s32(a, b)
异或
可用于清零:veorq(v, v)
4.7 float16 与 bfloat16 专用
Intrinsics
操作
说明
vaddq_f16(a, b)
加法
8 路 float16 并行(ARMv8.2-A FP16)
vmulq_f16(a, b)
乘法
8 路 float16 并行
vfmaq_f16(acc, a, b)
乘累加
8 路 float16 FMA
vcvtq_f32_f16(v)
转换
4×f16 → 4×f32
vcvt_f16_f32(v)
转换
4×f32 → 4×f16
vaddq_bf16(a, b)
加法
8 路 bfloat16(ARMv8.6-A BF16)
vmulq_bf16(a, b)
乘法
8 路 bfloat16
5. 实战案例:neon_get_mean_std_impl 逐行解析
5.1 背景
在音频异常检测中,需要对滑动窗口内的样本计算 均值(mean) 和 标准差(std) ,用于 sigma 异常点检测。传统标量实现需要两遍遍历:第一遍求 sum 和 sum²,第二遍计算方差。NEON 可以在一遍循环中同时完成。
5.2 完整代码
来源:detect_zero_padding_signal_fp_neon.c
static inline float32x4_t neon_get_mean_std_impl(const float* _a, int n)
{
// ① 初始化 4 个累加器,全部置零
float32x4_t res = vdupq_n_f32(0); // 结果向量 [unused, max, sum, sum²]
float32x4_t v_sy0 = vdupq_n_f32(0); // Σx 累加器
float32x4_t v_syy0 = vdupq_n_f32(0); // Σx² 累加器
float32x4_t v_max = vdupq_n_f32(_a[0]); // |x|_max 累加器
// ② 主循环:每次处理 8 个 float32(2 个 NEON 寄存器)
for (int k = 0; k + 7 < n; k += 8) {
// 加载 4 个 float32
float32x4_t _a0 = vld1q_f32(_a + k); // _a[k..k+3]
float32x4_t _a1 = vld1q_f32(_a + k + 4); // _a[k+4..k+7]
// 累加求和: Σx += _a0 + _a1
v_sy0 = vaddq_f32(v_sy0, _a0);
v_sy0 = vaddq_f32(v_sy0, _a1);
// 累加平方和: Σx² += _a0² + _a1²
v_syy0 = vaddq_f32(v_syy0, vmulq_f32(_a0, _a0));
v_syy0 = vaddq_f32(v_syy0, vmulq_f32(_a1, _a1));
// 更新绝对值最大值
v_max = vmaxq_f32(v_max, vabsq_f32(_a0));
v_max = vmaxq_f32(v_max, vabsq_f32(_a1));
}
// ③ 水平归约:将向量结果合并到标量
res[0] = 0; // 保留字段
res[1] = vmaxvq_f32(v_max); // 全局 |x|_max
res[2] += vaddvq_f32(v_sy0); // 全局 Σx
res[3] += vaddvq_f32(v_syy0); // 全局 Σx²
return res;
}
5.3 数据流图解
输入数组 _a[0..n-1]:
┌────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬ ... ─┐
│a[0]│a[1]│a[2]│a[3]│a[4]│a[5]│a[6]│a[7]│a[8]│a[9]│ │
└────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴───────┘
│ │ │
▼ ▼ ▼
第 0 轮 (k=0) 第 1 轮 (k=8) ...
┌────────────────┐ ┌────────────────┐
│ vld1q_f32 │ │ vld1q_f32 │
│ → _a0[0..3] │ │ → _a0[8..11] │
│ → _a1[4..7] │ │ → _a1[12..15] │
└───────┬────────┘ └───────┬────────┘
│ │
┌─────┼─────┬─────┐ │
▼ ▼ ▼ ▼ ▼
┌───────────────────────────────────┐
│ v_sy0 += _a0 + _a1 (累加 Σx) │
│ v_syy0 += _a0² + _a1² (累加 Σx²)│
│ v_max = max(|_a0|,|_a1|) │
└───────────────────────────────────┘
│
▼ 循环结束后
┌───────────────────────────────────┐
│ vaddvq_f32(v_sy0) → Σx 标量 │
│ vaddvq_f32(v_syy0) → Σx² 标量 │
│ vmaxvq_f32(v_max) → max|x| 标量│
└───────────────────────────────────┘
│
▼ 调用者计算
┌───────────────────────────────────┐
│ mean = Σx / n │
│ std = √(Σx²/n - mean²) │
└───────────────────────────────────┘
5.4 调用示例
// 在 zpd_detect() 中调用
const float* _a = audio + idx - windows;
float32x4_t ret = neon_get_mean_std_impl(_a, n);
if (ret[0]) continue; // 保留字段,可用于错误标志
float smax = ret[1]; // 窗口内最大绝对值
float sy = ret[2]; // Σx
float syy = ret[3]; // Σx²
if (smax < maxmax) continue; // 信号太弱,跳过
float mean = sy / (float)n;
float std = sqrt((syy / (float)n) - mean * mean);
// 5-sigma 异常检测
if (audio[idx] > mean + 5 * std || audio[idx] < mean - 5 * std)
sigma3Iy = 1;
5.5 关键设计要点
每次处理 8 个元素(2×4 路) :隐藏指令延迟,流水线更饱满
一遍循环完成 3 个统计量 :sum、sum²、max,减少内存遍历次数
水平归约延迟到最后 :vaddvq/vmaxvq 只在循环后调用一次
结果打包到 float32x4_t :避免多返回值的开销
6. 实战案例:int16 NEON 均值/方差计算
6.1 背景
在 detect_zero_padding_signal_s16.c 中,音频以 int16 存储,需要在 int16/int32 精度下完成统计。
6.2 代码
/* ---- y sigma ---- */
{
int k = 0;
int smax = 0;
int sy = 0;
int syy = 0;
const int16_t* _a = audio + idx - windows;
// 初始化累加器
int32x4_t v_sy0 = vdupq_n_s32(0); // Σx 累加器 (int32 宽度)
int16x4_t v_max = vdup_n_s16(0); // max|x| 累加器 (int16 宽度)
// 主循环:每次 8 个 int16
for (; k + 7 < n; k += 8) {
int16x4_t _a0 = vld1_s16(_a + k); // 加载 4×int16
int16x4_t _a1 = vld1_s16(_a + k + 4); // 加载 4×int16
// int16 → int32 扩展后累加(防溢出!)
v_sy0 = vaddw_s16(v_sy0, _a0); // Σx += _a0(int16 扩展到 int32)
v_sy0 = vaddw_s16(v_sy0, _a1);
// 更新绝对值最大值
v_max = vmax_s16(v_max, vabs_s16(_a0)); // max = max(max, |a0|)
v_max = vmax_s16(v_max, vabs_s16(_a1));
}
// 水平归约
smax = vmaxv_u16(v_max); // 全局 max|x|
if (smax < maxmax) continue;
sy += vaddvq_s32(v_sy0); // 全局 Σx
mean = (sy / n);
// 第二遍:计算 Σ|x - mean|
int32x4_t v_syy0 = vdupq_n_s32(0);
int16x4_t v_mean = vdup_n_s16((int16_t)mean);
for (k = 0; k + 7 < n; k += 8) {
int16x4_t _a0 = vld1_s16(_a + k);
int16x4_t _a1 = vld1_s16(_a + k + 4);
int16x4_t _b0 = vsub_s16(_a0, v_mean); // x - mean
int16x4_t _b1 = vsub_s16(_a1, v_mean);
v_syy0 = vaddw_s16(v_syy0, vabs_s16(_b0)); // Σ|x-mean|
v_syy0 = vaddw_s16(v_syy0, vabs_s16(_b1));
}
syy += vaddvq_s32(v_syy0);
std = (int16_t)(syy / n);
}
6.3 关键差异:vaddw 扩展累加
vaddw_s16(int32x4_t acc, int16x4_t a) 的工作方式:
acc: [ i32_0 ] [ i32_1 ] [ i32_2 ] [ i32_3 ]
+ + + +
a: [ i16_0 ] [ i16_1 ] [ i16_2 ] [ i16_3 ]
↓ 符号扩展 ↓ ↓ ↓
[ i32扩展 ] [ i32扩展 ] [ i32扩展 ] [ i32扩展 ]
= = = =
结果: [ new_i32_0] [ new_i32_1] [ new_i32_2] [ new_i32_3]
→ int16 自动扩展到 int32 后再累加,避免溢出
6.4 int16 vs float32 统计计算对比
方面
int16 版本
float32 版本
累加器宽度
int32 (防溢出)
float32 (本身)
扩展累加
vaddw_s16
vaddq_f32
绝对值
vabs_s16
vabsq_f32
平方
需 vmull_s16 扩展
vmulq_f32
方差公式
MAD(平均绝对偏差)
方差(Σx²/n - mean²)
水平归约
vaddvq_s32
vaddvq_f32
精度
受 int16 范围限制
~7 位有效数字
7. 实战案例:NEON 宏模板------批量向量运算
7.1 可展开的宏模板
arm_neon_acc.h 中定义了通用的 NEON 运算宏,支持任意展开宽度:
// UNROLL = 每次处理几个 float(必须是 4 的倍数)
#define NEON_ADD_UNROLL_FP32(a, b, c, n, UNROLL) \
do { \
if ((n) % (UNROLL)) return -1; \
for (ssize_t _i = 0; _i < (n); _i += (UNROLL)) { \
for (ssize_t _j = 0; _j < (UNROLL); _j += 4) { \
float32x4_t _va = vld1q_f32((a) + _i + _j); \
float32x4_t _vb = vld1q_f32((b) + _i + _j); \
vst1q_f32((c) + _i + _j, vaddq_f32(_va, _vb)); \
} \
} \
} while (0)
7.2 UNROLL 展开示意
UNROLL = 8 时,每次外循环处理 8 个 float,内循环 2 次 NEON 指令:
外循环 i=0:
┌──────────────────────────────┬──────────────────────────────┐
│ 内循环 j=0: vld1q + vld1q │ 内循环 j=4: vld1q + vld1q │
│ + vaddq + vst1q (4 个) │ + vaddq + vst1q (4 个) │
└──────────────────────────────┴──────────────────────────────┘
← 处理 a[0..3], b[0..3] → ← 处理 a[4..7], b[4..7] →
外循环 i=8:
← 处理 a[8..11], b[8..11] → ← 处理 a[12..15], b[12..15] →
7.3 使用示例
// 向量加法: c = a + b
float a[64], b[64], c[64];
NEON_ADD_UNROLL_FP32(a, b, c, 64, 4); // UNROLL=4: 每次 4 个
NEON_ADD_UNROLL_FP32(a, b, c, 64, 8); // UNROLL=8: 每次 8 个,更好
NEON_ADD_UNROLL_FP32(a, b, c, 64, 16); // UNROLL=16: 每次 16 个,最优
// 向量减法: c = a - b
NEON_SUB_UNROLL_FP32(a, b, c, 64, 8);
8. 实战案例:卷积/池化/ReLU 的 NEON 加速
8.1 1D 卷积 (conv1d_f32)
static inline int conv1d_f32(const float* input, const float* kernel,
const float* bias, float* output,
int input_len, int kernel_len,
int stride, int padding) {
int out_len = (input_len + 2 * padding - kernel_len) / stride + 1;
for (int o = 0; o < out_len; o++) {
float sum = 0.0f;
int start = o * stride - padding;
#if defined(__ARM_NEON)
int k = 0;
float32x4_t vsum = vdupq_n_f32(0.0f);
// NEON 主循环:每次处理 4 个 kernel 元素
for (; k + 3 < kernel_len; k += 4) {
int idx = start + k;
// 处理边界:越界位置填 0
float in0 = (idx >= 0 && idx < input_len) ? input[idx] : 0.0f;
float in1 = (idx+1 >= 0 && idx+1 < input_len) ? input[idx+1] : 0.0f;
float in2 = (idx+2 >= 0 && idx+2 < input_len) ? input[idx+2] : 0.0f;
float in3 = (idx+3 >= 0 && idx+3 < input_len) ? input[idx+3] : 0.0f;
float32x4_t vin = {in0, in1, in2, in3};
float32x4_t vker = vld1q_f32(&kernel[k]);
vsum = vmlaq_f32(vsum, vin, vker); // vsum += vin * vker
}
// 水平归约
sum = vaddvq_f32(vsum);
// 标量处理尾部
for (; k < kernel_len; k++) {
int idx = start + k;
float in_val = (idx >= 0 && idx < input_len) ? input[idx] : 0.0f;
sum += in_val * kernel[k];
}
#else
// 标量 fallback
for (int k = 0; k < kernel_len; k++) {
int idx = start + k;
float in_val = (idx >= 0 && idx < input_len) ? input[idx] : 0.0f;
sum += in_val * kernel[k];
}
#endif
if (bias) sum += *bias;
output[o] = sum;
}
return out_len;
}
8.2 Conv1d + ReLU 融合
// 关键:ReLU 只需一行改动
// 标量版本:output[o] = sum > 0.0f ? sum : 0.0f;
// NEON 融合版本(在主循环中):
float32x4_t vzero = vdupq_n_f32(0.0f);
vsum = vmaxq_f32(vsum, vzero); // 逐 lane ReLU
8.3 Mean Pooling
static inline int meanpool_f32(const float* input, float* output,
int input_len, int pool_size, int stride) {
int out_len = (input_len - pool_size) / stride + 1;
float inv_pool = 1.0f / (float)pool_size;
for (int o = 0; o < out_len; o++) {
float sum = 0.0f;
int start = o * stride;
#if defined(__ARM_NEON)
int k = 0;
float32x4_t vsum = vdupq_n_f32(0.0f);
for (; k + 3 < pool_size; k += 4) {
float32x4_t vdata = vld1q_f32(&input[start + k]);
vsum = vaddq_f32(vsum, vdata);
}
sum = vaddvq_f32(vsum);
for (; k < pool_size; k++)
sum += input[start + k];
#else
for (int k = 0; k < pool_size; k++)
sum += input[start + k];
#endif
output[o] = sum * inv_pool;
}
return out_len;
}
8.4 Max Pooling
float32x4_t vmax = vdupq_n_f32(-1e-10);
for (; k + 3 < pool_size; k += 4) {
float32x4_t vdata = vld1q_f32(&input[start + k]);
vmax = vmaxq_f32(vmax, vdata); // 逐 lane 取 max
}
float max_val = vmaxvq_f32(vmax); // 水平取 max
8.5 ReLU (in-place)
static inline int relu_f32(float* data, int len) {
float32x4_t vzero = vdupq_n_f32(0.0f);
int i = 0;
for (; i + 3 < len; i += 4) {
float32x4_t vdata = vld1q_f32(&data[i]);
vdata = vmaxq_f32(vdata, vzero); // ReLU: max(x, 0)
vst1q_f32(&data[i], vdata);
}
// 标量尾部
for (; i < len; i++)
if (data[i] < 0.0f) data[i] = 0.0f;
return 0;
}
9. float16 与 bfloat16 的 NEON 支持
9.1 float16 (IEEE 754 Half-Precision)
前提 :ARMv8.2-A + FP16 扩展(大多数 2018 年后的手机 SoC 支持)
#include <arm_neon.h>
// 编译选项:-march=armv8.2-a+fp16
// 加载 8 个 float16
float16x8_t v = vld1q_f16(half_ptr);
// 8 路 float16 乘法
float16x8_t result = vmulq_f16(a, b);
// 8 路 float16 FMA: acc += a * b
float16x8_t acc = vfmaq_f16(acc, a, b);
// float16 ↔ float32 转换
// 4×f16 → 4×f32(输入 64-bit,输出 128-bit)
float32x4_t f32 = vcvt_f32_f16(vget_low_f16(v));
// 4×f32 → 4×f16
float16x4_t f16 = vcvt_f16_f32(f32_val);
9.2 bfloat16 (Brain Float 16)
前提 :ARMv8.6-A + BF16 扩展(Cortex-A77+, Cortex-X1+)
// 编译选项:-march=armv8.6-a+bf16
// 加载 8 个 bfloat16
bfloat16x8_t v = vld1q_bf16(bf16_ptr);
// 8 路 bfloat16 乘法
bfloat16x8_t result = vmulq_bf16(a, b);
// bfloat16 → float32 转换(需先到 float32 才能做复杂运算)
// bf16 的主要价值在 ML 训练中保持与 f32 相同的指数范围
9.3 float16 vs bfloat16 在 NEON 中的差异
方面
float16
bfloat16
尾数精度
10 bit (~3.3 位十进制)
7 bit (~2.4 位十进制)
指数范围
±65504
±3.4×10³⁸ (同 f32)
NEON 并行数
8 路
8 路
FMA 支持
✅ vfmaq_f16
✅ vfmaq_bf16 (ARMv8.6+)
硬件可用性
广泛 (2018+)
较新 (2020+)
典型场景
移动端推理
ML 训练/推理
9.4 实用建议
// 推理场景的混合精度模式
// 1. 输入 float32,权重 float16
float16x8_t w_f16 = vld1q_f16(weight_half);
float32x4_t w_lo = vcvt_f32_f16(vget_low_f16(w_f16)); // 低 4 个
float32x4_t w_hi = vcvt_f32_f16(vget_high_f16(w_f16)); // 高 4 个
// 然后用 float32 做计算...
// 2. 输出截断回 float16
float16x4_t out_lo = vcvt_f16_f32(result_lo);
float16x4_t out_hi = vcvt_f16_f32(result_hi);
float16x8_t out = vcombine_f16(out_lo, out_hi);
vst1q_f16(output_ptr, out);
10. 性能优化技巧与踩坑指南
10.1 循环展开策略
推荐展开方式:每次处理 2×NEON_WIDTH
float32: 2×4 = 8 个/轮 → 减少循环开销,隐藏指令延迟
int16: 2×8 = 16 个/轮 → 同上
float16: 2×8 = 16 个/轮 → 同上
10.2 常见陷阱
陷阱
说明
解决方案
int16 乘法溢出
vmulq_s16 结果仍是 int16
用 vmlal_s16 扩展到 int32
对齐要求
vld1q 不要求对齐,但对齐更快
用 __attribute__((aligned(16)))
尾部处理
n 不是 4/8 的倍数时需标量处理
循环条件用 k + 3 < n
水平归约开销
vaddvq 有 3-4 cycle 延迟
只在循环外调用一次
float16 硬件缺失
模拟非常慢
运行时检测 getauxval(AT_HWCAP2)
#ifdef __ARM_NEON 位置
必须在编译时确定
NDK 默认启用,CMake 加 -mfpu=neon
10.3 编译选项
# CMakeLists.txt
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -O2 -march=armv8-a")
# 如果需要 float16 支持
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -march=armv8.2-a+fp16")
# 如果需要 bfloat16 支持
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -march=armv8.6-a+bf16")
10.4 性能对比参考
操作
标量
NEON (float32)
加速比
向量加法 (1K)
1.0x
3.5-4.0x
~4x
向量乘法 (1K)
1.0x
3.5-4.0x
~4x
mean+std (1K)
1.0x
3.0-3.5x
~3.5x
1D Conv (k=3)
1.0x
2.5-3.0x
~3x
ReLU (1K)
1.0x
3.5-4.0x
~4x
注:实际加速比取决于数据规模、内存带宽、CPU 微架构等因素。以上为典型值。
附录 A:NEON 类型速查(按数据类型索引)
float32
类型: float32x4_t (128-bit), float32x2_t (64-bit)
加载: vld1q_f32, vld1_f32
存储: vst1q_f32, vst1_f32
加法: vaddq_f32
减法: vsubq_f32
乘法: vmulq_f32
乘累加: vmlaq_f32 (acc += a*b), vfmaq_f32 (FMA)
除法: vdivq_f32
绝对值: vabsq_f32
最大值: vmaxq_f32
最小值: vminq_f32
水平求和: vaddvq_f32
水平最大: vmaxvq_f32
广播: vdupq_n_f32 / vmovq_n_f32
int16
类型: int16x8_t (128-bit), int16x4_t (64-bit)
加载: vld1q_s16, vld1_s16
存储: vst1q_s16, vst1_s16
加法: vaddq_s16, vadd_s16
减法: vsubq_s16, vsub_s16
乘法: vmulq_s16 (注意溢出!)
扩展乘累加: vmlal_s16 (int16→int32)
扩展加法: vaddw_s16 (int32 += int16)
绝对值: vabs_s16 (64-bit), vabsq_s16 (128-bit)
最大值: vmax_s16, vmaxq_s16
水平求和: vaddvq_s16, vaddv_s16
窄化: vqmovn_s32 (int32→int16, 饱和)
扩展: vmovl_s16 (int16→int32)
int32
类型: int32x4_t (128-bit), int32x2_t (64-bit)
加载: vld1q_s32
存储: vst1q_s32
加法: vaddq_s32
减法: vsubq_s32
乘法: vmulq_s32
乘累加: vmlaq_s32
水平求和: vaddvq_s32
广播: vdupq_n_s32
float16
类型: float16x8_t (128-bit), float16x4_t (64-bit)
加载: vld1q_f16
存储: vst1q_f16
加法: vaddq_f16
乘法: vmulq_f16
乘累加: vfmaq_f16
绝对值: vabsq_f16
最大值: vmaxq_f16
最小值: vminq_f16
转换→f32: vcvt_f32_f16 (64→128)
转换←f32: vcvt_f16_f32 (128→64)
合并: vcombine_f16(lo, hi) → float16x8_t
拆分: vget_low_f16, vget_high_f16
bfloat16
类型: bfloat16x8_t (128-bit), bfloat16x4_t (64-bit)
加载: vld1q_bf16
存储: vst1q_bf16
加法: vaddq_bf16
乘法: vmulq_bf16
乘累加: vfmaq_bf16
转换→f32: vcvt_f32_bf16
转换←f32: vcvt_bf16_f32
前提: ARMv8.6-A + bf16 扩展
附录 B:源文件索引
文件
数据类型
NEON 使用
detect_zero_padding_signal_fp_neon.c
float32
float32x4_t, vld1q_f32, vaddq_f32, vmulq_f32, vmaxq_f32, vabsq_f32, vaddvq_f32, vmaxvq_f32
detect_zero_padding_signal_s16.c
int16
int16x4_t, int32x4_t, vld1_s16, vaddw_s16, vsub_s16, vabs_s16, vmax_s16, vaddvq_s32, vmaxv_u16
detect_zero_padding_signal_int.c
int32
int32x4_t, vld1q_s32, vaddq_s32
arm_neon_acc.h
float32
通用宏模板 + conv/pool/relu 实现
版权声明 :本文代码片段来自 公司 内部项目,仅供技术学习交流使用。