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.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最近的地方。
1. 消除数据依赖(用独立累加器)
2. 保证数据供给(用DMA或Cache优化)
3. 合理展开循环(4~8路为佳)
4.4 本章知识体系
为了让你更直观地理解,我画了一张图。这张图展示了MAC优化的三个层次:从流水线利用,到溢出防护,再到并行计算。每一步都是递进的。
嗯,这张图你看懂了吗?三层是递进关系。先搞定流水线,再防止溢出,最后上并行。别跳步,跳步容易出问题。
4.5 实战建议
最后,给你几个我自己的实战建议:
- 先测后优化:别一上来就搞展开、搞并行。先用profiler看看瓶颈在哪。我见过有人花了一周优化MAC,结果瓶颈在内存访问上,白忙活。
- 用好编译器选项:很多DSP编译器有自动展开和自动并行化的选项。比如TI的
--opt_level=3配合--auto_parallel。先试试编译器能帮你做多少,再手动优化剩下的。 - 注意数据对齐:双MAC并行时,数据最好8字节对齐。不对齐的话,内存访问会慢很多。我习惯用
#pragma DATA_ALIGN来强制对齐。 - 保留中间结果:调试时,把每个累加器的中间值打印出来。我曾经靠这个发现了一个隐蔽的溢出bug。
好了,MAC优化就聊到这儿。内容不多,但都是实打实的经验。你回去可以拿一个FIR滤波器试试,按照我说的三步走,看看性能能提升多少。
记住,DSP优化的本质,就是让硬件别闲着。流水线别等、累加器别爆、MAC单元别空。做到这三点,你的代码就离“高效”不远了。
公众号:蓝海资料掘金营,微信deep3321