计算机基础:内存与高速缓存系统

从存储金字塔到 DRAM 单元、从虚拟内存到 TLB、从 ECC 到 NUMA 与多通道——一条指令背后的内存系统全景。

CPU 一次乘法大约需要 0.3 ns,机械硬盘一次寻道要 10 ms。两者之间是 3 千万倍 的速度差。整套内存工程——多级缓存、DRAM 单元、页表、TLB、ECC、NUMA、多通道——都是为了回答这一个问题:这条鸿沟,怎么填?

这是《计算机基础深度解析》的第 2 篇。我们不会停留在"DDR 比硬盘快、内存断电就丢"这种层面,而是顺着一条 load 指令,从 CPU 流水线一路追下去,看它如何穿过 L1、L2、L3 缓存,经过 TLB 和页表查表,被内存控制器调度到对应通道,最后落到具体 DRAM 单元上的一个电容上——以及每一层到底在做什么、为什么这样设计。

系列导航

📚 计算机基础深度解析系列(共 6 篇):

  1. CPU 与计算核心
  2. → 内存与高速缓存系统当前位置
  3. 存储系统(HDD、SSD、NVMe、RAID)
  4. 主板、显卡与扩展(PCIe、USB、BIOS)
  5. 网络、电源与实战故障排查
  6. 全景串联:从单条指令到整机视角

1. 存储金字塔:为什么不能只有一种"内存"

如果世界上存在一种"又快、又大、又便宜"的存储介质,电脑里只放一大块这玩意就行了。但这种介质并不存在:SRAM 快但贵且密度低;DRAM 密度高但速度慢;NAND Flash 更密、能持久但更慢;机械硬盘最便宜最大但是机械结构。存储金字塔是这种物理与经济矛盾的工程妥协。

存储层次:速度与容量的取舍

读这张图时记住一个朴素的规律:每往下一层,容量大约 ×10、速度也大约慢 10 倍。CPU 寄存器约 0.3 ns,L1 约 1 ns,L2 约 4 ns,L3 约 15 ns,DRAM 约 100 ns,NVMe SSD 约 100 µs,HDD 约 10 ms。

把这些纳秒翻译成"人类时间"会更直观。假设一次寄存器访问 = 1 秒:

层级真实时间1 寄存器访问 = 1 秒时
寄存器0.3 ns1 秒
L1 缓存1 ns3 秒
L2 缓存4 ns13 秒
L3 缓存15 ns50 秒
DRAM100 ns5 分钟
NVMe SSD100 µs4 天
HDD10 ms1 年

每一次 cache miss 落到 DRAM,相当于本来 1 秒能做完的事变成了泡杯咖啡才回得来;每一次 page fault 落到磁盘,相当于本来 1 秒能做完的事变成了一年。这也是为什么"加内存就好"或者"换 SSD 就好"是不准确的认知——真正决定性能的,是你的 working set(工作集)落在金字塔的哪一层

2. DRAM vs SRAM:为什么 CPU 缓存这么小

DRAM 和 SRAM 都是用晶体管和电压来存比特,区别全在"存储单元"上。而单元决定了一切:密度、成本、速度、功耗,甚至 DRAM 为什么要刷新。

DRAM vs SRAM:缓存为什么很小却很快

DRAM = 1 个晶体管 + 1 个电容(1T1C)。 一个比特存成一个微小电容上的电荷。写入时拉高字线(WL),驱动位线(BL),向电容充/放电;读取时拉高字线,由感应放大器(sense amp)判断电容里有没有电荷。一个 DRAM 单元在现代工艺下大约只有 6 F²(F = 最小特征尺寸),所以一颗 DDR5 芯片在几平方毫米里能塞 16 Gb。

但密度的代价是两个很丑陋的事实:

  1. 电容会漏电。 电荷在毫秒级就会自然流失,所以 DRAM 每隔约 64 ms 就要刷新一次——把每一个单元读出来再写回去,仅仅是为了"记得自己原来的值"。这要消耗带宽和功耗。
  2. 读取是破坏性的。 感应放大器为了检测电平要把电容里的电荷抽走,所以读完之后必须立刻写回原值。

SRAM = 6 个晶体管的交叉耦合锁存器(6T)。 一个比特存成两个反相器互相反馈形成的稳定状态。只要供电不断,状态就一直保持——不需要刷新、不破坏、亚纳秒访问。代价是单 bit 占用面积是 DRAM 的约 20 倍。这就是为什么你的 CPU 每个核心只有 64 KB 的 L1,而不是 64 GB。

