DRAM 与内存控制器

cache miss 以后,请求并不是掉进一个统一的“主存延迟”黑箱。DRAM 有 bank、row buffer、列访问、预充电、刷新和总线仲裁。同样是读取 16B,命中已经打开的 row、换到同一 bank 的另一行、撞上 refresh,等待和服务时间都不同。

本篇的核心问题是:怎样在公开声明的教学时序模型里区分 row hit、row conflict、refresh wait,以及请求等待时间和服务时间?核心案例使用 2 个 bank、64B row、16B block 的小型控制器。证据等级为教学时序模型;参数名参考 gem5 DRAMInterface,但数值是本文题设,不是某款 DRAM 器件。

实验目录为 examples/computer-architecture/cache/。本文附件包括 dram.json.txtcache_model.py.txtrun_cases.py.txt

地址先映射到 bank 和 row

本文映射函数很小:

1
2
3
block = address // 16
bank = block % 2
row = address // 64

block 交替落入两个 bank;每 64B 进入下一行。地址 0 的 block 为 0,bank 0,row 0;地址 32 的 block 为 2,bank 0,row 0。它们访问同一 bank 的同一 row,因此第二次可以是 row hit。地址 128 的 block 为 8,bank 0,row 2,与 row 0 冲突。

真实控制器的地址映射可能更复杂,会交织 channel、rank、bank group、bank、row 和 column。本文用最小函数,是为了让 row buffer 行为可手算,不把它写成硬件映射事实。

主存访问先由地址映射决定 bank 和 row,再由当前 open row 决定服务时间。固定写成 bank=f(addr); row=g(addr) 后,单个“内存延迟”常数会被拆成排队、激活、列访问、预充电和刷新。

三类 row 事件的服务时间

参数固定如下:t_rcd=3t_cl=2t_rp=3t_rfc=8t_refi=20

  • row empty:bank 没有打开行,需要 activate 后读列,服务时间 t_rcd + t_cl = 5
  • row hit:目标 row 已经打开,只需要列访问,服务时间 t_cl = 2
  • row conflict:bank 打开的是另一行,需要 precharge、activate、列访问,服务时间 t_rp + t_rcd + t_cl = 8

主案例请求为:

请求 地址 到达时刻 bank row 行事件 start service done wait
row-open 0 0 0 0 empty 0 5 5 0
row-hit 32 7 0 0 hit 7 2 9 0
row-conflict 128 10 0 2 conflict 10 8 18 0
after-refresh 144 21 1 2 empty 28 5 33 7

前 3 个请求分别覆盖三种 row 事件。第 4 个请求到达在 refresh 边界之后,必须等 refresh 完成,所以 wait=7refresh_wait=7

等待时间和服务时间必须分开

wait = start - arrival,表示请求到达后排队和等待全局事件的时间。service 表示开始由控制器服务后,这类 row 事件自身要用的时间。

第 4 个请求的 service 仍是 5,因为 refresh 结束后它面对的是 empty row;它的额外代价体现在 wait 和 refresh_wait 中。如果把 7 个周期直接加进 service,就会误以为这种 row 访问本身变慢了。

这种分解在性能分析里很重要。若 service 高,可能是 row conflict 多;若 wait 高,可能是 bank 排队、总线忙或 refresh;若二者都高,才需要继续拆调度策略和访问布局。

refresh 后不能沿用旧 open row

refresh 反例固定为 1 个 bank。先访问地址 0,打开 row 0;之后在 refresh 边界之后访问地址 16,它仍属于 row 0。若模型错误地保留 open row,就会把第二次访问判为 hit。

修正后的输出是:第二次访问 refresh_wait=7row_event=emptyservice=5。这表示 all-bank refresh 关闭了模型里的 open row;refresh 后即使访问同一 row,也要重新 activate。

这个处理仍是教学简化。真实 DRAM 的刷新粒度、per-bank refresh、保留策略和控制器优化都更复杂。本文只声明当前模型的假设:all-bank refresh 会使所有 open row 失效。

跨越全局维护事件时,局部 open-row 状态不能默认保留。本文模型把 all-bank refresh 写成 refresh -> close(open_rows);类似问题还包括 power-down、precharge 和调度器主动关闭行。

调度器还没有做什么

本文控制器按请求到达顺序 issue,没有重排,没有读写切换,没有 bank group 约束,也没有公平性。gem5 的 DRAMInterface 源码里能看到 tRCD、tCL、tRP、tRFC、tREFI、tRRD、tRAS 等参数名;本文只抽取最少几个参数讲 row buffer。

真实控制器常会偏好 row hit,因为它服务时间短。但如果一直偏好 row hit,其他 bank 或其他 row 的请求可能饥饿。调度策略还会考虑读写批处理、QoS、刷新 deadline 和功耗状态。本文没有模拟这些内容,因此不能从四个请求推出任何商品控制器策略。

这篇文章的目标是建立阅读口径:主存延迟由结构和调度共同产生。下一次看到“cache miss penalty = 80 cycles”,应知道那是一个折叠参数,而不是 DRAM 内部所有步骤都恒定为 80。

复跑与验收

执行:

1
bash examples/computer-architecture/cache/scripts/run_all.sh

本篇关注 outputs/dram.json。验收应检查:前三个事件分别为 empty, hit, conflict;服务时间分别为 5、2、8;最后一个请求有 refresh_wait=7;refresh 反例中 same-row-after-refresh 的 row_event=empty

这些记录证明指定教学控制器按题设运行。没有 gem5 仿真,没有真实 DRAM timing 校准,没有硬件性能计数器。

两道练习

练习一: 在本文参数下,某请求到达时 bank 已打开另一行,且不需要等待总线或 refresh。它的服务时间是多少?

解答:row conflict 需要 t_rp + t_rcd + t_cl = 3 + 3 + 2 = 8。因为题设没有额外等待,所以 wait 为 0。

练习二: 若请求 A 在 t=0 访问地址 0,请求 B 在 t=4 访问地址 32。B 是否一定 row hit?它何时开始?

解答:地址 0 和 32 在本文映射下都是 bank 0、row 0;B 的 row 类型是 hit。但 A 从 t=0 服务到 t=5,bank 和 bus 未空闲,B 到达 t=4 后要等到 t=5 开始。B 的 service 为 2,done 为 7。row hit 只说明服务短,不代表没有排队。

参考资料

  • gem5 DRAMInterface.py 固定快照。2026-09-22 核查;支持 page policy 与 tRCD、tCL、tRP、tRFC、tREFI 等参数名。
  • gem5:Classic Caches。2026-09-22 核查;用于说明 cache miss 下层路径与 MSHR 语境,不作为本文 DRAM 模型运行证据。
  • 本系列 cache 教学模型。本文实际附件来自本地工作区当前输出,不声称 GitHub 链接已经包含本轮未提交文件。