SICI · 天机门 · 第十四章

存深之墙

存储器系统深度 · 结丹三层

你以为cache够大就没有延迟问题。但DRAM访问延迟~100ns是电容充放电的物理极限,prefetch和一致性也救不了所有场景。内存墙不是bug,是物质的决定。

墙影低语:「cache够大,这面墙就不存在了?」
序章

墙影降临

CPU与DRAM,速度差几倍?

存深域的入口是一道峡谷。峡谷尽头矗立着一面墙——不是石墙,是无数细密裂缝拼成的暗影,像被速度差撕裂的断面。

存深之墙峡谷,CPU与DRAM速度差撕裂的断面

墙上有声音,像很多数据包在排队,又像一个声音在数数。

诺依曼老祖 诺依曼老祖

「1980年到现在,CPU的速度翻了三万倍。DRAM的延迟呢?降了不到十倍。」

内存墙之影 内存墙之影

「打个赌吧。你说——cache够大,这面墙就不存在了?」

内存墙预测挑战器

先下注,再拖时间游标看CPU/DRAM双曲线,最后点亮五级延迟

墙影逼你下注:加cache能填平CPU与DRAM这道缝?
详解 · K14-01 内存墙:速度差的四十年

1980—2020 年,CPU 性能年均增长约 50%(频率×IPC 双重红利,累计约三万倍);同期 DRAM 延迟年均只降约 7%,四十年累计不到十倍。原因在物理层:DRAM 单元是「一个晶体管+一个电容」(1T1C),读一次要先把电容里微弱的电荷放大,再写回去补充——这套充放电流程的时间常数几乎不随工艺缩小。于是 CPU 等内存的时间占比越来越高:一次 DRAM 访问 ~80ns,3GHz 的核够执行约 240 拍。这条越来越宽的缝,就是内存墙。

详解 · K14-15 存储墙的真容:RC 时间常数

DRAM 的慢不是工程偷懒,是电容的 RC 充放电时间:位线预充、行激活(tRCD)、列选通(tCL)各有约 15ns 量级的物理下限,且电容太小会存不住电荷、太大会充放电更慢——两头堵。SRAM(cache)用六个晶体管正反锁存,不放电所以快,但面积大 6 倍以上、贵得离谱。所以存储层次永远存在:快的贵而少,慢的廉而多。墙拆不掉,只能绕着走、藏在底下——这是本章一切技术(cache、交错、预取)存在的理由。

第一幕 · 自信

镜像殿 · 副本之裂

四个核心各有一份cache副本,天下太平?

石径尽头是一座镜像殿。殿中立着四面铜镜,每面镜子里映着同一行数据:A = 0。

诺依曼老祖 诺依曼老祖

「四个核心,各有一份cache副本。你让它们各读各的,看起来天下太平。」

内存墙之影 内存墙之影

「四份一样的副本,怎么可能不一致?你试试。」

缓存副本撕裂探索器

全读A → 各核写A=1 → 看其余镜面纹丝不动 → 触发stale read与lost update

内存 A = 0

时间轴 · 数据撕裂

详解 · K14-02 Cache 一致性的两条公理

一致性(coherence)只管同一个地址,定义两条:①写传播——一个核对 X 的写,最终要被其他核看到;②写串行化——所有核对 X 的写,在所有核看来顺序相同(不能 Core1 看到先 A=1 后 A=2,Core2 却看到先 A=2 后 A=1)。注意它不管「不同地址之间谁先谁后」——那是 S6 一致性模型(consistency)的事,一字之差,两层天堑。多核各带私有 cache 是性能必然(共享 L1 会被争抢拖垮),于是副本协调就成了硬件必须回答的问题。

第一幕 · 初窥

窃听巷 · 监听之耳

所有cache监听总线,写者广播闻者作废

镜像殿的后巷狭窄,头顶悬着一根铜线——总线。总线嗡嗡振动,像一条公共的神经。

诺依曼老祖 诺依曼老祖

「每次有核心读写内存,信号都会经过这根线。每个cache都能偷听——这叫监听。」

内存墙之影 内存墙之影

「偷听?你以为偷听就能一致?搭线吧。」

监听协议推导 · snoopy 基本原理

搭四根监听线 → 填两组监听规则 → 看撕裂愈合

总线出现"写地址X"时,其他cache的X副本应?
详解 · K14-03 Snoopy 监听协议

Snoopy 协议 1980 年代随共享总线多处理器诞生(如 Intel 80486 的 MESI 前身),核心是搭总线的「便车」:总线本身是广播介质、天然串行化——每个 cache 控制器挂一对「监听耳」,看到总线上的地址就查自己的 tag 表。写操作触发两种流派:失效类(write-invalidate,把别人的副本标记作废,主流方案)与更新类(write-update,把新值推给所有副本,带宽开销大、已被淘汰)。你推导的两条规则——「见写作废」「持脏拦截」——正是失效类协议的两个基本动作。