整个内存层次的存在,就建立在这一个取舍上:CPU 在最在乎速度的位置(寄存器、L1、L2、L3)用 SRAM;外面一层用 DRAM 换密度;再外面用 Flash 换持久性和容量。

3. CPU 缓存:三级桥梁

即使 DRAM 已经达到 100 ns,对 CPU 而言还是慢了 约 300 倍。为了把这道鸿沟填平,现代 CPU 把 SRAM 直接做在芯片里,让"热数据"在片上被服务。缓存不是一块东西,而是一个微缩版的层次结构。

级别共享方式典型大小延迟由什么构成
L1(分为 L1-I 和 L1-D)每核心独享各 32-64 KB~1 ns / 4 周期SRAM
L2每核心独享(或两核心共享)256 KB - 2 MB~4 ns / 12 周期SRAM
L3(LLC)全核心共享8 MB - 96 MB~15 ns / 40 周期SRAM
DRAM片外8-128 GB~100 nsDRAM

命中率是相乘的。设 L1 命中率 95%,L2 在剩下 5% 中命中 85%,L3 在再剩下里命中 70%:

平均延迟
  = 0.95   × 1 ns
  + 0.05  × 0.85 × 4 ns
  + 0.05  × 0.15 × 0.70 × 15 ns
  + 0.05  × 0.15 × 0.30 × 100 ns
  ≈ 1.4 ns

比每次都直接访问 DRAM 快约 70 倍。整个层次结构的目的,就是让 99% 的访问看起来像 L1 访问,同时还假装你拥有 32 GB 的内存。

为什么是三级,不是两级或四级? 每多一级,流水线就要多一次 tag 比较;两级则 L2 到 DRAM 的跳跃太大(约 25 倍);四级带来的复杂度收益不成比例。三级是当代工艺下经验上的最优解——不过随着 AMD 通过 3D V-Cache 把 L3 堆叠到 96 MB,这一边界正在被推动。

4. 虚拟内存:每个进程都活在自己的地址空间里

到目前为止我们说的都是物理地址——DRAM 上的真实坐标。但没有任何现代进程直接看到物理地址。程序运行在虚拟地址空间里,硬件在每一次内存访问时都会把虚拟地址翻译成物理地址。

虚拟地址到物理地址的翻译

翻译以**页(page)**为单位,通常 4 KB。一个 64 位虚拟地址被切分为:

  • VPN(虚拟页号)——程序的哪一页?
  • Offset(页内偏移)——这一页里的哪个字节?

操作系统为每个进程维护一张页表,把 VPN 映射到 PFN(物理帧号)。每次 load/store 时,硬件会:

  1. 拆分虚拟地址;
  2. 走页表查到对应的 PFN;
  3. 把 PFN 和 offset 拼起来形成物理地址;
  4. 把物理地址发给内存控制器。

这一套机制一次性买到了四个非常大的好处:

  • 隔离:进程 A 不可能不小心(或恶意)读到进程 B 的内存,因为两者的页表映射到不同的物理帧;
  • 大内存假象:页表项可以标为"不在内存中",触发缺页异常,让 OS 从磁盘把页调进来——这就是 swap;
  • 共享:两个进程可以通过把同一组物理帧映射进各自的页表来共享一个动态库;
  • 权限:每个页表项都有读/写/执行位,由硬件强制——这是 OS 安全机制的根基。

代价是:每次访问内存都要先走一遍页表,而页表本身就在内存里。x86-64 是 4 级页表,一次 TLB miss 最坏要打 4 次 DRAM——几百纳秒。这正是 TLB 存在的理由。

5. TLB:你可能没听过、但每个 CPU 都有的关键缓存

TLB(Translation Lookaside Buffer,地址翻译旁路缓存)是一个非常小、全相联、由 SRAM 实现的最近翻译过的 VPN→PFN 映射缓存。每个 CPU 都有(通常还分 L1 TLB 和 L2 TLB),entry 数从 64 到 1024 不等。

TLB 命中与未命中的两条路径

CPU 发出一个虚拟地址时:

  • TLB 命中(约 99% 的情况):1 个周期就拿到 PFN,直接去访问缓存/DRAM。代价几乎为零。
  • TLB 未命中(约 1%):硬件(或在某些架构上是软件)要走一遍页表。x86-64 上最多 4 次访存——100~400 ns。结果会被填回 TLB,下一次就命中了。

