内存控制器设计:请求调度策略
各位同学,今天我们来聊聊内存控制器里最核心的一块——请求调度策略。说白了,就是当一堆读写请求涌向DRAM时,控制器该怎么排队、怎么插队、怎么避免有人饿死。
我在做DDR4控制器项目时,就遇到过这么个情况:系统跑着跑着,某个低优先级的核死活拿不到数据,延迟飙到天上去了。后来一查,就是调度策略没处理好。嗯,这节课咱们就把这块掰开揉碎了讲。
FR-FCFS调度:先就绪,再老幼
FR-FCFS,全称是First-Ready First-Come First-Serve。名字挺长,但逻辑很简单——先服务那些已经准备好(行命中)的请求,再按到达顺序服务其他请求。
你想想看,DRAM访问最耗时的操作是什么?是行激活(tRCD)和预充电(tRP)。如果请求恰好命中当前打开的行,那就能省掉这两步,延迟直接砍掉一大截。
我在一个视频处理芯片的项目里,用FR-FCFS把内存带宽利用率从65%提到了82%。为什么?因为视频流的数据访问模式很规律,大量请求落在同一行上,FR-FCFS正好吃准了这个特点。
但FR-FCFS有个隐患——饥饿问题。如果一直有行命中的请求插队,那些行不命中的请求可能永远排不上队。我见过一个极端案例:某个核的请求延迟从正常的50ns飙到了2μs,就是因为被连续的行命中请求活活饿死了。
饥饿避免:给老请求一个插队权
怎么解决饥饿?说白了就是加个老化机制。每个请求在队列里待的时间越长,优先级就越高。当某个请求的等待时间超过阈值,它就能强行插队。
我常用的做法是两阶段优先级:
- 第一阶段: 按FR-FCFS正常调度
- 第二阶段: 如果某个请求等待超过N个周期(比如64个周期),直接提升到最高优先级
代码实现起来其实不复杂,我贴一段伪代码给大家看看:
// 请求调度核心逻辑
if (存在老化请求) {
// 老化请求优先
选择等待时间最长的老化请求;
} else {
// 正常FR-FCFS
if (存在行命中请求) {
选择最早到达的行命中请求;
} else {
选择最早到达的请求;
}
}
曾经有个项目,我把阈值设成了32,结果发现行命中率从80%掉到了55%,得不偿失。后来调到96,才找到平衡点。
Bank/Rank级并行度:别让DRAM闲着
DRAM内部有多个Bank,每个Bank可以独立工作。如果你只盯着一个Bank发请求,其他Bank全闲着,那带宽利用率肯定上不去。
我画了一张图,帮大家理解Bank级并行度的概念:
看到没?Bank 0在激活行,Bank 1在读数据,Bank 2在预充电,Bank 3闲着。它们可以同时干活,但共享的数据总线一次只能传一个Bank的数据。调度器的任务就是让这些操作流水线起来,别让总线闲着。
Rank级并行度也是类似的道理。一个DIMM上可能有多个Rank(比如双面内存就是两个Rank),每个Rank有独立的片选信号。你可以让Rank A在预充电,同时Rank B在读数据,这样带宽利用率又能往上提一截。
实战中的调度策略选择
不同的应用场景,调度策略的侧重点完全不同。我整理了一个表格,方便大家对照:
| 应用场景 | 推荐策略 | 关键参数 | 我的经验 |
|---|---|---|---|
| 视频编解码 | FR-FCFS + 弱老化 | 老化阈值128周期 | 行命中率可达85%以上 |
| 数据库/OLTP | 公平轮转 + 强老化 | 老化阈值64周期 | 避免长尾延迟 |
| AI推理 | Bank级并行优先 | Bank轮转粒度4请求 | 带宽利用率提升20% |
| 实时系统 | 优先级固定 + 老化 | 高优先级无老化 | 确保实时任务延迟可控 |
我个人习惯在项目初期先用FR-FCFS + 中等老化(96周期)作为基线,然后根据实际负载特征微调。你想想看,如果一开始就搞复杂的调度算法,出了问题你都不知道是调度的问题还是别的问题。
最后说一句,调度策略没有银弹。你在仿真里跑得再漂亮,上了芯片可能又是另一回事。我的建议是:把调度参数做成可配置的,留几个寄存器给软件调。这样芯片回来后,还能根据实际负载做优化。
公众号:蓝海资料掘金营,微信deep3321