S03 —— 有了 DRAM,为什么 CPU 还需要 Cache?多层 Cache 是怎么配合的

金句:木桶最大的那块短板,往往决定了你努力的上限。


CPU 和内存的速度鸿沟

现代 CPU 的计算能力非常强,但访问内存的速度却慢得可怜:

Bash
性能差距(2024 年典型桌面 CPU):

CPU 计算一个加法:~1 cycle(约 0.3ns @ 3.5GHz)
访问 L1 Cache:~4 cycles(约 1.2ns)
访问 L2 Cache:~12 cycles(约 3.5ns)
访问 L3 Cache:~40 cycles(约 11ns)
访问 DRAM:~200~300 cycles(约 60~90ns)

内存带宽:DDR5-480076.8 GB/s(双通道)
CPU 计算能力:3.5GHz × 8 cores × 4 FLOPs/cycle ≈ 112 GFLOPS

问题:
  - 内存延迟 ~60~90ns,CPU 全速等待的话,每年有 200 多天在等内存
  - 内存带宽虽然高,但单个内存请求的延迟改善很慢

Bash
速度对比(把 CPU 计算速度设为"1秒"):

CPU 核心计算      ─────────── 1 秒 ───────────
L1 Cache 命中     ───────────── 4 秒 ────────────
L2 Cache 命中     ────────────────── 12 秒 ─────────────────
L3 Cache 命中     ────────────────────────── 40 秒 ──────────────────────
DRAM 访问         ─────────────────────────────────────────── 200 秒 ──────────────────────────────────

内存延迟比 L1 慢了 200 倍,这就是"内存墙"(Memory Wall)。

为什么 DRAM 不能做快一点

"让 DRAM 和 CPU 一样快"——技术上很难,原因是物理限制:

Bash
DRAM 速度受限的根本原因:

1. DRAM Cell 的物理限制:
   - 电容充放电需要时间(RC 延迟)
   - 位线(Bit Line)的电容很大,充放电慢
   - 刷新操作占用带宽(~1.4%)

2. 内存总线的物理限制:
   - DDR5 内存通道带宽可达 76.8 GB/s
   - 但单个请求的延迟(latency)由物理距离决定
   - 从 CPU 到内存控制器到 DIMM 槽,距离 ~10~20cm
   - 光速下 20cm 往返 ≈ 1.3ns(这是最小延迟)
   - 实际加上晶体管延迟和协议开销,~60~90ns

3. 成本权衡:
   - SRAM Cache 做在 CPU 芯片上,功耗大、面积贵(6T/Cell)
   - DRAM 是独立的芯片,便宜、容量大
   - 用 SRAM 做所有内存,成本是天价

Cache 的基本原理:局部性

Cache 的存在基于两个重要的局部性原理:

Bash
时间局部性(Temporal Locality):
  - 如果一个数据刚被访问过,它很可能很快又被访问
  - 例:循环变量 i,在循环的每次迭代中都被访问

空间局部性(Spatial Locality):
  - 如果一个数据被访问,它附近的数据很可能很快被访问
  - 例:访问数组元素 a[i],a[i+1]、a[i+2] 很可能马上需要

Cache 的设计就是为了抓住这两个局部性。

Cache Line:Cache 不是按"字节"管理的,而是按"Cache Line"(64 字节)管理。当 CPU 读取地址 A 的数据时,Cache 会把 A 所在的整个 Cache Line(64 字节)都取过来——因为空间局部性意味着附近的数据很可能也需要。

Bash
Cache Line 的工作方式:

CPU 读地址 0x1000(1 byte)
  → Cache 发现这个地址不在 Cache 里(Cache Miss)
  → 从 DRAM 读取整个 Cache Line(0x1000 ~ 0x103F,共 64 字节)
  → 放到 Cache 里(tag=0x10, index=..., offset=0)
  → 返回 1 byte 给 CPU

后续访问 0x1001~0x103F:
  → Cache Miss → 已在 Cache,直接返回(Cache Hit)

多层 Cache 的架构

现代 CPU 典型 3 层 Cache:

Bash
典型 3 层 Cache 架构(桌面 CPU,2024):

CPU Core 0          CPU Core 1          CPU Core 2          CPU Core 3
  │                     │                     │                     │