TLB 是虚拟内存能用得起的根本原因。99% 命中率下,平均翻译开销 ~1 ns;没有 TLB 的话,每条 load 都要 100 ns。绝大多数程序根本感觉不到虚拟内存的存在。但 TLB miss 也是为什么大型随机工作集的程序(部分数据库、图算法、某些 ML 推理模式)的实际性能往往比 cache miss 数据预测得还差。**大页(HugePage,2 MB 或 1 GB)**的主要意义就是减轻 TLB 压力:一条 TLB entry 现在能覆盖 512 倍的内存。

6. 内存通道:为什么两条 8GB 比一条 16GB 强

DRAM 通过**内存通道(channel)**连到 CPU。一个通道就是一条独立的 64 位数据通路,带自己的命令/地址总线。现代桌面 CPU 是 2 通道;服务器 CPU 是 4、8 甚至 12 通道。

内存通道:CPU 与 DRAM 间的并行数据通道

每个通道都可以独立地在传数据,所以峰值带宽几乎与通道数成线性关系

配置通道数单通道速率总带宽
单通道 DDR4-3200125.6 GB/s25.6 GB/s
双通道 DDR4-3200225.6 GB/s51.2 GB/s
四通道 DDR5-4800438.4 GB/s153 GB/s
12 通道 DDR5-4800(Sapphire Rapids)1238.4 GB/s460 GB/s

延迟没有改善——单条 load 仍然是 ~100 ns。改善的是同一时间能并行处理多少条独立的 load。这正是以下场景最在意的事:

  • 多核工作负载(每个核心都想要自己的内存流量);
  • GPU 类型的内存密集代码(渲染、视频、科学计算);
  • 任何在大数组上做流式访问的程序。

这也是技术上"2 × 8GB 强于 1 × 16GB“的原因:单条只能填一个通道,总容量虽相同,带宽却只有一半。在主流 Intel/AMD 桌面板上,正确的双通道插法通常是 A2 + B2(即从 CPU 看过去的第 2 和第 4 槽)。

7. ECC:当宇宙射线变成 bug

DRAM 很可靠,但不完美。封装材料里的 α 粒子、宇宙射线带来的中子、电磁噪声都会让某个 bit 翻转。Google 在 2009 年发表的大规模实测显示,DRAM 的实际错误率比厂商公开数据高得多——每 GB 每年大约一次可纠正错误,少数模块会差出数量级。

游戏机这类场景下,一次未检测到的 bit flip 顶多是偶尔崩溃。但在一个有上亿行数据的数据库里,它意味着静默数据损坏并被备份系统忠实地复制下去

ECC 内存:检测并纠正 bit flip

ECC 内存为每 64 个数据 bit 额外增加 8 个校验 bit(典型方案是 Hamming SEC-DED 码):

  • SEC(Single Error Correction):任意单 bit 翻转都能被检测到并自动纠正,CPU 拿到的是干净数据;
  • DED(Double Error Detection):任意双 bit 翻转能被检测到(但无法纠正),上报给操作系统去 panic,而不是把脏数据返回给应用。

代价也是真实的:

  • 成本:硅面积多 ~12%(72 bit 存 64 bit),ECC 内存条零售价高 30-50%;
  • 性能:编/解码会带来轻微延迟(现代控制器上几乎可忽略);
  • 兼容性:大多数消费级 CPU(Intel Core、绝大多数 Ryzen)禁用 ECC;Xeon、EPYC、Threadripper Pro 和 Apple Silicon 才支持。

判断准则:如果丢一个 bit 会变成"提 bug"而不是"重启一下”,那就上 ECC。服务器、NAS、跑金融/CAD/科学计算的工作站都该上;纯游戏机一般不必。

DDR5 引入了 on-die ECC,但这只在芯片内部做纠错,保护不了从芯片到 CPU 这段总线——后者仍然要靠传统的 ECC DIMM。

8. NUMA:当内存有了"邮编"

一旦主板上有两颗 CPU(或一颗 CPU 里有多个 chiplet),内存就变成了非一致访问(NUMA, Non-Uniform Memory Access):每颗 CPU 都有自己的内存控制器和本地 DRAM。访问本地内存很快;访问对面 socket 的内存得走 socket 间互联(Intel UPI、AMD Infinity Fabric)。

NUMA 拓扑与访问不对称

典型双路服务器:本地访问 ~80 ns,跨 UPI 远程访问 ~140 ns——慢约 1.7 倍,且互联带宽有限。在 4 路、8 路系统上最坏可达 3 倍以上。

