🐧 第六章 存储层次
📄 一句话概括

平均访问时间 = 命中开销 + 缺失率 × 缺失代价。命中率从 99% 掉到 95%,平均时间就从 6 个周期涨到 14 个周期——只掉 4 个点,性能掉一倍多。

📝 本节和前面几节的关系

6.1 到 6.5 讲了一堆数字,这一节把它们收成一个能算的公式,顺便说明为什么【多级】一定比【一级大缓存】强。

一、一层缓存怎么算

公式:平均访问时间 = 命中开销 + 缺失率 × 缺失代价。 代入:命中 4 周期、缺失 200 周期——命中率 99% 时是 6 个周期,95% 时是 14 个周期。 命中率掉 4 个点,平均时间涨了一倍多。 这就是为什么【缓存命中率】是性能里最敏感的一个数——它不线性,缺失那部分的代价被放大了 50 倍。 所以【优化缓存】的着力点是【减少缺失次数】,不是【让命中更快】。命中那 4 个周期已经压不动了。

二、多级怎么算

从最下面往上算:把上一层的缺失当成下一层的一次访问。 L3:40 + 10% × 200 = 60 周期。L2:12 + 10% × 60 = 18 周期。L1:4 + 10% × 18 = 5.8 周期。 结果只有 5.8 周期——比 L1 的 4 周期多一点,但比直接访问内存的 200 周期快几十倍。 这就是多级的全部意义:每一层只兜住上一层的 10%,层层下来,绝大多数访问都被最便宜的那一层接住了。

三、为什么不是【一级超大的缓存】

因为【快】与【大】在硬件上是矛盾的:要 1 纳秒,就得又小又贵。 一级大缓存只有两个选择:要么慢成 L2,要么贵到买不起。 分层是唯一能同时满足【快】和【大】的办法——用 90% 的命中率把慢的那层挡住。 这个思路不只用在缓存上:页表分级、存储金字塔、CDN,全是同一招。

四、三条结论

一、命中率比容量重要:掉 1 个点就可能掉十几个周期。 二、多级比一级大缓存有效:最常用的那部分才值得用最贵的存储。 三、局部性是一切的前提:没有局部性,命中率会掉到接近 0。 三条合起来:缓存不是【装得多】,是【装得准】。

五、动手 Demo:总结——平均访问时间怎么算

这一节把公式摆出来,算一遍【命中率每掉几个点,平均时间涨多少】,再算一遍多级缓存叠加之后的结果。

hit, miss = 4, 200
for rate in (0.99, 0.95, 0.90, 0.80, 0.50):
    avg = hit + (1 - rate) * miss
    print(rate, avg)

l3 = 40 + 0.1 * 200
l2 = 12 + 0.1 * l3
l1 = 4 + 0.1 * l2
print("L3/L2/L1", l3, l2, l1)

实跑输出:

--------------------------------------------------------------------
总结——平均访问时间怎么算
--------------------------------------------------------------------
① 一层缓存:平均时间 = 命中开销 + 缺失率 × 缺失代价

  命中率     缺失率     平均访问时间     相对全命中
  --------------------------------------------------
  99%        1%         6.0 周期         1.5 倍
  95%        5%         14.0 周期        3.5 倍
  90%        10%        24.0 周期        6.0 倍
  80%        20%        44.0 周期        11.0 倍
  50%        50%        104.0 周期       26.0 倍

  **命中率从 99% 掉到 95%,平均时间就从 6 个周期涨到 14 个周期**——
  **只掉了 4 个点,性能掉了一倍多。**
  **这就是为什么【缓存命中率】是性能里最敏感的一个数。**

② 两层缓存:把上一层的缺失当下一层的访问

  层级     命中率     命中开销     这一层要承担
  ----------------------------------------------
  L1       90%        4 周期       10% 落到 L2
  L2       90%        12 周期      10% 落到 L3
  L3       90%        40 周期      10% 落到内存
  内存     —          200 周期     全部由它兜底

  **从下往上算**:**L3 这一层平均 60.0 周期,L2 18.0 周期,L1 5.8 周期。**
  **注意最后结果只有 5.8 周期**——**比 L1 的 4 周期多了一点,**
  **但比直接访问内存的 200 周期快了几十倍。**

  **这就是【多级缓存】的全部意义**:**每一层只兜住上一层的 10%,**
  **层层下来,绝大多数访问都被最便宜的那一层接住了。**

③ 三层结论

  结论                     为什么
  ----------------------------------------------------------
  缓存命中率比容量重要     掉 1 个点就可能掉十几个周期
  多级比单级大缓存有效     最常用的那部分才值得用最贵的存储
  局部性是一切的前提       没有局部性,命中率会掉到接近 0

命中率 99%→6.0 周期,95%→14.0 周期,50%→104 周期。 注意 99% 那行只比全命中多 2 个周期——而 90% 那行已经是 6 倍。 多级叠加的结果是 5.8 周期——三层各 90% 的命中率,合起来的效果相当于一层 99.9% 的缓存。 这就是【层层挡住】的威力:不需要每层都很强,只需要每层都挡住一大半。 反过来:如果某一层命中率崩了(比如工作集突然变大),上面几层跟着一起崩——这就是【缓存雪崩】在 CPU 里的版本。

📋 Linux 上量真实的命中率:perf stat -e L1-dcache-loads,L1-dcache-load-misses <命令>——两个数一除就是命中率。

30 秒实验

  1. 把公式里的缺失代价从 200 改成 400,看命中率 95% 时平均时间变多少。
  2. perf stat -e L1-dcache-load-misses,L1-dcache-loads ls,算一次真实命中率。
  3. 把多级模型改成每层 95%,看最终平均时间掉到多少。

常见误区

  1. 【命中率 95% 已经很好了】。要看缺失代价:代价 200 周期时,95% 意味着平均慢 3.5 倍。
  2. 【做一层特别大的缓存更简单】。大就慢,慢到那个程度它就不是缓存了。
  3. 【缓存命中率只能靠硬件】。程序的数据布局与访问顺序直接决定命中率,软件能做的一大半都在这里。
自测三问(点开对答案) 1. 平均访问时间的公式是什么? —— **命中开销 + 缺失率 × 缺失代价。**它说明**缺失那部分被放大了几十倍,所以命中率极度敏感。** 2. 为什么多级缓存比一级大缓存有效? —— **每一层只兜住上一层的 10%**,**层层下来绝大多数访问被最便宜的那层接住。** 3. 三层各 90% 命中率,合起来相当于多少? —— **大约相当于一层 99.9% 的缓存**(实测平均 5.8 周期)。**不需要每层都强,只需要每层都挡住一大半。**

相关笔记