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

地址翻译也要缓存,那张缓存叫 TLB。它只有几百项,512 项 × 4 KB 只能盖住 2 MB;换成 2 MB 的大页,同样的 TLB 能盖住 1 GB——随机访问 64 MB 的命中率从 3.5% 跳到 99.8%。

📝 本节和前面几节的关系

5.5 讲了页表,5.16 讲了四级页表,这一节补上那块让它们能用的缓存——没有 TLB,四级页表根本不可用。

一、为什么翻译也要缓存

四级页表意味着一次访存要先查四次表,再取一次数据——慢五倍。 但 TLB 命中时,翻译结果是现成的,一次都不用查。 所以真实情况是:95% 以上的访问 TLB 命中,只有少数要真去查表。 平均下来每次访存多 0.2 次查表访存——基本可以忽略。 注意这 0.2 次不是【白给】的:它是靠【局部性】换来的,程序总在一小片地址里来回走,TLB 才装得下。

二、TLB 只有几百项

典型 TLB 是 512 项左右。 用 4 KB 页时,512 × 4 KB = 2 MB——这就是它盖得住的最大工作集。 工作集 64 MB 就要 16384 页,是 TLB 的 32 倍——盖不住。 工作集 4 GB 要 100 多万页——更别提了。 所以 TLB 的命中率完全取决于【程序有没有局部性】,跟 TLB 大小关系没那么大。

三、大页是救 TLB 的办法

同一个 64 MB 的工作集:4 KB 页要 16384 项,2 MB 页只要 32 项。 实测 TLB 命中率:4 KB 页 3.5%,2 MB 页 99.8%。 差了两个数量级。这就是 5.17 说的【大页不省内存、省 TLB】在这里的兑现。 所以虚拟化、数据库缓冲池、大数组计算都偏爱大页——它们的工作集天生很大,靠小页根本盖不住。 代价照旧:内部碎片、要连续物理内存、碎片多时申请失败。

四、两件事别混

TLB 缓存的是【地址翻译】;数据缓存(L1/L2/L3)缓存的是【数据本身】。 两者独立:TLB 命中但数据不在缓存里,照样要等内存。 所以一次访存可能同时缺两样:翻译没缓存 + 数据没缓存——那是最坏情况,等于把两次慢都付一遍。 排查时分开看:TLB 缺失多说明【地址跳得散】,数据缺失多说明【数据跳得散】。

五、动手 Demo:转译后备缓冲区——翻译也要缓存

这一节算三笔账:TLB 缺失要额外查几次表、512 项能盖多大工作集、换成大页之后命中率能救回多少。

th, tlb = 0.95, 512
print("平均额外访存", (1 - th) * 4)
for size in (64 * KB, 2 * MB, 64 * MB, 4 * GB):
    print(size, size // 4096, "页")

# 用 LRU 模拟 TLB,比较 4 KB 页与 2 MB 页
for ps in (4096, 2 * MB):
    npg = 64 * MB // ps
    hits = tlb_sim(npg, tlb, 20000)
    print(ps, hits / 20000.0)

实跑输出:

--------------------------------------------------------------------
转译后备缓冲区——翻译也要缓存
--------------------------------------------------------------------
① 一次访存,可能要先查几次页表

  假设 TLB 命中率 95%、四级页表:

  情况           额外访存次数         占多少
  -------------------------------------------
  TLB 命中       0 次                 95%
  TLB 未命中     4 次(每级一次)     5%

  **平均每次访存多 0.20 次查表访存。**
  **听起来不多,但把这 5% 的未命中摊到每一次内存访问上,就不是小数了。**

② TLB 只有几百项,工作集一大就不够

  工作集     要用多少页     TLB 装得下吗
  -----------------------------------------------
  64 KB      16 页          装得下
  2 MB       512 页         装得下
  64 MB      16384 页       装不下(缺 33 倍)
  4 GB       1048576 页     装不下(缺 2049 倍)

  **TLB 只有 512 项,所以它盖得住的最大工作集是 512 × 4 KB = 2 MB。**
  **超过这个数,就靠【局部性】救命**——**程序总在一小块地方来回走,TLB 就够用。**

③ 为什么大页能救 TLB

  页大小     512 项盖得住     随机访问 64 MB 的命中率
  ----------------------------------------------------
  4 KB       2.0 MB           3.5%
  2 MB       1.0 GB           99.8%

  **同样是 64 MB 的工作集,4 KB 页的 TLB 命中率是个位数,2 MB 页接近满。**
  **所以【大页】不是省内存,是让 TLB 重新有用**(5.17 那节的结论在这里兑现)。

5% 的缺失、每次缺 4 次查表——平均多 0.2 次访存,可以接受。 512 项 × 4 KB = 2 MB,所以 64 MB 的工作集是 TLB 的 32 倍。 同样的 TLB,4 KB 页命中 3.5%,2 MB 页命中 99.8%。 所以【大页】不是锦上添花,是【工作集大的程序唯一能用 TLB 的办法】。 这也解释了为什么【同样的程序,开了透明大页会快一截】——快的部分就是省下的查表。

📋 Linux 上看 TLB 效果:perf stat -e dTLB-loads,dTLB-load-misses <命令>——两个数一除就是 TLB 命中率。

30 秒实验

  1. perf stat -e dTLB-load-misses,dTLB-loads ls,算一次 TLB 命中率。
  2. cat /sys/kernel/mm/transparent_hugepage/enabled,看 THP 开没开。
  3. 把上例的工作集从 64 MB 改成 4 MB,看 4 KB 页的命中率变多少(应该接近满)。

常见误区

  1. 【TLB 和数据缓存是一回事】。TLB 缓存翻译结果,数据缓存缓存数据,两者独立。
  2. 【加大 TLB 就能解决】。TLB 加不了太多(要快到 1 周期);真正的解法是【大页】和【局部性】。
  3. 【TLB 缺失会报错】。它只是多查几次表,慢一点,不影响正确性。
自测三问(点开对答案) 1. 为什么地址翻译也要缓存? —— **四级页表意味着一次访存要先查四次表**,慢五倍;**TLB 命中就一次都不用查。** 2. 512 项的 TLB 用 4 KB 页能盖多大?用 2 MB 页呢? —— **4 KB 页:2 MB;2 MB 页:1 GB。**差 512 倍,**所以大页是工作集大的程序唯一能用 TLB 的办法。** 3. TLB 缓存与数据缓存有什么区别? —— **TLB 缓存【地址翻译结果】,数据缓存缓存【数据本身】。****两样都缺的时候,等于把两次慢都付一遍。**

相关笔记