这意味着:

  • OS 是 NUMA-aware 的:Linux 调度器会尽量把线程留在它分配内存的那个 socket 上,新分配也尽量分到本地节点(numactlmbindset_mempolicy);
  • 内存分配器是 NUMA-aware 的:glibc/jemalloc/tcmalloc 都用 per-thread arena 来保证就近分配;
  • 数据库是 NUMA-aware 的:PostgreSQL、MySQL、ClickHouse、Spark 都暴露绑核/绑内存的选项;
  • 忽视 NUMA 会让本来能线性扩展的工作负载损失 30-60% 吞吐

即使在单 socket 的现代 CPU 上也能见到"类 NUMA"现象:AMD 的 chiplet(CCD)设计意味着一个 CCD 里的核心访问另一个 CCD 缓存里的数据要走 IO Die,延迟更高。架构启示是普适的——内存有拓扑,好的系统软件必须尊重这个拓扑。

9. DDR 代际演进:每一代到底换了什么

每代 DDR 大约让峰值带宽翻倍、电压再降一档。但有意思的是它们怎么做到的。

代数年份每针速率单通道峰值电压关键架构变化
DDR2000200-400 MT/s1.6-3.2 GB/s2.5 V时钟双沿采样
DDR22003400-800 MT/s3.2-6.4 GB/s1.8 V4n prefetch(内部总线 = I/O 的 2 倍宽)
DDR320070.8-2.1 GT/s6.4-17 GB/s1.5 V8n prefetch、fly-by 拓扑
DDR420141.6-3.2 GT/s12.8-25.6 GB/s1.2 VBank Group 提升并发
DDR520204.8-8.4 GT/s38.4-67.2 GB/s1.1 V每条 DIMM 切成两个独立 32 位子通道、on-die ECC、片上 PMIC

DDR5 最关键的变化是把每条 DIMM 内部切成两个独立的 32 位子通道:从外部看是双通道,从内部看其实是四通道。这就是为什么 DDR5 对多核工作负载的提升不成比例地大——内存控制器能在每条 DIMM 上同时发出两倍数量的独立请求。

DDR6(标准化中)目标 8.8-17.6 GT/s,每 DIMM 4 个子通道。

10. Q & A

Q1. 既然有缓存把平均延迟拉到 ~1 ns,为什么内存速度还重要?

因为平均值掩盖了尾部。一次 cache miss 到 DRAM 仍然是 ~100 ns。任何 working set 装不下 L3 的程序——大数据库、巨型数组、ML 模型——会持续不断地产生这种 miss。更快的内存(更高 MT/s、更低 CL)能降低这些 miss 的代价;双通道则能让 miss 的服务速率翻倍。DDR4-3200 → DDR4-3600 在游戏中带来的 5-15% 帧率提升,几乎全部来自 miss 代价的降低。

Q2. 应该优先选频率还是 CL 时序?

直接看真实纳秒延迟:ns = CL × 2000 / MT/s。DDR4-3200 CL16 和 DDR4-3600 CL18 都正好是 10 ns 首字延迟,那么后者用同样的延迟换来更高带宽,更优。但 DDR4-3600 CL16 是 8.9 ns,明显比两者都强。一般原则是:频率提升与带宽相乘,时序提升与缓存 miss 较多的工作负载相乘。对绝大多数用户,DDR4-3600 CL16 或 DDR5-6000 CL30 是甜点。

Q3. DDR5 带宽翻倍了,为什么我体感没"快一倍"?

因为大多数消费级负载根本不是带宽瓶颈——而是延迟瓶颈或计算瓶颈。游戏在卡 CPU 或卡 GPU 之前,根本喂不饱单通道 DDR4-3200。DDR5 真正发挥作用的是:(1) 多核生产力工作(编译、转码、仿真);(2) 共享系统内存的核显;(3) 核心数继续上升的未来工作负载。如果你今天就受带宽限制,profiler 早就告诉你了。

Q4. “内存不够用了"到底发生了什么?

内核会按严重程度依次做三件事:

  1. 回收 page cache:OS 把空闲内存当文件缓存用。压力上来时先丢最旧的缓存页。代价:未来的磁盘读取会 miss。
  2. 换出匿名页(swap):那些没有文件背书的页(程序堆、匿名 mmap)被写到 swap 分区/文件。下次访问触发 major page fault,要从磁盘把页读回来。代价:NVMe SSD 上每次 ~10 ms,延迟尾部直接爆炸。
  3. OOM killer:连 swap 都跟不上,内核会挑一个进程杀掉。Linux 上的选择大体与占用内存量成正比。

所以"8GB 够用,反正我有 SSD"是只对了一半:SSD 只是在你已经开始 swap 之后减轻痛苦,但它无法掩盖你已经掉出 DRAM 那一层的事实。

Q5. 多核系统里"缓存一致性"为什么是个大问题?

