ARM NEON SIMD 编程实战:从音频信号处理到AI算子研发实战

ARM NEON SIMD 编程实战:从 int16/int32/float32 到音频异常检测---AI算子研发

日期 :2026-06-11

适用平台 :AArch64 (ARMv8-A+) / Android NDK / Linux ARM

目录

  1. [什么是 SIMD 与 NEON](#什么是 SIMD 与 NEON)
  2. [NEON 数据类型总览](#NEON 数据类型总览)
  3. [int16 / int32 / float32 / float16 / bfloat16 对比](#int16 / int32 / float32 / float16 / bfloat16 对比)
  4. [NEON Intrinsics 速查表](#NEON Intrinsics 速查表)
  5. [实战案例:neon_get_mean_std_impl 逐行解析](#实战案例:neon_get_mean_std_impl 逐行解析)
  6. [实战案例:int16 NEON 均值/方差计算](#实战案例:int16 NEON 均值/方差计算)
  7. [实战案例:NEON 宏模板------批量向量运算](#实战案例:NEON 宏模板——批量向量运算)
  8. [实战案例:卷积/池化/ReLU 的 NEON 加速](#实战案例:卷积/池化/ReLU 的 NEON 加速)
  9. [float16 与 bfloat16 的 NEON 支持](#float16 与 bfloat16 的 NEON 支持)
  10. 性能优化技巧与踩坑指南

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

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 调用示例

c 复制代码
// 在 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 关键设计要点

  1. 每次处理 8 个元素(2×4 路):隐藏指令延迟,流水线更饱满
  2. 一遍循环完成 3 个统计量:sum、sum²、max,减少内存遍历次数
  3. 水平归约延迟到最后vaddvq/vmaxvq 只在循环后调用一次
  4. 结果打包到 float32x4_t:避免多返回值的开销

6. 实战案例:int16 NEON 均值/方差计算

6.1 背景

detect_zero_padding_signal_s16.c 中,音频以 int16 存储,需要在 int16/int32 精度下完成统计。

6.2 代码

c 复制代码
/* ---- 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 运算宏,支持任意展开宽度:

c 复制代码
// 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 复制代码
// 向量加法: 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)

c 复制代码
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 融合

c 复制代码
// 关键: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

c 复制代码
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

c 复制代码
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)

c 复制代码
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 支持)

c 复制代码
#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+)

c 复制代码
// 编译选项:-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 实用建议

c 复制代码
// 推理场景的混合精度模式
// 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 编译选项

cmake 复制代码
# 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)

目录

  1. [什么是 SIMD 与 NEON](#什么是 SIMD 与 NEON)
  2. [NEON 数据类型总览](#NEON 数据类型总览)
  3. [int16 / int32 / float32 / float16 / bfloat16 对比](#int16 / int32 / float32 / float16 / bfloat16 对比)
  4. [NEON Intrinsics 速查表](#NEON Intrinsics 速查表)
  5. [实战案例:neon_get_mean_std_impl 逐行解析](#实战案例:neon_get_mean_std_impl 逐行解析)
  6. [实战案例:int16 NEON 均值/方差计算](#实战案例:int16 NEON 均值/方差计算)
  7. [实战案例:NEON 宏模板------批量向量运算](#实战案例:NEON 宏模板——批量向量运算)
  8. [实战案例:卷积/池化/ReLU 的 NEON 加速](#实战案例:卷积/池化/ReLU 的 NEON 加速)
  9. [float16 与 bfloat16 的 NEON 支持](#float16 与 bfloat16 的 NEON 支持)
  10. 性能优化技巧与踩坑指南

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

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 调用示例

c 复制代码
// 在 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 关键设计要点

  1. 每次处理 8 个元素(2×4 路):隐藏指令延迟,流水线更饱满
  2. 一遍循环完成 3 个统计量:sum、sum²、max,减少内存遍历次数
  3. 水平归约延迟到最后vaddvq/vmaxvq 只在循环后调用一次
  4. 结果打包到 float32x4_t:避免多返回值的开销

6. 实战案例:int16 NEON 均值/方差计算

6.1 背景

detect_zero_padding_signal_s16.c 中,音频以 int16 存储,需要在 int16/int32 精度下完成统计。

6.2 代码

c 复制代码
/* ---- 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 运算宏,支持任意展开宽度:

c 复制代码
// 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 复制代码
// 向量加法: 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)

c 复制代码
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 融合

c 复制代码
// 关键: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

c 复制代码
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

c 复制代码
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)

c 复制代码
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 支持)

c 复制代码
#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+)

c 复制代码
// 编译选项:-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 实用建议

c 复制代码
// 推理场景的混合精度模式
// 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 编译选项

cmake 复制代码
# 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 实现

版权声明:本文代码片段来自 公司 内部项目,仅供技术学习交流使用。

相关推荐
dualven_in_csdn3 小时前
一键起飞条件分析
音视频
有什么事5 小时前
云手机多开哪个强?ARM架构:云手机多开的信任基石与性能核心
arm开发·智能手机·架构
头枝5 小时前
ARM Cortex-M DWT CYCCNT 必须显式初始化,jlink调试时正常,使用时异常的问题
arm开发·stm32·单片机·问题·cyccnt
咖喱年糕6 小时前
KEIL5 编译错误解决:MDK 5.37 及以上版本添加 AC5(ARM Compiler 5)编译器
arm开发·单片机·keil·嵌入式软件·ac5
故渊at6 小时前
第九板块:Android 多媒体体系 | 第二十三篇:AudioFlinger 与 AudioPolicyService 音频架构
android·架构·音视频·audiopolicy·audioflinger
纳祥科技7 小时前
NX699,内置精度±5%晶振的lightning苹果PD快充12W
单片机·手机·音视频
振南的单片机世界8 小时前
中央对齐PWM:边沿对齐EMI大,中央对齐更安静
arm开发·stm32·单片机·嵌入式硬件
三佛科技-187366133978 小时前
GD32F103VDT6是什么芯片?GD32 ARM Cortex-M3微控制器MCU解析
arm开发·单片机·嵌入式硬件
学Linux的语莫8 小时前
OpenCV 视频处理入门教程
人工智能·opencv·音视频