📄 一句话概括
磁盘缓存=把读过的数据在内存里留一份;它靠的是局部性——程序访问数据往往扎堆在附近几块上。
📝 本节和前面几节的关系
5.1 摆出了内存与磁盘的速度差,这一节给第一条路:少去磁盘。
一、办法很朴素:留一份
既然去一次磁盘那么贵,那就去过就别白去。 磁盘缓存的做法是:读的时候顺手在内存里留一份副本;下次还要同一块,直接从内存给。 写也一样——先写进缓存,攒一攒再一起落盘,把很多次小写合成一次大写。 所以缓存既是【加速】,也是【合并】。
二、为什么它一定有用:局部性
凭经验我们相信缓存能命中,但凭什么是必然的?靠的是【局部性】。 时间局部性:刚用过的数据,很可能马上还要再用(比如循环里的计数器)。 空间局部性:用了某一格,很可能接着用旁边几格(比如遍历数组)。 程序的行为天然带这两种偏爱——所以缓存只要猜中这一点,就能拦下大部分访问。
三、命中率才是关键
缓存有用,不代表可以乱缓存。真正决定效果的是命中率。 命中率高——绝大部分访问都在内存里解决,磁盘几乎不被打扰。 命中率低——每次都白缓存一次,反而多花了管理缓存的开销。 而缓存容量有限,只能装一部分,所以【留下什么、赶走什么】是它最要紧的策略。
四、这一招到处都在用
CPU 有缓存,磁盘有缓存,操作系统、数据库、浏览器、CDN 全都各有一层缓存。 它们的共同点一模一样:慢的东西前面,放一小块快的东西挡着。 所以你一旦看懂了磁盘缓存,几乎就同时看懂了其他所有缓存。 要小心的一点也相同:缓存里的数据可能不是最新的——这是所有缓存都得面对的代价。
五、动手 Demo:提高磁盘访问速度的磁盘缓存
磁盘缓存的做法很朴素:把读过的数据在内存里留一份。 下次再要同一块,直接从内存给,不用去碰磁盘。
cache = {}
hits = 0
for a in (7, 3, 7, 3, 9, 7, 3, 7, 9, 3):
if a in cache:
hits += 1
print("块 %d 命中" % a)
else:
cache[a] = True
print("块 %d 未命中,去磁盘读" % a)
print("命中 %d 次" % hits)
实跑输出:
--------------------------------------------------------------------
提高磁盘访问速度的磁盘缓存
--------------------------------------------------------------------
磁盘缓存的做法很朴素:**把读过的数据在内存里留一份。**
下次再要同一块,直接从内存给,不用去碰磁盘。
模拟一段地址序列,看留一份能省下多少次磁盘访问:
第几次 要哪一块 结果 累计命中
----------------------------------------------------------
1 7 未命中,去磁盘读进来 0
2 3 未命中,去磁盘读进来 0
3 7 命中,直接从内存给 1
4 3 命中,直接从内存给 2
5 9 未命中,去磁盘读进来 2
6 7 命中,直接从内存给 3
7 3 命中,直接从内存给 4
8 7 命中,直接从内存给 5
9 9 命中,直接从内存给 6
10 3 命中,直接从内存给 7
10 次访问命中 7 次,磁盘只被碰了 3 次。
**这就是【局部性】的价值**:程序的访问往往扎堆,
同一块数据被反复用——缓存只要猜中这一点,
就能把大部分访问拦在内存里。
10 次访问命中 7 次,磁盘只被碰了 3 次——因为同一块被反复要。 这就是局部性的价值:程序的访问扎堆,缓存才拦得住。
30 秒实验
- 把访问序列改成 1、2、3、4、5、6、7、8、9、10,再看命中率会变成多少。
- 想想浏览器的缓存和这里说的磁盘缓存,是不是同一个思路。
常见误区
- 以为缓存越大越有用。装不下的部分才是关键,命中率比容量重要。
- 以为缓存里的数据总是最新的。多一份副本就多一份不一致的风险。
自测三问(点开对答案)
1. 磁盘缓存的基本做法是什么? —— 把读过的数据在内存里留一份副本,下次要同一块就直接从内存给。 2. 缓存为什么通常能命中? —— 因为程序的访问有局部性:刚用过的、旁边的数据很快还会被用到。 3. 缓存是免费的吗? —— 不是。容量有限、要管理换进换出,还可能造成缓存与实际数据不一致。- ☐ 下一节(5.3 将磁盘当成内存使用的虚拟内存)
相关笔记
- 程序加载到内存后才能运行 — 速度差是怎么来的
- 将磁盘当成内存使用的虚拟内存 — 从少读变成借来用
- 用好内存先从数组开始 — 顺序访问为什么对缓存友好
- 🖥️ 服务器工作原理 · 索引 — 服务器怎么攒出来的
- ⚙️ 程序是怎样跑起来的 · 索引