4. 乘法累加器(MAC)优化:乘加指令流水线、多级累加防止溢出、双MAC并行计算

各位同学,咱们今天聊一个DSP里最核心、也最实在的话题——MAC优化。

MAC,就是乘法累加器。说白了,就是先乘一下,再加一下。这个操作在数字信号处理里太常见了,FIR滤波器、FFT、矩阵乘法,哪个都离不开它。我做了十几年嵌入式DSP开发,可以说,MAC优化的好坏,直接决定了你的算法能不能跑得动、跑得快。

嗯,咱们今天不讲虚的,直接上干货。

4.1 乘加指令流水线:别让CPU等你

先说说流水线。现代DSP处理器,比如TI的C6000系列、ADI的SHARC系列,它们的MAC指令都是流水线执行的。什么意思呢?就是一条MAC指令不是一口气做完的,而是分成好几步:取指令、译码、取操作数、执行、写回。每一步由不同的硬件单元并行处理。

我刚开始做DSP优化时,犯过一个低级错误。我写了个循环:

for(i = 0; i < N; i++) {
    sum += a[i] * b[i];
}

看起来没问题对吧?但实际跑起来,效率低得吓人。为什么?因为每次循环都要等上一次的乘法结果写回累加器,才能开始下一次。流水线被堵死了。

我个人习惯的做法是:手动展开循环。比如这样:

for(i = 0; i < N; i += 4) {
    sum0 += a[i]   * b[i];
    sum1 += a[i+1] * b[i+1];
    sum2 += a[i+2] * b[i+2];
    sum3 += a[i+3] * b[i+3];
}
sum = sum0 + sum1 + sum2 + sum3;

你看,这样就把4次MAC操作“摊开”了。流水线可以同时处理多条指令的不同阶段,互不等待。性能提升非常明显,我实测过,有些场景能快3倍以上。

小技巧:展开因子不是越大越好。一般4~8路展开就够了,再大反而会因为寄存器不够用导致性能下降。具体多少,得看你的DSP有多少个通用寄存器。

4.2 多级累加防止溢出:别让数据“爆”了

接下来聊一个坑——溢出。

MAC操作,尤其是定点DSP,累加器是有位宽限制的。比如32位的累加器,你连续加几十个32位的乘积,很容易就溢出了。溢出之后,数据就全乱了。

我曾经在一个音频处理项目里遇到过这个问题。当时做的是自适应滤波器,系数更新时累加值越来越大,结果输出全是噪声。排查了整整两天,才发现是累加器溢出了。

怎么解决?多级累加

说白了,就是不要把所有的乘积都往一个累加器里塞。你可以分多个累加器,每个累加器只处理一部分数据,最后再汇总。比如:

int32 acc0 = 0, acc1 = 0, acc2 = 0, acc3 = 0;
for(i = 0; i < N; i += 4) {
    acc0 += a[i]   * b[i];
    acc1 += a[i+1] * b[i+1];
    acc2 += a[i+2] * b[i+2];
    acc3 += a[i+3] * b[i+3];
}
int64 final_sum = (int64)acc0 + acc1 + acc2 + acc3;

注意最后我用的是64位累加。这样每个32位累加器都不会溢出,最后汇总时用更宽的位宽来保证精度。

避坑指南:我曾经见过有人把多个累加器的结果直接相加,结果又溢出了。记住,汇总时一定要用更宽的位宽,或者做饱和处理。别省这一步。

另外,还有一种更高级的做法——块浮点。就是把一组数据统一缩放到一个合适的指数,然后做定点运算。这样既能防止溢出,又能保持精度。不过这个比较复杂,咱们后面章节再细讲。

4.3 双MAC并行计算:榨干DSP的每一滴性能

最后说说双MAC。很多高端DSP,比如TI的C674x、C66x系列,内部有两个MAC单元。这意味着一个时钟周期内,你可以执行两条MAC指令。

但前提是——你的代码要能“喂饱”这两个单元。

怎么做到?数据依赖要解除。你不能让第二条MAC指令等第一条的结果。所以,还是得靠循环展开和独立累加器。

我举个例子,用TI的C6000编译器intrinsic写法:

// 双MAC并行示例(伪代码)
for(i = 0; i < N; i += 2) {
    // 这两条MAC可以并行执行
    sum0 = _mpy(a[i],   b[i])   + sum0;
    sum1 = _mpy(a[i+1], b[i+1]) + sum1;
}

你看,sum0和sum1是独立的,没有数据依赖。编译器或者硬件调度器就可以把它们分配到两个MAC单元上并行执行。理论上,性能翻倍。

但实际中,你还要考虑内存带宽。如果数据从内存里读不过来,MAC单元就得干等着。所以,我建议配合DMA或者Cache预取来使用。把数据提前搬到离CPU最近的地方。

核心要点:双MAC并行计算的关键在于——
1. 消除数据依赖(用独立累加器)
2. 保证数据供给(用DMA或Cache优化)
3. 合理展开循环(4~8路为佳)

4.4 本章知识体系

为了让你更直观地理解,我画了一张图。这张图展示了MAC优化的三个层次:从流水线利用,到溢出防护,再到并行计算。每一步都是递进的。

MAC优化三层架构 第一层:乘加指令流水线 循环展开 → 消除数据依赖 → 流水线满负荷 第二层:多级累加防止溢出 分多个累加器 → 独立累加 → 最后用宽位汇总 第三层:双MAC并行计算 独立累加器 + 数据预取 → 双MAC单元同时工作 从底层到顶层,逐层优化,榨干DSP性能

嗯,这张图你看懂了吗?三层是递进关系。先搞定流水线,再防止溢出,最后上并行。别跳步,跳步容易出问题。

4.5 实战建议

最后,给你几个我自己的实战建议:

  • 先测后优化:别一上来就搞展开、搞并行。先用profiler看看瓶颈在哪。我见过有人花了一周优化MAC,结果瓶颈在内存访问上,白忙活。
  • 用好编译器选项:很多DSP编译器有自动展开和自动并行化的选项。比如TI的--opt_level=3配合--auto_parallel。先试试编译器能帮你做多少,再手动优化剩下的。
  • 注意数据对齐:双MAC并行时,数据最好8字节对齐。不对齐的话,内存访问会慢很多。我习惯用#pragma DATA_ALIGN来强制对齐。
  • 保留中间结果:调试时,把每个累加器的中间值打印出来。我曾经靠这个发现了一个隐蔽的溢出bug。
我的一个小习惯:每次写完MAC相关的代码,我都会用随机数据跑一遍,跟浮点版本对比。误差在1%以内才算通过。别信“定点跟浮点一样”这种话,差一个bit都可能出问题。

好了,MAC优化就聊到这儿。内容不多,但都是实打实的经验。你回去可以拿一个FIR滤波器试试,按照我说的三步走,看看性能能提升多少。

记住,DSP优化的本质,就是让硬件别闲着。流水线别等、累加器别爆、MAC单元别空。做到这三点,你的代码就离“高效”不远了。


公众号:蓝海资料掘金营,微信deep3321