每个核心有自己的 L1/L2 cache,但它们共享同一份逻辑内存视图。如果核 0 把 x = 1 写进自己的 L1,而核 1 自己 cache 里还有一份 x = 0,硬件必须检测到并使其它副本失效或更新。标准协议是 MESI(Modified、Exclusive、Shared、Invalid),由硬件实现,由 L3 或目录跟踪。

性能悬崖是 false sharing(伪共享):两个线程写两个不同变量,但这两个变量恰好落在同一条 64 字节 cache line 上。每次一个核写,就让另一个核的副本失效,尽管逻辑上根本没有共享。修复方法是把每线程的热数据按 cache line 对齐。这一个改动能在高竞争的计数器、无锁队列上带来 3-10 倍加速。

Q6. 什么是"内存序”?我为什么要在意?

现代 CPU 会大幅重排 load 和 store 来保持流水��繁忙。只要单线程视角自洽,硬件几乎可以任意重排。但跨线程时,一个核心看到的写入顺序未必是另一个核心实际写出的顺序。x86 是相对强一致的模型(TSO,但 store buffer 带来唯一例外),ARM 和 PowerPC 弱得多。这就是为什么并发代码要使用内存屏障、原子操作、acquire/release 语义——强制硬件按算法要求的顺序"对外公开"写入。几乎每一个"在 x86 笔记本上跑得好、上 ARM 服务器就崩"的 bug,本质都是内存序 bug。

Q7. 为什么服务器要这么多内存通道?

服务器 CPU 通常就是带宽瓶颈。一颗 96 核的 EPYC 跑内存数据库时,对 DRAM 的请求是源源不断的;单通道根本喂不饱。12 通道 DDR5-4800 提供 ~460 GB/s——即便如此通常仍是瓶颈。同样的逻辑也适用于 GPU,所以才有 HBM(高带宽内存):把 8-12 颗 DRAM die 在硅中介层上叠在 GPU 旁边,提供 ~3 TB/s 带宽。

Q8. 怎么判断一个慢程序是不是"内存瓶颈"?

Linux 上三个快速信号:

  1. perf stat -e cache-misses,cache-references,LLC-loads,LLC-load-misses ./prog:LLC miss 率 > 10% 通常意味着是 DRAM 瓶颈;
  2. perf stat -e dTLB-load-misses,iTLB-load-misses:TLB miss 高,意味着加大页可能有用;
  3. pmu-toolstoplev 做 top-down profiling,直接把流水线 stall 归因到 “Backend / Memory Bound”、“Frontend Bound”、“Bad Speculation” 或 “Retiring”——这能告诉你瓶颈到底是不是内存。

如果确实是内存带宽瓶颈,处方包括:把算法做 cache blocking / tiling、NUMA 绑定、大页、prefetch 提示,极端情况下改数据布局(SoA vs AoS)。

11. 总结

  • 内存层次结构存在,是因为没有任何一种介质同时又快又密。SRAM 快但贵(缓存);DRAM 密但慢(主存);Flash 和磁盘更密、能持久。
  • 一条 load 指令静悄悄地穿过 寄存器 → L1 → L2 → L3 → 内存控制器 → 通道 → DRAM rank/bank/row/column,并叠加一层 TLB + 页表 的地址翻译。
  • 缓存 利用时间局部性和空间局部性,把平均 DRAM 延迟从 100 ns 降到 ~1 ns。L1/L2/L3 的命中率是相乘的。
  • 虚拟内存 给每个进程独立地址空间,硬件强制隔离、共享和按需调页;TLB 让这一切的开销变得可承受。
  • DDR 代际 不断让带宽翻倍、电压下探。DDR5 的真正创新 是每条 DIMM 内部两个子通道、on-die ECC、片上电源管理。
  • 多通道 让带宽近线性扩展。永远开双通道;做正经活的话,把主板上的所有通道都填满。
  • ECC 让"静默损坏"变成"被记录的事件"。服务器必备,桌面可选。
  • NUMA 意味着内存有拓扑。本地访问快,远程访问慢。现代 OS 和运行时认真对待这件事——你在多 socket 或 chiplet 系统上也应该。

下期预告

《计算机基础(三):存储系统》 里,我们会顺着数据再往外走一层:SSD 内部的控制器与 FTL、SLC/MLC/TLC/QLC 的取舍、NVMe 队列与 PCIe 通道、RAID 与纠删码,以及为什么存储栈是当前演进最快的一层。下期见。

Liked this piece?

Follow on GitHub for the next one — usually one a week.

GitHub