1. 延迟从哪里来:先把“实时”拆成可计算的参数
1.1 算法耗时只是其中一项,链路每一环都要算清楚
刚开始写这个实时信号处理库的时候,我也有过一个很天真的想法:只要每个算法函数都足够快,整个库的实时性自然就有保障。后来在真实项目里反复翻车,才意识到问题完全不是这样。实时性的核心是“确定性”,而不是单纯的“快”。一个算法平均耗时1毫秒,但偶尔会跳到30毫秒,这在非实时系统里无伤大雅,在实时链路里就是事故。
需要算清楚的环节有很多。以最常见的采集-处理-输出链路为例:ADC采样结果要先通过DMA搬进内存,这一步有传输时间;之后处理线程被唤醒,这有调度延迟;处理算法本身消耗时间;处理完还要把结果写回到DAC或共享内存,这又有访存和缓存开销。在嵌入式平台上,中断响应和上下文切换的时间也要计入预算。如果库的接口设计得不好,比如用户每次调用都要做一次动态分配,那内存分配器的加锁和系统调用就可能占掉一大半时间片。
做一个实时信号处理库,第一步不是去优化FFT,而是把上面这些环节一个一个列出来,明确哪些是库能控制的、哪些是使用方要承担的。能控制的部分,比如块拷贝、滤波器计算、FFT状态更新,要尽量做到耗时稳定;不能控制的部分,比如外部DMA带宽、操作系统的调度策略,至少要在文档里给出测量方法和建议值,不然用户很难判断瓶颈到底出在哪里。我自己在维护库的过程中,发现用户反馈里有一大半问题都不是算法本身算得慢,而是链路某个环节的数据搬运、等待或者锁竞争把时间吃掉了。
1.2 不同采样率和块大小下,时间预算到底有多紧张
具体数值更容易说明问题。比如音频里常见的48kHz采样率,如果用512个样本作为一个处理块,那么每个块的时间预算就是512 / 48000,约10.67毫秒。这个时间对普通PC的CPU来说很宽裕,要在10毫秒内完成一个512点FFT、一个32阶FIR滤波完全做得到,余量很大。但如果把采样率降到8kHz,块大小是64个样本,那每个块的预算只有8毫秒;虽然计算量变小了,但系统调度、函数调用、线程切换的固定开销所占比例会变得非常大。这就是为什么低采样率的实时系统,处理难度并不一定比高采样率低。
反过来,工业振动监测或者电机故障诊断这类场景,采样率可能到100kHz以上,单块1024个样本对应的预算只有约10.24毫秒,而你需要在这个窗口内完成多通道的FFT、包络分析和特征提取,计算密度比音频场景高很多。所以库在设计接口时,不能替用户假设“一帧到底是多少点”。比较好的做法是支持帧大小可配置,并且在初始化阶段就让用户明确设置帧大小和预期的处理超时阈值。一些做得比较细致的库还会根据这些参数自动给出一个“推荐处理预算”,辅助用户判断给定的硬件能不能跑得动。
这里还有一类很容易被忽略的可变开销:循环的次数取决于数据内容。比如某项特征提取里如果包含一个“检测到峰值后触发计算”的逻辑,那么不同块的耗时可能相差好几倍,这就会造成抖动。我在库的内部处理里一直尽量保持每个处理周期的工作量恒定,或者至少把最大工作量控制在预算以内,即使这意味着多算一些无用数据,也要保证时间上的可预测性。对一个面向不确定场景的实时信号处理库来说,宁可慢一点,也不要时快时慢。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 数据怎么流:环形缓冲、块大小与内存策略
2.1 为什么样本级回调最终被块级处理取代
实时信号处理库面对的输入信号基本是连续不断的流,但处理环境通常不是每个样本唤醒一次,而是攒到一定数量后批量处理。最开始的库版本里,我提供的是一个process_sample接口,让用户在每个新样本到来时调用一次。从接口的简洁角度看,这种设计很吸引人,特别是做嵌入式开发的人会觉得这样最直观;但它有两个严重的性能问题。
第一,函数调用次数爆炸。48kHz采样率下,每秒就是48000次函数调用,如果链路里有滤波、频谱、特征提取三个模块,每个模块都按样本处理,每秒的调用次数会翻几倍。调用本身虽然不贵,但加上状态检查和分支判断,固定开销就被放大了。
第二,几乎所有高效算法都依赖数据块。FFT天然需要整块数据,FIR滤波器虽然是逐点计算,但向量化需要连续读取数据,样本级接口会破坏这种连续性。哪怕是简单的均值滤波,逐样本实现也会因为分支判断产生额外开销。后来我把接口改成block-based:每次处理一个指定大小的缓冲区,模块内部维护跨块的状态。调用方可以把ADC的DMA缓冲区直接传进来,模块读完以后把结果写到另外一个预分配缓冲区。这样函数调用次数从每秒几万次降到每秒几百次,算法实现也可以直接利用向量化。块级处理还有一个隐藏的好处:处理线程可以在两个块之间做一次调度让步,系统的整体响应会变得更平滑。
2.2 无锁环形缓冲:实现边界与真正的适用场景
数据流动要靠队列。采集线程把数据包放入队列,处理线程从队列取出。队列如果加锁,就会把实时性毁掉一半,因为锁
