
《Memory Systems: Cache, DRAM, Disk》这本书很多人是冲着Cache章节去读的翻到DRAM部分就开始犯困。我一开始也这样直到被一个具体问题卡住DDR4颗粒明明标称64ms刷新间隔为什么服务器在高负载下跑着内存响应却没有肉眼可见的顿挫为了把这个事搞清楚我把DRAM部分从头到尾啃了几遍这篇是第二篇学习笔记专门聊聊DRAM这一大章。先交代点背景Bruce Jacob那本原书是2007年前后出版的技术底座还停留在DDR2/DDR3那一代但这完全不影响它成为理解现代内存系统的最佳起点。因为从DDR到DDR5整个访问协议的骨架几乎没有变变的只是频率、电压和预取宽度。拿这本书打底再去看DDR4/DDR5的手册你会发现脑子里已经有一张完整的地图。这篇笔记适合两类人。一类是做系统底层开发、日常跟性能调优打交道的人另一类是正在学计算机组成原理、觉得教材里DRAM部分太简略的学生党。我会尽量把原书里比较绕的物理模型、时序逻辑用大白话解释清楚再补上一些实操命令和现代DRAM的扩展知识读完你至少能看懂内存条参数那串数字到底在说什么。1. 为什么原书要把DRAM单独写成一个大章节1.1 三种存储器的本质是同一道题读这本书有个特别有意思的视角Cache、DRAM、Disk这三样东西表面上是完全不同的器件其实是同一道题的三次作答——如何在成本、速度、功耗之间拿到一个可用的折中点。SRAM最快但一个比特要4到6个晶体管面积大、成本高只适合做CPU内部的小容量缓存DRAM用一个晶体管加一个电容就能存一个比特密度比SRAM高一个数量级代价是它需要不断刷新才能保住数据Disk或者说现代的SSD进一步把成本压到了物理极限代价是访问延迟比DRAM高两到三个数量级。把三者连起来看就是一个典型的存储层次结构。CPU寄存器大约0.3nsL1/L2 Cache大约1到10nsDRAM主存大约70到100nsSSD几十微秒HDD五到十毫秒。这些数字一摆出来你就能明白为什么系统设计者愿意花大量精力去提高Cache命中率——一次DRAM访问的代价差不多是L1 Cache命中的一百倍。原书的核心贡献之一就是把这三层放在同一个框架下讲透。Cache部分讲的是如何利用局部性、用硬件自动维护一个小而快的副本DRAM部分讲的则是如何用最低的成本实现大容量随机存储Disk部分讲的是如何用机械或电荷陷阱来换更低的每比特成本。同一个问题的三种解法读起来互相印证比单独学任何一层都透彻。1.2 DRAM部分为什么劝退原书把DRAM的篇幅写得特别足因为DRAM是少有的从电容漏电一路干到操作系统调度的器件。一个比特的存储要谈物理电荷一个阵列的访问要谈行缓冲和bank冲突往上还有内存控制器、页面策略、NUMA调度。跨层因素太多所以读起来特别容易迷路。我自己读下来的感受是DRAM部分最劝退的不是公式而是符号和术语的数量。tRCD、tRP、tRAS、tRFC、行激活、预充电、bank group、rank、刷新……这些东西如果一个个孤立地记很容易记混。最好的方式是把它们串进一个完整的访问故事里——从CPU发起一次读请求到数据回到CPU中间到底发生了什么。这个过程就是接下来的重点。原书这一章还特别强调了成本驱动设计的思路DRAM的所有怪癖无论是刷新、行列复用、还是bank划分根源都是在保证容量的前提下尽量压低单位比特成本。记住这个主线DRAM的每个设计决策就都说得通了。2. DRAM的物理底层一个晶体管加一个电容2.1 一比特的存储模型DRAM的存储单元在教科书里通常画成一个晶体管串一个电容。电容用来存电荷有电荷表示逻辑1没有电荷表示逻辑0晶体管是访问开关由字线控制在读写时把电容接到位线上。这个结构简单到了极致但它带来了三个连锁后果。第一个后果是漏电。电容不是理想的电荷会通过介质慢慢漏掉所以DRAM必须定期刷新——先把每个单元读一遍再写回去。DDR4标准的典型刷新间隔是64ms高温下会缩短到32ms。这个数字不是拍脑袋定的而是根据电容电荷衰减特性和误码率要求反推出来的在典型结温和电压下64ms内电荷衰减导致的误读概率可以被接受。第二个后果是破坏性读出。当你把电容接到位线上时电荷会在两者之间重新分配。位线的寄生电容比存储电容大不少电荷一分享存储单元的电压就被拉塌了。也就是说DRAM的读操作本质上是读出即破坏读完之后必须立刻把数据写回那个单元。每一次DRAM读都伴随放大-锁存-写回的连锁动作这也是为什么读操作比想象中耗时。第三个后果是行列网格。为了让面积利用率和成本可控DRAM把单元排成紧密的二维阵列同一行的单元共享一条字线同一列的单元共享一条位线。这个网格结构直接引出了先打开行再从行里取列的两步寻址——后面要讲的激活、预充电全是在这个网格上做文章。2.2 感测放大器藏在每个bit背后的功臣说到读操作原书专门花了篇幅讲感测放大器sense amplifier。它可能是DRAM里最容易被忽视、但作用最关键的电路。存储电容上的电荷量实在太小直接判断逻辑0还是逻辑1非常不靠谱必须靠感测放大器来做信号放大。感测放大器的基本原理类似跷跷板先把位线预充电到Vdd/2然后把存储电容接上去哪怕电容只让位线电压产生了极微小的偏移感测放大器也会通过正反馈把这个偏移放大到满摆幅从而判别出0和1。这个过程要花多少时间直接决定了tRCD这个参数。我当年读到这一段时才反应过来为什么内存参数表里tRCD通常比CL还大——因为感测放大器的建立时间、位线的电荷分享时间这些都是实打实的物理过程想快都快不了。2.3 从物理模型看刷新为什么不能无限推迟关于刷新原书有一个重要的细节刷新是每个bank独立进行的一次刷新命令会同时触发某个bank所有行的一遍遍历。刷新命令要占用命令总线如果落在内存控制器最忙的时候确实会造成访问排队但现代控制器会做刷新调度尽量把刷新命令错峰插入。我自己在实际中碰到过一个和刷新有关的坑。某台服务器在夏天出现间歇性延迟尖峰排查了CPU、网络、磁盘都没找到原因最后发现是机房空调故障导致内存温度升高温度高了刷新间隔自动缩短刷新开销变大才引发性能抖动。所以内存散热在某些场景下不是玄学它直接影响刷新的频率和实际可用带宽。顺带说一个搜索引擎里很常见的词DRAM和DDR、PSRAM的区别。DDR是DRAM的一种接口标准DRAM是存储介质类型而PSRAM伪静态随机存储器是用DRAM的存储单元外面套一个SRAM接口由内部逻辑自己处理刷新使用体验和SRAM差不多、成本更低。它不算SRAM只是长得像。理解了这个再去看芯片手册上的pseudo SRAM就不会被绕晕。3. Bank架构与一次读请求的完整旅程3.1 为什么要分Bank如果整个芯片只有一个存储阵列那每一次访问都要经历关掉所有行、打开一行、再关掉、再打开的过程效率太低。于是DRAM把存储阵列切成了多个独立的bank每个bank都有自己的行缓冲和读出放大器组。这样设计有三个直接收益一个bank在处理写回时另一个bank可以同时执行激活实现时间上的流水不同bank的访问可以交叠内存控制器通过调度把多个内存请求之间的空洞填满同一时刻对同一个bank的访问自然排队跨bank的访问几乎可以并行。用大白话说bank就像多个独立的小仓库每个仓库有一个专属的卸货台也就是行缓冲。卸货台一旦摆上了某一行的货再有人来取这一行的其他货物就能直接从卸货台拿不用再进仓库翻货。这就是后面要讲的row hit。DDR4时代又在bank之上引入了bank group的概念把几个bank组成一组每组有独立的读出通道进一步提高了并行度。多核CPU同时访问内存时不同核心的请求如果能落到不同的bank group冲突就大大减少。3.2 激活、列读、预充电一次访问的三步舞一个完整的DRAM读请求大致是这样的预充电Precharge把当前bank的位线电压预充到感测放大器最敏感的工作点同时关闭当前打开的行。这一步相当于把卸货台清空、把货架复位。激活Activate选中目标行字线拉高整行单元被接到读出放大器上放大后的数据锁存到行缓冲。这一步把行加载到了卸货台。列读Column Read / CAS在行缓冲里找到目标列把数据送到芯片引脚上。如果连续读多个地址且它们恰好落在同一行只需要一次激活后续所有命令都命中行缓冲这就是row hit。反过来如果要访问的行和当前行缓冲不是同一行就得先预充电再激活这就是row miss。原书强调row hit和row miss的访问延迟差距可以达到好几倍所以内存控制器调度器的主要工作之一就是尽量把访问相同bank相同行的请求聚合到一起。3.3 地址引脚复用为什么一个地址要分两次传DRAM的地址引脚远比CPU地址总线少行地址和列地址共用同一组引脚。原因还是成本——引脚数量直接决定封装面积和最终售价。为了省引脚访问一个内存地址时控制器先发一个RAS信号配合行地址再发一个CAS信号配合列地址。DRAM芯片内部把这两个半地址拼成一个完整地址定位到具体的存储单元。听起来绕但这是DRAM保持成本优势的关键设计之一。它带来的副产品就是tRCD这个时序参数——从行地址有效到列地址可以发出需要等待读出放大器完成稳定放大。理解了引脚复用你就自然理解了时序参数为什么要存在。原书还提到一个细节虽然地址引脚复用但数据引脚不复用。DDR本质上是在同一个时钟周期内在上升沿和下降沿各传一次数据这就是DDRDouble Data Rate名字的来源。它和地址复用是两个不同维度的设计一个为省引脚一个为提带宽。4. 时序参数DRAM性能的灵魂4.1 每个参数都是物理延迟不是随便定的数字DDR4-3200 CL16的内存条用工具查看时大概率会看到16-22-22-36这么一串数字。这一串其实对应四个最常见的参数参数含义DDR4-3200 CL16换算背后物理过程CL16列读命令到数据输出16×0.625ns≈10ns锁存地址、读出放大、输出驱动tRCD22激活到列读的最短间隔22×0.625ns≈13.75ns位线电荷分享、感测放大建立tRP22预充电到激活的最短间隔22×0.625ns≈13.75ns位线复位到Vdd/2、驱动关闭tRAS36行激活到预充电的最短时间36×0.625ns≈22.5ns行数据完整导通的保持时间这里有个基础换算DDR4-3200的数据速率是3200MT/s对应的总线时钟频率是1600MHz一个tCK是0.625ns。CL16换算过来大约10ns纯粹是从命令到数据的信号路径时间。这几个参数背后全是器件物理。CL低说明读出路径和输出路径延迟小tRCD低说明感测放大器建立快tRP低说明位线预充效率高。超频玩家喜欢挑CL更低的内存因为CL直接降低了纯延迟而提升频率只是提高带宽延迟的改善非常有限。4.2 频率和延迟的反直觉关系这是原书里值得玩味的一页DDR4频率从2133升到3200带宽涨了50%但绝对延迟几乎没怎么降有时候甚至更高。原因很简单tRCD、tRP这类物理时间以纳秒计是固定的频率提高后每个周期更短换算成周期数就不得不变大。所以你去看DDR3-1333 CL9的条子真实延迟和DDR4-3200 CL16差不了太多。这就给系统设计提了个醒内存带宽和延迟是两回事。大量顺序读、大块数据搬运时带宽重要随机小对象访问、指针追逐时延迟更重要。别再一听高频内存就觉得延迟一定低先动手算一下真实ns延迟很多优化思路会因此改变。另外提一句XMP/EXPO这类一键超频本质是加载颗粒SPD里保存的更激进时序。厂商在特定平台测试过这套时序但换到不同主板、不同温度环境不保证一定稳定。所以开XMP后如果出现蓝屏或不稳定不用太惊讶手动放宽一档时序往往就能解决。4.3 内存控制器的训练到底在训什么每台服务器开机BIOS都要花一两秒做内存训练。这个过程本质上是控制器对每根DIMM做一遍访问测试动态确定适用于该颗粒的最优时序参数。因为不同颗粒的电气特性存在差异实际可用的时序往往比SPD里标称的略宽松或略严格所以开机阶段需要通过训练把每个参数的边界探出来。做系统运维的老手应该都见过这个现象换了一条不同品牌的内存开机变慢了甚至POST不过。这不一定是兼容性问题更可能是内存控制器没能在第一轮训练里找到稳定参数。碰到这种情况先清一次CMOS让BIOS重新做完整训练常常就好使了。原书虽然没有写DDR4时代的具体训练流程但这个概念从DDR2起就一直存在。训练的对象除了CL这些时序还包括电压校准、均衡参数等。越是高速的DDR训练越复杂这也是DDR5对主板布线要求更高的原因之一。5. 一层一层往外看DRAM如何和Cache、Disk协同5.1 两个行的微妙对应读到这里再回头看Cache那部分你会发现DRAM的行缓冲其实也是一个小型的行级缓存。Cache line是64字节DRAM行缓冲动辄8KB级别两者尺寸差了两个数量级。这带来一个实际影响当CPU发生Cache miss并发的多个处理器核经常凑巧要读同一内存页或相邻区域内存控制器对row hit的调度可以直接影响多核程序的实际性能。多核场景下不同核同时访问内存bank数量有限请求冲突难以避免。DDR4里bank group的设计就是为了缓解这种冲突——把一个大的bank拆成几个bank group每组有独立的读出通道多个核的请求可以分到不同的组里并行处理。这也是为什么内存从DDR3到DDR4bank数量不增反减但性能却提升的关键原因。原书讲DRAM时反复强调内存控制器是系统性能的隐形调度员。它的调度策略直接决定了row hit率、bank级并行度、以及最终的有效带宽。很多人在做性能优化时只盯着算法复杂度忽略了内存访问模式对DRAM内部状态的影响其实调整一下数据结构布局、让同一时间段的访问尽量落在同一bank或同一行往往能带来不小的提升。5.2 三种存储介质的延迟量级与代价原书最让我印象深刻的是把三者放在同一个坐标轴上看。从CPU发起一次取值存储层次典型延迟相对量级L1 Cache1ns左右1xL2 Cache5-10ns约10xDRAM70-100ns约100xSSD20-100us约10000xHDD5-10ms约1000000x这个量级差异解释了为什么操作系统要拼命做页面缓存为什么数据库要预读、要写缓冲为什么Redis这类KV缓存有存在价值——本质上都是在拉近某层存储与CPU的距离。大模型推理里的KV Cache也是同理本质是用DRAM容量换请求延迟。明白了这个坐标轴你再看任何一层缓存的设计都能立刻看出它的利与弊它到底想替下一层挡掉哪部分延迟付出多少容量和一致性的代价。5.3 Linux下实测DRAM信息的两个命令讲完理论落一点实操。第一个是dmidecodesudo dmidecode -t memory这个能看物理插槽、容量、类型、标称频率但很多情况下时序参数不显示全看不到颗粒的具体时序。它适合先确认机器到底插的是什么内存摸摸底。第二个是decode-dimms需要先装i2c工具sudo apt install i2c-tools sudo modprobe i2c-i801 sudo decode-dimms它读的是DIMM上SPD芯片里的完整参数包括CL、tRCD、tRP、tRFC、制造商、颗粒编号、支持的电压阶梯。折腾内存参数、评估二手内存能不能用在特定场景时这个输出很顶用。另外一个土办法开机时进BIOS或查看自检日志多数服务器主板会打印内存训练完成后的最终时序比SPD标称值更接近真实运行状态。在Linux下sudo dmesg | grep -i memory多少也能抓到一些内存控制器上报的训练信息不过不同厂商输出的详细程度差异很大别抱太大期望。6. 翻译这本书时踩过的几个词汇坑6.1 bank、rank、die的翻译困境原书读得越细越会发现术语翻译是个大坑。bank这个词有人翻成库有人翻成排我发现行业内现在最通行的做法是保留英文原文直接说bank。因为中文语境里无论库还是排都无法传递独立行缓冲独立读通道这个完整的结构含义。同理rank也是翻成列会和column冲突翻成秩又不直观建议直接写rank然后注明含义一组共享片选信号的DRAM芯片集合组内芯片的位宽拼在一起组成完整数据总线。die是另一个坑。芯片制造语境里指晶圆上切下来的一块裸片有人翻成晶粒有人翻成管芯原书讲颗粒结构时频繁出现。翻译时我最后统一用晶粒第一次出现时在括号里注释英文防止读者对照原版时对不上。这种术语一致性看起来是小问题但在长文阅读时非常影响理解速度。6.2 几个特别容易翻歪的术语destructive readout直译破坏性读出是准确的但要补一句读出即须回写否则读者不理解为什么读操作也附带写回。precharge翻成预充电能对上字面但真正含义是把位线复位到中间电压以便感测放大器下一次能分辨微弱信号。所以每提到预充电最好顺手带一句相当于清货台。row hit / row miss原书里的口语化术语翻译成行命中/行未命中不够直观我一般会在前几次出现时补充解释打开的行正好就是目标行。还有一处让我印象很深burst length翻成突发长度看起来没毛病但不了解背景的读者容易不知道它说的是一次列访问连续传几个数据字。实战中配合预取概念一起讲读者才真正明白DDR4为什么在相同频率下比DDR3带宽翻倍。6.3 给同样啃英文原版的人几个方法建议第一千万不要按章节顺序硬刷。先看每章末尾的总结把这一章要回答的问题列出来再倒回去看推导。第二单独维护一个术语表每遇到一个新缩写就写上全称、物理含义、所在上下文。第三遇到为什么比是什么更容易卡住的地方不妨先跳过参数本身直接在纸上画一遍访问时序流水图。DRAM这种东西一旦你脑子里有一个信号在组件之间流动的动态画面所有时序符号瞬间就有了位置。顺便说一句Bruce Jacob当年写这本书时面向的主要是做体系结构和存储器设计的研究生有几章读起来确实像论文综述。如果你只是想了解现代内存的大致框架前三部分足够如果是要做性能优化或硬件部署DRAM部分全读不亏。我自己读了至少两遍每次带着不同的实际问题回去翻都能找到新的答案。第二遍之后再回头看你手头那套DDR4参数表那些数字就不再是无意义的规格参数了。