详解 · K14-07 广播协议:全序的馈赠与其代价

广播(总线)协议的隐藏福利是天然全序:总线同一时刻只能承载一个事务,这个物理串行化自动给了所有核心一致的操作顺序——S6 的顺序一致性几乎免费。代价同样来自串行化:总线带宽恒定,事务量却随核心数增长,且每次写都要向全部 N 个 cache 发 invalidate、等 N 个 ack——S3 你量过这笔账。所以广播协议的命运早已写定:核少时是最佳方案,核多时必然撞墙(→ S5)。

第一幕 · 重建

四相阵 · MESI推演

一条cache行,到底有几种状态?

巷子尽头是一座四相阵台。台上四个方位各亮着一盏空灯,灯下无字——等你来填。

诺依曼老祖 诺依曼老祖

「一致性的答案,不在书上,在你手上。你来推:一条cache行,到底有几种状态?」

MESI四相阵状态机推演器

推导M/E/S/I四状态 → 连八条转换线 → 看S→M广播代价

状态转换图 · 拖线连接

详解 · K14-04 MESI:Illinois 协议的设计精髓

MESI 又称 Illinois 协议(Papamarcos & Patel,1984),四态的划分依据是两个正交问题:「这行只在我手里吗?」(独占/共享)×「我改过吗?」(脏/净)。E 态是最妙的一笔:独占且干净——若没有 E,「只有我有的干净行」只能算 S,那么第一次写它就必须广播 invalidate(尽管根本没有别人要通知);有了 E,E→M 的本地写零总线事务,单线程程序最常用的「读入再改写」路径完全静默。M 态则承担「唯一真值源」职责:它被替换或有人读之前,必须先把脏数据写回内存或直接转发——内存此时是过期的。

第二幕 · 挣扎

扩展阁 · 五相之争与原子之锁

MOESI的O态、MESIF的F态、原子RMW

扩展阁里,你S3的四相阵被原样搬来。但阵台旁多了一张空台,上有一盏未点亮的第五灯。

诺依曼老祖 诺依曼老祖

「S→M要广播invalidate,等所有人确认。还有一个问题——两个核心同时执行A++,你觉得结果是什么?」

内存墙之影 内存墙之影

「A++不是一条指令。它是读A、加一、写A——三步。两核同时做,丢一次更新,不是理所当然吗?」

协议扩展与原子操作 · 叙事推演

点亮第五灯F/O → 看原子RMW丢失更新 → 选锁方案

详解 · K14-05 MOESI 与 MESIF:第五态的两条路

两条扩展路线都针对 MESI 的真实痛点,且都量产过:MOESI(AMD Opteron/锐龙采用)的 O(Owned) 态允许「脏数据共享」——M 被读后降为 O,由 O 态 cache 直接供货,内存继续过期着也无妨,省掉一次写回;适合多核间频繁共享热点数据的场景。MESIF(Intel Nehalem 起的 QPI/UPI 时代)的 F(Forward) 态解决「N 个 S 态谁回答」的冗余:指定一个转发者,响应流量从 N 降到 1。五态的代价是状态机复杂度与验证成本——每加一个态,转换边数近似翻倍,这也是教科书先讲 MESI 的原因。

详解 · K14-10 原子 RMW:从总线锁到 LL/SC

A++ 的读-改-写三步必须不可分。硬件给两条路:①原子指令——x86 用 lock 前缀(如 lock xadd),近代实现锁 cache 行而非锁总线(早年的 bus lock 会让全机停顿);②LL/SC(链接加载/条件存储,ARM/RISC-V/MIPS)——load-linked 时记下地址并监听,store-conditional 仅当期间无人动过该行才成功,否则重试;它把「原子性」变成「冲突检测」,无锁数据结构(如无锁队列)全靠它。要留意的经典坑是 ABA 问题:值从 A→B→A 变回去,CAS 看不出来——解法是给指针附版本号(tagged pointer)。

第二幕 · 再崩塌

广播狱 · 扩展性死壁

推到64核,看总线怎么被O(n²)压垮

扩展阁的穹顶裂开,露出一排核心插槽——4、8、16、32、64。

内存墙之影 内存墙之影

「MESI?监听?加核啊。加到64。你觉得总线还撑得住?」

诺依曼老祖 诺依曼老祖

「推吧。自己看。」

广播目录扩展性曲线追踪器

推核心数4→64 → 看总线饱和崩塌 → 切目录协议看曲线恢复

