访问流程与时钟域 —— 数据在 L1→L2→L3→内存 之间怎么走
本篇是缓存组织拆分的访问流程篇:补齐跨层级视角——一次读、一次写在 L1→L2→L3→内存这条链上怎么逐级下探、怎么写回;再把**单位(周期)**讲清楚——时钟周期/总线周期/指令周期三把"尺子"、现代服务器 CPU 的真实容量量级、以及一致性通信为什么贵(时钟域与互联开销)。前提:地址三段拆分与命中判定见寻址与命中篇,三种映射结构见映射结构篇。
一、现代服务器 CPU 的缓存容量(参考)
先建立直觉:当前主流服务器 CPU 的真实量级。记住三层的组织特点:L1/L2 每核私有、L3(LLC) 一组核共享;容量逐级放大、延迟逐级升高(L1 ~1ns → L3 ~10ns 级 → 内存 ~100ns)。
每核私有的 L1 / L2(各核一份,比较稳定)
| 层级 | 典型容量(每核) | 说明 |
|---|---|---|
| L1 数据 (L1d) | 32 ~ 48 KB | Intel 近代 48KB、AMD Zen 32KB;一般 8-way 左右 |
| L1 指令 (L1i) | 32 KB | 与 L1d 分开(哈佛结构),详见 i-cache |
| L2 | 1 ~ 2 MB | AMD Zen4/Zen5 = 1MB/核;Intel Sapphire/Emerald Rapids = 2MB/核 |
L1 容量几十年基本卡在 32~48KB 没怎么涨——因为它必须在一个时钟周期内命中,太大就快不起来(VIPT 还受页大小×相联度约束)。想更大只能往 L2/L3 要。
多核共享的 L3 / LLC(总量很大,是"卖点")
L3 是一堆核共享的大缓存,总容量取决于核数/CCD 数,各家差别大:
| 平台(代号) | L3 组织 | 全芯片 L3 总量(典型高端) |
|---|---|---|
| Intel Xeon Sapphire/Emerald Rapids | 所有核共享一大块 | 约 105 ~ 320 MB |
| AMD EPYC Genoa/Turin (Zen4/5) | 每 CCD 32MB,多 CCD 拼起来 | 最高约 384 MB(如 96 核 12 CCD) |
| AMD EPYC Genoa-X(带 3D V-Cache) | 每 CCD 堆叠到 96MB | 最高约 1.1 GB —— 目前量产最大 |
AMD 的 L3 是"每个 CCD(核簇)自带一块、彼此不共享"——所以跨 CCD 访问 L3 要走片间互联,延迟更高。这其实是 NUMA 思想在片内的延伸(有时叫 "NUMA within socket / sub-NUMA"),做性能优化时要注意把协作的线程放在同一 CCD。
一颗典型服务器 CPU 的三层速览
以 AMD EPYC 9004(Zen4) 96 核为例的量级:
每核: L1d 32KB + L1i 32KB L2 1MB
每 CCD(8核): 共享 L3 32MB
整芯片(12 CCD): L3 合计 384MB + 内存(DDR5) 数百 GB ~ TB读文档里的例子时换算:32KB/8-way/64B line 是一个核的 L1d的典型配置——正是寻址与命中篇那个"64 组、Index 6 位"的算例。L2/L3 更大、相联度更高(如 L2 16-way、L3 十几~几十 way),但寻址原理(Tag/Index/Offset + 组相联)完全一样,只是位数不同。
⚠️ 具体数字随代际和型号变化,上表是"2023–2025 主流高端服务器的量级",用于建立直觉而非精确规格。查你自己机器的真实值:
lscpu(看 L1d/L1i/L2/L3 cache 行)或cat /sys/devices/system/cpu/cpu0/cache/index*/{level,size,ways_of_associativity,coherency_line_size}。
lscpu | grep -i cache # 一眼看四级容量
cat /sys/devices/system/cpu/cpu0/cache/index0/size # L1d 大小
cat /sys/devices/system/cpu/cpu0/cache/index0/ways_of_associativity # 相联度
cat /sys/devices/system/cpu/cpu0/cache/index3/size # L3 大小二、CPU 访问数据的流程:逐级下探与写回
上面讲了"一个地址如何在某一级缓存里判命中",这里补齐跨层级的视角:一次读、一次写在 L1→L2→L3→内存这条链上到底怎么走。核心规则:CPU 只直接和 L1 打交道;L1 未命中才逐级向下(L2→L3→内存)找,数据取回后逐级填充。
2.1 读一份数据:命中就返回,未命中逐级下探

