咱们接着上一篇《CPU学习笔记——cache(1)》往下聊。上一篇我们梳理了Cache的基本架构、容量与命中率的关系,重点搞明白了“为什么CPU里要放这么一小块SRAM”。这篇直接上硬骨头:映射方式、替换策略、写策略,以及多核时代谁也绕不开的缓存一致性。这些概念单拎出来看都挺抽象,但串起来其实就是回答一个问题——CPU怎么用一块又小又快的存储,去管理一块又大又慢的内存。你如果能把这个问题用自己的话讲清楚,Cache这块基本就吃透了。
这篇笔记适合正在啃《计算机组成原理》、准备面试造火箭、或者纯粹好奇CPU内部怎么干活的朋友。当然,如果你已经在写高并发服务或者做性能调优,这篇同样值得看——因为Cache在真实世界里挖的坑,远比你想象的多。
1. 映射方式的本质:给每份内存数据找一个“暂住证”
先说一个特别容易让人绕晕的点:Cache里的数据不是随便找个位置就塞进去的,它必须遵守一套严格的“入住规则”。这套规则就是映射方式。很多人死记硬背“直接映射、全相联、组相联”三个名词,却不知道这三个词到底在解决什么问题。
1.1 为什么不能随机存放
假设你有一本1000页的书(内存),桌上只放得下10页纸的摘抄本(Cache)。每次需要哪一页,就从书上抄到本子上。这时候问题来了:下次再从书上抄某一页时,你是随手放在本子的任意位置,还是规定“第X页只能抄到本子的第Y个位置”?
如果随手放,那找起来就是一场灾难——每页摘抄都得挨个翻一遍才能确认是不是自己要的那页,这违背了Cache“快速查找”的初衷。所以必须制定规则:给定一个内存地址,它能进入Cache的哪个位置。映射方式就是规则本身。
这个规则的核心,是把内存地址拆成三个字段:Tag(标签)、Index(索引)、Offset(块内偏移)。Index决定数据该去Cache的哪一行(或哪一组),Tag用来校验这一行里存的到底是不是你正在找的那个数据块,Offset则负责从块里取出具体的一个字节。搞懂这三者的分工,三种映射方式就全部拿下。
1.2 直接映射:“每人一座,没得商量”
直接映射的规则最硬:内存中的第i块,只能放进Cache的第 (i mod Cache行数) 行,一个萝卜一个坑。
这种方式的优点非常明显,Index直接从地址中截取,硬件只需要一次比较就能定位一行数据,速度极快,实现成本也最低。但它的致命缺陷也是众所周知的“抖动”:如果程序的访问地址恰好反复落在同一个Index上,比如每次都访问第i块和第(i+Cache行数)块,这两个块就会互相踢对方下场,Cache命中率瞬间跌到谷底。
实际工程里,直接映射几乎不会用在主数据Cache上,但它的思想被广泛用在一些特定场景,比如分支目标缓冲(BTB)这类对延迟极其敏感、且允许少量冲突的结构里。
1.3 全相联:“想住哪住哪,代价是找得慢”
全相联恰恰相反:内存中的任何一个块,都可以放进Cache的任意一行。理论上冲突概率最低,空间利用率最高。但问题是,查找时必须把Cache里所有行的Tag全部比对一遍,硬件上要做大量并行比较器,行数越多开销越大,延迟也相应变高。
所以全相联只适合Cache行数很少的场景,比如TLB(后面会专门讲)、或者某些极小的专用缓存。你想象一下:CPU把TLB做成全相联,是因为条目本身只有几十个,就算全部比一遍也就几十路并行比较;但L2 Cache有几十万行,全相联就别想了,光比较器就能把芯片面积吃光。
1.4 组相联:工业界的标准答案
组相联是前两者的折中。规则是:内存块先按Index分到固定的一组(类似直接映射),但组内有多个位置(路)可以挑(类似全相联)。比如常用的8路组相联,意思就是Index相同的数据块,可以在这一组里的8个位置中任选其一。
这带来了一个非常重要的变化——抖动问题大幅缓解。即使某几个地址冲突到同一组,只要组内还有空位,就不至于互相“掀桌子”。而且硬件上每组的比较器数量等于路的数量,8路就是8个比较器并行跑,延迟完全可控。所以现代CPU的L1、L2、L3清一色