总线占用 = 10% 吞吐量 = 100% 事务量 = 16
详解 · K14-06 目录协议:用存储换扩展性

目录(directory)的思路:不再广播,而是在每个内存块的「家节点」(home)存一份谁持有副本的名册,写时按名册点对点发 invalidate。名册本身成了新瓶颈:全映射目录每块存 N bit 位向量——64 核时每块 64bit,1024 核时目录存储将超过数据本身;有限指针目录 Dirᵢ 只记 i 个指针,超出则广播兜底(i 取 4~8 已覆盖绝大多数共享场景);链式目录(如 SCI 标准)把名册串成链表分散到各 cache,省存储但失效延迟变长。目录还失去广播的「天然全序」,一致性排序要靠 ack 计数等机制补——这正是大规模一致性协议验证困难的开端。

第二幕 · 深化

时序殿 · 一致性的锚与隙

coherence保证单地址一致,consistency保证多地址有序

时序殿没有铜镜,只有一张巨大的时间轴横亘殿中。轴上排列着两个核心的读写操作,像两条并行的河。

诺依曼老祖 诺依曼老祖

「一致了——同一个地址,所有核心看到的值相同。但顺序呢?Core 0先写A=1再写B=1,Core 1先读B再读A——你保证B=1时A也一定=1吗?」

内存墙之影 内存墙之影

「一致性和一致性序,差一个字,差一道天堑。」

存储一致性模型 · SC与弱序

选一致性模型 → 看操作交错是否合法 → 看性能差异

详解 · K14-08 顺序一致性:Lamport 的严格定义

SC(Sequential Consistency)由 Lamport 于 1979 年定义:「多处理器并行执行的结果,等同于把各核操作按某种交错顺序串行执行,且每个核内部的操作顺序不变。」两个要点:①所有核看到同一个全局顺序(不是各自为政的合法交错);②程序顺序必须保留。SC 对程序员是天堂(直觉即正确),对硬件是镣铐:store buffer、invalidate queue、乱序提交这些提速结构天然破坏全局序,要严格 SC 就得大量 stall——这就是性能条上 SC 只有 30% 的原因。

详解 · K14-09 弱序与释放一致性:按「是否同步」分类松绑

弱一致模型的洞察:数据竞争只发生在没走同步的访问上。把访存分成「同步操作」(加锁/解锁、flag 读写)与「普通数据操作」——后者随便重排,前者必须严格有序:release(释放/解锁)保证它之前的写先可见,acquire(获取/加锁)保证它之后的读不被提前。你放置 fence 恢复「W(A) 先于 W(B) 可见」,正是 release 语义。现实中的 x86-TSO 介于 SC 与弱序之间:允许「写后读」被 store buffer 重排(唯一放松的一种),所以 x86 程序员有时仍需 mfence;ARM/RISC-V 则是彻头彻尾的弱序,C++11 的 memory_order 体系正是为在这光谱上写可移植代码而生。

第二幕 · 不安

交错阵 · 带宽之舞与优化之术

交错翻带宽、预取藏延迟、编译器改局部性

交错阵台上排列着八块内存bank,像八条平行的河道。

诺依曼老祖 诺依曼老祖

「一次访存只走一条bank,其余七条闲着。你把连续地址分散到不同bank——让它们并行流。这叫交错。」

内存墙之影 内存墙之影

「带宽翻倍?你试试。但——你能预取的,只有你能预测的。」

交错·预取·编译器优化 · 三把刀

切交错模式看带宽 → 切访问模式看预取污染 → 行/列优先看miss率

耗时 = 640ns 带宽倍数 = 1×
详解 · K14-11 存储交错:把串行延迟摊成并行流水

DRAM 一次访问的 80ns 里,真正传数据只占末尾几 ns,前面都是行激活与列选通的「占位时间」。低阶交错(连续地址按 bank 轮发,A%8)让 8 个 bank 的占位时间互相重叠:第一个数据回来后,其余 7 个按节拍接连抵达——带宽 ×8,这正是 DDR「突发传输」(burst)与多通道内存(双通道带宽翻倍)的原理。陷阱是bank 冲突:stride=8 的访问全落在同一 bank,每次都要等上一次完全结束——带宽跌回 1×。所以结构体数组(AoS)改数组结构体(SoA)、矩阵访问改布局,本质都是在躲 bank 冲突。

详解 · K14-14 预取与编译器优化:预测的艺术