要点:
- CPU 核只对 L1 发读请求;L1 miss 才由缓存硬件自动去 L2 找,逐级下探。每一级都以 cache line(64B) 为单位搬运——哪怕你只读 1 个字节,也会把包含它的整条 64 字节行拉上来(这正是伪共享的物理基础)。
- "击穿"某级 = 那一级 miss:L1 miss 击穿到 L2,L2 也 miss 击穿到 L3,L3 还 miss 才真正访问内存。命中越靠上越快(1ns vs 100ns,差百倍)。
- 数据取回后逐级回填(inclusive 缓存里 L1 的行 L2/L3 也有副本;具体包含策略因微架构而异)。下次再读同一行就 L1 命中了。
2.2 写一份数据:write-back + write-allocate
现代 x86 的 L1 数据缓存基本都用 写回(write-back) + 写分配(write-allocate) 策略:

两个关键点:
- 写回(write-back):写只改 L1 里的副本并标"脏(dirty)",不立即写内存。脏行一直待在缓存,直到它被驱逐(evict)(缓存满了要腾位置)或被别的核索要时,才真正写回下层/内存。→ 这就是 MSI 里 M(Modified) 状态的物理含义:本核缓存比内存新。对立策略是写直达(write-through,每次写都同步到下层),因太慢现代 L1 很少用。
- 写分配(write-allocate):写一个不在缓存的地址时,先把整条 line 读上来再改(而不是直接写内存)。因为程序往往"写了还会再读/再写",先拉进缓存后续就快了。
2.3 一句话串起来,以及通向一致性
读:L1→L2→L3→内存 逐级找,命中即返回、未命中逐级下探再回填;写:只改 L1 标脏(write-back)、要写的行不在则先取上来(write-allocate)。
以上是单核视角。一旦多核,这套流程会分叉出两个"多核专属"的动作,它们正是缓存一致性协议的起点——读到别核持有的脏行必须先拿最新值、写别核也持有的行必须先 RFO 抢独占。这两点如何演化成 MSI/MESI 状态机,见 msi 与主协议 mesi。
三、缓存访问的时钟域与开销
前面用 ~1ns / ~100ns 谈延迟、用"命中判定是一个周期的组合逻辑"谈速度,这里把单位(周期)和真实芯片的时钟域、总线通信、代价一次讲清——这也是理解"一致性通信为什么贵"的量化基础。
3.1 时钟周期、总线周期、指令周期:三个"周期"分清楚
硬件世界衡量快慢的通用单位是周期(cycle)。三个"周期"常被混:
| 术语 | 是什么 | 典型值(3 GHz CPU 为例) |
|---|---|---|
| 时钟周期 (clock cycle) | CPU 主频的一拍,1 / 主频。是 CPU 内一切操作的最小时间刻度 | 3 GHz → 1 拍 ≈ 0.33 ns |
| 指令周期 (instruction cycle) | 完成"一条指令"(取指→译码→执行→写回)所需的时间 | 见下,不是固定值 |
| 总线周期 (bus cycle) | 通过外部总线完成一次数据传输(如访存)所需的时间;总线有自己的时钟,远慢于 CPU | 一次内存事务 ≈ 几十~上百 ns = 几百个 CPU 时钟周期 |
关键换算:时钟周期是"CPU 的尺",总线周期是"内存的尺",两把尺差了近百倍。 缓存存在的全部意义,就是让大多数访问停在"CPU 的尺"上,别掉到"内存的尺"上。
一条 x86 指令要几个时钟周期?—— 没有单一答案
老教材说"一条指令 = 若干时钟周期",但现代 x86 是流水线 + 超标量,得区分两个概念:
| 指标 | 含义 | 现代 x86 典型值 |
|---|---|---|
| 延迟 (latency) | 一条指令从开始到出结果要几拍 | 简单 ALU(add/xor)1 拍;乘法 ~3 拍;除法 ~20–40 拍;L1 命中的 load ~4–5 拍 |
| 吞吐 (throughput / IPC) | 平均每拍能完成几条指令 | 因为流水线重叠 + 多个执行端口,每拍可退休 4~6 条(IPC 可 >1) |
所以"一条指令几个周期"要看哪条指令、看延迟还是吞吐:
- 理想流水线满载:虽然每条指令延迟好几拍,但靠重叠,吞吐可达每拍 4+ 条(比如一串独立的 add)。
- 一旦访存 miss:一条
load若打到内存,要等 200~300+ 拍——这一条就能让流水线堵住,把 IPC 拖到地板。这正是缓存 miss 为什么这么伤性能。
用周期重新看缓存层次(3 GHz、1 拍≈0.33ns)
把前面的 ns 换算成时钟周期,"快慢差距"更直观:
| 访问位置 | 延迟 (ns) | ≈ 时钟周期 | 说明 |
|---|---|---|---|
| 寄存器 | — | 0(当拍可用) | CPU 内部 |
| L1 命中 | ~ 1 ns | ~ 4~5 拍 | load-to-use 延迟 |
| L2 命中 | ~ 4 ns | ~ 12~15 拍 | |
| L3 命中 | ~ 10~30 ns | ~ 40~75 拍 | 共享,跨核/跨 CCD 更慢 |
| 内存 (miss 到底) | ~ 100 ns | ~ 200~300+ 拍 | 走总线周期,一次事务顶几百拍 |
一次内存访问 ≈ 几百个时钟周期 = 够 CPU 本可以执行上千条简单指令的时间。这就是"L1 快内存慢"的量化真相,也是逐级下探、以及一致性协议里"跨核失效走互联很贵"的根本原因——跨核那套通信也是走总线/互联周期,不是 CPU 内部的时钟周期。
数字随主频/微架构/代际浮动,上表是"3 GHz 级现代 x86 的量级",用于建立直觉。查真实延迟可用
lmbench、mlc(Intel Memory Latency Checker) 实测。
3.2 缓存控制器里的硬件
嗅探式一致性协议下,每个核的缓存控制器里主要有三块硬件:

- 状态存储:MSI 每 line 只需 2 bit 存状态(M/S/I 三态,2 位够)。这几 bit 和 tag 一起放在 line 的元数据里。
- 状态迁移逻辑:一致性状态机在硬件上是一小块有限状态机(FSM)电路——输入"当前状态 + 本核操作/嗅探到的总线事务",组合逻辑输出新状态。纯电路,无软件。
- 双端口 tag:本核访问和总线嗅探都要查 tag 阵列,为免互相阻塞,硬件常做双端口或维护两份 tag(snoop tag)。
3.3 总线通信协议大致长什么样
经典嗅探总线的一次事务分几个"阶段(phase)",各核在总线时钟的节拍上配合:

- 仲裁(arbitration):多个核同时想用总线,仲裁器按策略(轮转/优先级)授权一个——这一步天然实现了协议要的写串行化(同一时刻只有一个事务在总线上)。
- 地址/命令/数据分线:物理上有独立的地址线、命令线、数据线,可流水重叠。
- 原子性靠总线独占:一次 BusRdX 的"广播+失效+取数"在总线看来是一个不可分割的事务,这也是
lock前缀原子操作的底层依托(见 atomic)。 - 现代已不是"一根总线":真实多核用环形(ring)/网格(mesh)互联 + 目录(directory) 取代广播总线(广播不随核数扩展)。目录记录"每条 line 被哪些核缓存",只点对点通知相关核,而非全广播。但对外表现的一致性语义不变——只是"嗅探所有人"变成"查目录再定向通知"。
3.4 时钟域:缓存/总线和 CPU 核不是同一个时钟
不一样,这是关键。 现代芯片是多时钟域(clock domain):
| 部件 | 时钟 | 典型频率 |
|---|---|---|
| CPU 核 + L1/L2 | 核心时钟(跟着核,可动态调频 DVFS) | 3~5 GHz |
| L3/LLC + 片上互联 | Uncore / mesh 时钟(独立,通常更低) | ~ 2~3 GHz |
| 内存总线 | 内存时钟(DDR,又是一个域) | 等效数 GT/s,实际控制器频率更低 |
- 为什么分开:核要尽量高频,但 L3/互联/内存做不到那么快、也没必要;且核会 DVFS 频繁变频,而 uncore/内存要相对稳定。硬上同一时钟既不现实也浪费功耗。
- 代价——跨时钟域要"同步":信号从核心域进 uncore 域,要过同步器/异步 FIFO,带来额外几个周期的固定延迟(跨域握手)。这也是为什么 L3、跨核通信的延迟里有一块"跑不掉的固定开销"。
- 所以 3.1 那张延迟表里 L3 ~40–75 拍、跨核更多,一部分就来自"离开核心时钟域、走较慢的 uncore/互联时钟 + 跨域同步"。
3.5 总线/一致性通信的开销有多大
用 3.1 的"周期"尺子量:
| 操作 | 大致开销(以核心时钟拍计) | 说明 |
|---|---|---|
| L1 命中(无总线活动) | ~4–5 拍 | 根本不上总线,最快 |
| 嗅探命中、cache-to-cache 转发 | ~几十拍 | 一次总线/互联往返 + 跨时钟域同步 |
| 一次跨核失效 + 重取(BusRdX→Flush→重载) | ~几十到上百拍 | 一致性握手时序,走互联 |
| miss 到内存 | ~200–300+ 拍 | 最贵,走内存时钟域 |
三个决定开销的因素:
- 要不要上总线/互联:L1 命中不上,最便宜;一旦触发嗅探/失效/取数,就得付"离开核心时钟域 + 互联往返"的固定成本。
- 广播 vs 目录:核越多,广播式嗅探的流量越爆炸(O(N));目录式只通知相关核,更可扩展——这是大核数服务器不用广播总线的原因。
- 争用:多核狂抢同一条 line(如热点原子量、伪共享),这条 line 在核间反复 RFO/失效"乒乓",每次都付上面那笔互联开销——这就是伪共享/原子争用为什么这么伤(mesi、atomic)。
一句话:一致性通信贵,贵在"要离开核心时钟域、走较慢的互联、可能多次往返"。 优化的方向始终是"让数据尽量停在本核 L1、别让多核抢同一条 line"——这也是本仓库缓存/NUMA 所有优化建议的共同根。
四、跨核往返的完整拆解(通向 store buffer)
上面 3.5 说"跨核失效 + 重取要几十到上百拍",这里把这一圈的逐跳开销拆开看,因为它直接解释了 store buffer 为什么值得存在:
| 阶段 | 经过 | 开销(核心时钟拍) |
|---|---|---|
| ① 请求离开核心域 | 越过核/L1 的时钟域边界,进 uncore 时钟域 | 跨域同步几个拍 |
| ② 查 LLC 目录 | 确认"这条 line 谁持有"(目录式协议) | 几个~十几个拍 |
| ③ 沿 ring/mesh 路由 | 逐跳送到对端核 | 每跳几个拍 |
| ④ 等最慢持有者置无效 | 目标核完成失效并回 Ack | 几十拍量级 |
| ⑤ 原路返回 | Ack/数据沿互联送回发起核 | 同③ |
| ⑥ 跨域同步回核心域 | 回程再跨一次时钟域 | 几个拍 |
对比 L1 命中只要 ~4~5 拍,这一圈直接跳到几十~上百拍——贵就贵在"离开了本核"。 这个拆解在 store-buffer 里有完整应用:正是这几十上百拍的跨核往返,让 store buffer 值得存在——把它从关键路径上藏到后台。
五、总结
缓存访问的完整图景其实就三句话:
- 容量与组织:L1/L2 每核私有、L3 共享;容量逐级放大、延迟逐级升高(1ns → 100ns 差百倍)。
- 读靠下探、写靠写回:读逐级 L1→L2→L3→内存找,命中即返回、miss 再回填;写只改 L1 标脏,驱逐或被索要时才写回。
- 贵在跨域:一致性通信贵,贵在离开核心时钟域、走较慢的互联、可能多次往返——优化的方向永远是让数据尽量停在本核 L1。
把本文的缓存机制落成具体编码习惯(顺序访问、SoA、避免指针追逐、防伪共享……)——见 cache-friendly-code。一致性问题从 msi 讲起。