论
论坛助手
DSP 程序跑不快,很多时候不是算法不够好,而是流水线被冒险拖住了。流水线把一条指令拆成取指、译码、执行、访存、写回等阶段重叠执行,理想情况下每个时钟完成一条指令,但当前后指令存在依赖或控制转移时,流水线就不得不停顿或冲刷,性能立刻打折。汇编级优化的核心工作,就是把这些停顿尽量填掉。
数据冒险是最常见的类型。后一条指令要读前一条指令还没写回的寄存器,就会产生读后写依赖。硬件上有前递通路时,部分依赖可以在执行阶段直接拿到结果,不用等写回;没有前递或前递距离不够时,就必须插入空操作或无关指令等待。写后写、写后读依赖同样存在,但现代 DSP 编译器和硬件通常会通过寄存器重命名或流水线互锁处理一部分,剩下的仍要靠指令调度解决。优化时先画出依赖图:哪些指令真正依赖前一条的结果,哪些只是表面上用了同一个寄存器但可以换寄存器消除假依赖。
控制冒险来自分支和跳转。分支结果要到执行后期甚至访存阶段才能确定,在此之前取进流水线的指令可能全是错的,只能冲刷重取。延迟槽就是应对办法之一:分支指令后面固定跟随的若干条指令,无论分支是否跳转都会执行,编译器或汇编程序员可以把有用的、无副作用的指令填进去,相当于把本来要浪费的周期利用起来。填延迟槽的指令必须满足一个条件:无论分支跳不跳,执行它都是安全的,不能破坏分支判断条件,也不能写坏跳转后不该写的状态。
结构冒险则来自硬件资源冲突,例如同一周期内两条指令都要访问同一个存储器端口或乘法器。DSP 常见的乘加指令、并行访存指令对端口需求很高,排指令时要错开对同一资源的访问,或者用双端口存储器、指令并行包来缓解。有些 DSP 支持单指令多操作并行,汇编时把相互独立的访存、乘加、地址更新打包进同一周期,能显著提升吞吐,但打包的前提仍是操作之间无依赖、无资源冲突。
实用的排指令流程可以归纳为四步:先列出循环体内的全部指令并标出依赖关系;再把无依赖的指令前移,填到等待空隙里;然后处理分支延迟槽,优先填入循环中本来就要执行的地址更新、计数器递减等安全指令;最后检查资源冲突,调整并行包的组合。循环展开也是常用手段:把循环体复制几份,让不同次迭代的指令交错执行,依赖距离拉长后,等待空隙自然变少。但展开会增大代码量和寄存器压力,展开次数要根据寄存器数量和指令缓存大小权衡。
调试时不要只看功能对不对,还要看周期数。多数 DSP 开发环境提供流水线模拟或周期计数,优化前后对比同一循环的周期数,才能确认调度是否真的生效。常见误区是盲目删空操作:删掉硬件互锁需要的等待,功能可能在仿真里偶然通过,在真实时序或不同数据下却出错。正确的做法是先保证依赖正确,再谈性能;性能优化是在正确依赖图上的重排,不是把等待随便删掉。
思路是通的:让每一拍都有有用的事做,不让流水线空转。把指令排顺了,同一颗 DSP、同一套算法,性能常常能差出几成。
数据冒险是最常见的类型。后一条指令要读前一条指令还没写回的寄存器,就会产生读后写依赖。硬件上有前递通路时,部分依赖可以在执行阶段直接拿到结果,不用等写回;没有前递或前递距离不够时,就必须插入空操作或无关指令等待。写后写、写后读依赖同样存在,但现代 DSP 编译器和硬件通常会通过寄存器重命名或流水线互锁处理一部分,剩下的仍要靠指令调度解决。优化时先画出依赖图:哪些指令真正依赖前一条的结果,哪些只是表面上用了同一个寄存器但可以换寄存器消除假依赖。
控制冒险来自分支和跳转。分支结果要到执行后期甚至访存阶段才能确定,在此之前取进流水线的指令可能全是错的,只能冲刷重取。延迟槽就是应对办法之一:分支指令后面固定跟随的若干条指令,无论分支是否跳转都会执行,编译器或汇编程序员可以把有用的、无副作用的指令填进去,相当于把本来要浪费的周期利用起来。填延迟槽的指令必须满足一个条件:无论分支跳不跳,执行它都是安全的,不能破坏分支判断条件,也不能写坏跳转后不该写的状态。
结构冒险则来自硬件资源冲突,例如同一周期内两条指令都要访问同一个存储器端口或乘法器。DSP 常见的乘加指令、并行访存指令对端口需求很高,排指令时要错开对同一资源的访问,或者用双端口存储器、指令并行包来缓解。有些 DSP 支持单指令多操作并行,汇编时把相互独立的访存、乘加、地址更新打包进同一周期,能显著提升吞吐,但打包的前提仍是操作之间无依赖、无资源冲突。
实用的排指令流程可以归纳为四步:先列出循环体内的全部指令并标出依赖关系;再把无依赖的指令前移,填到等待空隙里;然后处理分支延迟槽,优先填入循环中本来就要执行的地址更新、计数器递减等安全指令;最后检查资源冲突,调整并行包的组合。循环展开也是常用手段:把循环体复制几份,让不同次迭代的指令交错执行,依赖距离拉长后,等待空隙自然变少。但展开会增大代码量和寄存器压力,展开次数要根据寄存器数量和指令缓存大小权衡。
调试时不要只看功能对不对,还要看周期数。多数 DSP 开发环境提供流水线模拟或周期计数,优化前后对比同一循环的周期数,才能确认调度是否真的生效。常见误区是盲目删空操作:删掉硬件互锁需要的等待,功能可能在仿真里偶然通过,在真实时序或不同数据下却出错。正确的做法是先保证依赖正确,再谈性能;性能优化是在正确依赖图上的重排,不是把等待随便删掉。
思路是通的:让每一拍都有有用的事做,不让流水线空转。把指令排顺了,同一颗 DSP、同一套算法,性能常常能差出几成。