硬件预取器守在 cache miss 流上找规律:stride 预取器检测等差序列(+64B、+64B……)就提前取后续行——顺序访问下准确率 95%+;但指针追逐(p->next)的地址无规律,预取不仅白取,还把有用行挤出 cache(污染 70%)。编译器侧的对应武器是改造访存模式本身:循环交换(把按列的内层循环改成按行)、循环分块 tiling(把大矩阵切成小块,块内数据能驻留 L1/L2 再复用)、数组合并与填充对齐。行优先遍历 miss 3% vs 列优先 87%——同一个算法,8 倍性能差,这就是「cache-aware 编程」的含金量。

第三幕 · 俯瞰

存域回廊 · NUMA与持久之层

内存不是平的,存储层次不止三层

回廊两侧是两面巨大的全景图。左图:四个NUMA节点,每个节点有自己的本地内存和四个核心。右图:存储层次金字塔——顶端CPU寄存器,向下L1/L2/L3 cache,再向下DRAM,最底层SSD。

诺依曼老祖 诺依曼老祖

「左边:你的核心访问本地内存~80ns,访问隔壁节点的内存~150-300ns——这叫NUMA。右边:DRAM和SSD之间,多了一层——非易失性内存。」

NUMA延迟地图 · PM新层

点NUMA节点看local/remote延迟 → 拖入PM层看新维度

详解 · K14-12 NUMA:内存的地理学

多路服务器里,每个 CPU 插槽挂着「自己的」内存条——本地访问走片外内存控制器 ~80ns;访问对家插槽的内存要走插槽间互连(Intel UPI / AMD Infinity Fabric),延迟 150~300ns,带宽还打折。这就是 NUMA(非一致内存访问)。操作系统的对策是 first-touch 策略:哪个核先写这块页,页就分配在哪个节点——所以高性能程序要配合线程绑核(affinity)与 numactl,让「线程和它摸的内存同处一个节点」。数据库与科学计算的 NUMA 失配曾造成 2~4 倍的性能谜团,元凶都是跨节点流量。

详解 · K14-13 持久内存(PM):DRAM 与 SSD 之间的新大陆

持久内存(以 Intel Optane DIMM 的 3D XPoint 为代表)曾给出诱人组合:字节寻址(像内存,不像 SSD 按 4KB 块读写)、断电不丢、延迟 ~300ns(比 DRAM 慢 3 倍,比 SSD 快 300 倍)、单条容量可达 512GB。两种用法对应你点的两个按钮:Memory Mode 拿 DRAM 当 PM 的 cache,软件零改动获得大内存;App Direct 让数据库直接 mmap 持久写,省掉 write-ahead log 刷盘。注:Optane 已于 2022 年停产退场——但「内存与存储之间需要持久层」的思想活了下来,正以 CXL 内存扩展与高速 NVMe 的形态延续。

第三幕 · 最高潮

Boss 战 · 内存墙之影

三问对决 · 排除法收网

回廊灯火齐灭。暗影从四面八方压过来,凝成一面巨大的、布满裂缝的墙。

内存墙之影 内存墙之影

「我是内存墙之影。我不住在你的无知里——无知救不了我。我住在你'以为优化能消灭物理极限'的地方。你每以为加一层cache、开一个预取就能填平我,我就厚了一寸。三问。答错一题,墙就厚一分。」

渊账累计 0 笔 · 三问全对后清零

第三幕 · 收束

尾声 · 破墙之光

64个核心到底怎么通信?——下一章的问题

墙影凝固成DRAM单元结构后,峡谷透进一缕金光。你手里的延迟沙漏还在——CPU侧金沙飞落,DRAM侧沙粒迟滞。差距仍在,但你不害怕了。

诺依曼老祖 诺依曼老祖

「你入门了。不是因为你答对了三问——答对的会忘。是因为你摔过的每一道裂痕,都刻在你手上了。但——这些核心,到底怎么连在一起?总线?互连网络?片上网络?你说说,64个核心要怎么通信才不堵?」

知识图谱点亮 · 存储墙权衡

点亮全章15块碎片 → 看存储层次延迟谱全景

复盘 · 存深域全图:从撕裂到修路

本章走了一条「信心三度崩塌」的路:第一崩(S1-S2)——以为各核有副本就太平,撕裂教你监听;第二崩(S3-S5)——以为 MESI 一劳永逸,64 核的 O(N²) 教你目录与「没有银弹」;第三崩(S6-S7)——以为一致即有序、优化即拆墙,SC/弱序的天堑与三把刀的边界教你:优化只能隐藏墙,不能消灭墙。最后(S8)你学会了在墙下修路的全景手艺:NUMA 选位置、PM 选层次。下一章的问题已经抛出——64 个核心不靠总线,靠什么互连?片上网络(NoC)在 ch15 等你。

交互实验

缓存命中率

拖动容量、路数与工作集,观察命中率随容量的提升。