L1d ──┬── L1i          L1d ──┬── L1i          L1d ──┬── L1i          L1d ──┬── L1i
4-way4-way          4-way4-way          4-way4-way          4-way4-way
32KB  │ 32KB            32KB  │ 32KB            32KB  │ 32KB            32KB  │ 32KB
~1ns  │                  │                     │                     │
       │                  │                     │                     │
       └──────────────────┴──────────────────┴─────────────────────┘
       │                     │                     │
      L2                   L2                   L2
   12-way                12-way                12-way
   512KB ~ 1MB           512KB ~ 1MB           512KB ~ 1MB
   ~3.5ns               ~3.5ns               ~3.5ns

                    ┌────────────────────────┐
                    │        L3(共享)       │
                    │      16~32MB            │
                    │     ~11ns(不命中率)    │
                    └──────────┬─────────────┘
                               │
                           DRAM
                          DDR5-4800
                          ~60-90ns

每层 Cache 的特点

大小 延迟 共享方式
L1 32KB~64KB/CPU ~1ns 每个核心独享(L1d 数据/L1i 指令)
L2 256KB~1MB/CPU ~3.5ns 每个核心独享(越来越普遍)
L3 16MB~64MB ~11ns 所有核心共享(Inclusive 或 Exclusive)

Cache 的映射方式:N-way Set Associative

Cache 不是随意放的,是通过映射来决定数据放在哪里:

Bash
直接映射(Direct Map)—— 最简单但冲突多:
  每个地址只能放在 Cache 的一个固定位置
  冲突时直接替换 → 颠簸(Thrashing)

全关联(Fully Associative)—— 无冲突但硬件贵:
  数据可以放在 Cache 的任意位置
  需要比较所有 Cache Line 的 Tag → 硬件复杂、功耗高

N路组相联(N-way Set Associative)—— 工业界的折中:
  Cache 分为多个 Set,每个 Set 有 N 个 Way
  同一地址先找 Set,再在 N 个 Way 里找 Tag
  现代 CPU 典型配置:L1 = 8-way,L2 = 12-way,L3 = 16-way

8-way Set Associative 示例(64KB L1,64B Cache Line):
  - Cache Line 数量:64KB / 64B = 1024 行
  - Set 数量:1024 / 8 = 128 个 Set
  - 地址划分:offset(6bit) + index(7bit) + tag(51bit)
  - 同一个地址的数据,最多只能出现在同一个 Set 的 8 个 Way 之一
  - 冲突概率:只有同一个 Set 的 8 个位置会冲突

缓存一致性和 MESI 协议

多核 CPU 里,每个核心有自己的 L1/L2 Cache。如果 Core 0 修改了地址 X,Core 1 的 Cache 里也有同一个地址 X 的副本——谁来决定最终值?

Bash
MESI 协议(缓存一致性协议):

M(Modified)—— 本核独占且已修改,数据不在主内存
E(Exclusive)—— 本核独占但未修改,数据和主内存一致
S(Shared)    —— 多个核共享,数据和主内存一致
I(Invalid)   —— Cache Line 无效(不在缓存中)

状态转换:

Core 0 读地址 X:
  → X 不在任何 Cache → 从 DRAM 读入,状态 = E(Core 0 独占)

Core 1 也读地址 X:
  → Core 0 的 Cache 状态变为 S(共享)
  → Core 1 也持有 X 的副本,状态 = S

Core 0 写地址 X:
  → Core 0 发出 RFO(Read For Ownership),广播给所有核
  → Core 1 的 X 副本变为 I(无效)
  → Core 0 写入 X,状态变为 M(已修改)

Core 1 再读地址 X:
  → Core 1 发现自己的 X 是 I → 向 Core 0 请求
  → Core 0 把 X 的最新值写回 DRAM(M→S)
  → Core 1 从 DRAM 读到 X(状态=S)

MESI 的开销:每次跨核写数据都要广播 RFO,在核心数很多的 CPU(16核以上)会成为瓶颈。新的协议(MOESI/ARM 的 DMB)在向这个方向发展。


总结

  • 内存墙:DRAM 延迟比 L1 Cache 慢 200 倍,CPU 大部分时间在等内存
  • Cache 基于局部性:时间局部性(刚访问的数据再访问)+ 空间局部性(附近的数据也访问)
  • Cache Line:最小单位是 64 字节,一次抓一批,充分利用空间局部性
  • 多层架构:L1(~1ns,独享)→ L2(~3.5ns,独享)→ L3(~11ns,共享)→ DRAM(~60ns)
  • N路组相联:8-way/12-way/16-way 是工业界成本和性能的折中
  • MESI 协议:多核 Cache 一致性,Modified/Exclusive/Shared/Invalid 四个状态保证数据不冲突

下篇预告(S04):Cache 是怎么和 CPU 核心连在一起的?核心内部的访存单元、数据通路、以及总线宽度是怎么设计的?


关注公众号「AI不着急」,回复”资料”获取内存学习路线图。

最后修改: 2024年5月19日

作者

评论

发表评论

您的邮箱地址不会被公开。