3. Hexagon 向量扩展指令集(HVX)基础

好,咱们进入第三章节。HVX,全称 Hexagon Vector eXtensions,是高通 DSP 上最核心的加速单元。说白了,它就是一套专门为信号处理、图像计算、AI 推理量身打造的向量处理引擎。你想想看,一个音频降噪算法,里面全是 FIR 滤波、FFT、矩阵乘法,用标量指令一条一条算,那效率得多低?HVX 就是来解决这个问题的。

我刚开始接触 HVX 时,其实有点懵。寄存器名字长,指令又多,跟 ARM NEON 和 x86 AVX 都不太一样。但摸熟了之后发现,它的设计思路非常直接——数据并行。一条指令,同时对多个数据做同一件事。

3.1 HVX 架构概览

HVX 不是一个独立的处理器,它是 Hexagon DSP 内部的协处理器单元。你可以把它理解成一个“加速卡”,挂在 DSP 核心旁边。

核心架构要点:

  • 向量处理单元(VPU):执行向量运算的核心,包含乘法器、加法器、移位器。
  • 向量寄存器文件(VRF):存放向量数据的“工作台”,容量大,带宽高。
  • 流式加载/存储引擎:专门负责把数据从内存搬到寄存器,或者写回去。支持各种奇奇怪怪的寻址模式。
  • 标量-向量交互:DSP 核心(标量)可以控制 HVX 的启动、停止,以及传递一些控制参数。

下面这张图,是我自己画的一个简化版 HVX 架构图,帮你建立直观印象。

HVX 架构简化框图 Hexagon DSP Core (标量控制单元) HVX VPU (向量处理单元) VRF (向量寄存器文件) L2 / DDR Stream Load/Store 引擎 控制/状态 数据通路 内存访问 流式传输

我的经验: 刚开始做 HVX 开发时,我总把 VPU 和 DSP Core 搞混。记住一句话:DSP Core 是大脑,负责决策和调度;HVX VPU 是肌肉,负责大量重复计算。 你的降噪算法里,哪个部分计算量大、数据规律性强,就往 HVX 上放。

3.2 向量寄存器(VRF)

VRF,全称 Vector Register File。它是 HVX 的“工作台”。所有要计算的数据,都得先搬到 VRF 里。

HVX 的向量寄存器宽度是 1024 位,也就是 128 字节。这个宽度是固定的,跟 ARM NEON 的 128 位、AVX-512 的 512 位都不一样。为什么是 1024 位?高通的设计哲学是:一次处理尽量多的数据,减少指令发射次数,降低功耗。

VRF 包含 32 个向量寄存器,编号从 V0 到 V31。每个寄存器都能存放 1024 位数据。你可以把它想象成 32 个“大桶”,每个桶能装 128 个字节。

属性 值 说明
寄存器宽度 1024 位 (128 字节) 固定宽度,不可配置
寄存器数量 32 (V0 - V31) 全部可用,无特殊用途寄存器
数据组织 字节、半字、字、双字 支持多种数据类型打包
对齐要求 128 字节对齐 加载/存储时,地址需对齐到 128 字节边界

注意: 我曾经在项目里踩过一个坑。加载数据时,地址没有 128 字节对齐,结果程序直接崩溃。HVX 的硬件要求很严格,不对齐的地址会触发异常。所以,分配缓冲区时,一定要用 memalign 或 posix_memalign,保证 128 字节对齐。

3.3 基本加载/存储指令

HVX 的加载/存储指令,跟 ARM NEON 的 VLD1/VST1 类似,但更灵活。它支持多种寻址模式,比如循环寻址、位反转寻址,这些在音频处理里非常有用。

最常用的指令是 vmem 和 vmemu。

  • vmem:对齐加载/存储。地址必须 128 字节对齐。性能最高。
  • vmemu:非对齐加载/存储。地址可以不对齐。性能稍差,但更灵活。

看个例子,把内存里的一段数据加载到 V0,然后存到另一段内存:

// 假设 src 和 dst 都是 128 字节对齐的
void hvx_copy_example(void *src, void *dst) {
    // 加载 128 字节到 V0
    asm volatile("vmem(%0) = V0" : : "r"(src) : "v0");
    
    // 做一些处理... 这里省略
    
    // 把 V0 存到 dst
    asm volatile("V0 = vmem(%0)" : : "r"(dst) : "v0");
}

注意看,HVX 的汇编语法跟 ARM 不太一样。它是 vmem(地址) = 寄存器 表示加载,寄存器 = vmem(地址) 表示存储。别搞反了,我刚开始就写反过,编译报错半天没找到原因。

除了单个寄存器,HVX 还支持 双寄存器加载,一次加载 256 字节:

// 加载 256 字节到 V0:V1 寄存器对
asm volatile("vmem(%0) = V0:1" : : "r"(src) : "v0", "v1");

这个特性在音频处理里特别有用。比如你要处理一个 256 点的 FFT,一次加载,一次计算,效率极高。

3.4 向量化编程入门

向量化编程,说白了就是把循环里的标量操作,改成向量操作。但这里面有很多门道。

我拿一个最简单的例子:向量加法。假设有两个数组 A 和 B,长度都是 128 字节,我们要计算 C[i] = A[i] + B[i]。

标量写法:

for (int i = 0; i < 128; i++) {
    C[i] = A[i] + B[i];
}

向量化写法(伪代码):

// 加载 A 和 B 到向量寄存器
V0 = vmem(A_addr);
V1 = vmem(B_addr);

// 向量加法
V2 = V0 + V1;

// 存结果
vmem(C_addr) = V2;

你看,128 次循环,变成了一条加法指令。这就是向量化的威力。

向量化编程的核心原则:

  • 数据并行:确保你的算法可以同时处理多个数据,没有数据依赖。
  • 内存对齐:尽量使用对齐加载/存储,避免性能损失。
  • 减少标量-向量交互:不要在循环里频繁地把向量数据拆成标量,或者把标量数据拼成向量。这会严重拖慢性能。
  • 利用双寄存器:能一次加载 256 字节,就别分两次。

但是,现实中的算法往往没这么简单。比如音频降噪里的 FIR 滤波器,它需要做卷积运算。卷积运算有数据依赖,不能简单地并行化。这时候就需要用到 HVX 的一些高级特性,比如 向量乘加(VMPY) 和 累加器(VACC)。

我举个例子,一个 8 阶 FIR 滤波器,系数是固定的,输入数据是连续的。向量化思路是:一次加载 8 个输入数据,跟 8 个系数做乘加,然后累加。HVX 的 VMPY 指令可以一次做 16 个 16 位乘法,效率非常高。

// 伪代码:FIR 滤波器的向量化实现
// 假设系数已经加载到 V0,输入数据加载到 V1
// V2 用于累加结果

// 向量乘加:V1 的每个元素乘以 V0 的对应元素,结果累加到 V2
V2 = VMPY(V1, V0);  // 实际指令更复杂,这里简化

// 继续处理下一个 8 个输入...
V1 = vmem(input_ptr + 8);
V2 += VMPY(V1, V0);

我的建议: 刚开始做向量化时,不要一上来就写汇编。先用 Hexagon SDK 提供的 HVX intrinsic(内联函数)来写。Intrinsic 是 C 函数,底层直接映射成 HVX 指令。这样既能享受向量化的性能,又不用手写汇编,调试起来也方便。等性能调优到瓶颈了,再考虑手写汇编。

最后,说一个我踩过的坑。有一次,我写了一个向量化的音频处理函数,性能死活上不去。后来发现,是 循环展开 没做好。HVX 的指令流水线很深,如果循环体太小,流水线会频繁 stall。我建议,循环体至少处理 4 个向量(512 字节)的数据,这样流水线才能跑满。

嗯,这一章的内容就到这里。HVX 的基础知识,是后续所有优化的基石。你把这些概念吃透了,后面讲 FIR 优化、FFT 优化时,才能跟得上节奏。