Cache miss和TLB miss
cache 缓存数据,TLB 缓存地址翻译结果。 一次访存要先过 TLB 拿到物理地址,再拿物理地址去查 cache,所以它们是串在一条路上的两道关卡。
Cache miss
- CPU 拿着物理地址去 L1/L2/L3 找那一行数据(64 字节),没找到就往下一级找,最后到内存。
- 是数据不在片上。
- 硬件全程处理,软件无感知。
- 代价是一次内存访问的延迟。
TLB miss
- CPU 拿着虚拟地址去 TLB 找对应的物理页号,没找到,就得去内存里的页表查。
- 是翻译不在片上,此时还不知道数据在哪。
- x86 上由硬件 page walker 处理,4 级页表就是最多 4 次内存访问(好在页表项本身也经过 cache,多数时候命中 L2/L3)。RISC 老架构(MIPS、早期 SPARC)是软件处理,直接触发异常进内核填 TLB。
- 查完页表如果发现页不存在,才升级成缺页异常(page fault),那是另一个量级的开销,走内核。
开销对比(x86-64,典型数量级,实现相关)
| 事件 | 大致开销 |
|---|---|
| L1 hit | 4 周期 |
| L1 miss → L2 hit | 12 周期 |
| L2 miss → L3 hit | 40 周期 |
| L3 miss → DRAM | 200 到 300 周期,约 80 到 100ns |
| L1 DTLB miss → L2 STLB hit | 7 到 10 周期 |
| STLB miss,page walk 全在 cache 里 | 20 到 40 周期 |
| STLB miss,page walk 打到 DRAM | 每级一次 DRAM,最坏几百到上千周期 |
| 缺页异常(软缺页) | 几千周期,1 到 3 µs |
| 缺页异常(要读磁盘) | 毫秒级 |
单次比较:一次 TLB miss 通常比一次 cache miss 贵,因为它最坏是多次串行的内存访问,而且拿到物理地址之后还可能紧接着来一次 cache miss,两个开销相加。
为什么实际中 cache miss 更常见地成为瓶颈
- TLB 覆盖范围:L1 DTLB 64 项 × 4KB = 256KB,STLB 1536 到 2048 项,覆盖 6 到 8MB。工作集在这以内 TLB 几乎不 miss。
- Cache 容量:L1 32 到 48KB,L2 1 到 2MB,L3 几十 MB。同样的工作集,先撑爆的是 L1/L2。
- 所以顺序访问、数组遍历这类程序,瓶颈是 cache;工作集 GB 级且随机访问(数据库、大哈希表、JVM 大堆)时 TLB miss 才成为主角,这正是大页解决的问题。
怎么看
如果 dTLB-load-misses 数量接近 cache-misses,说明随机访问跨了大量页,上 2MB 大页通常立竿见影。
一句话记忆
cache miss 是"东西不在手边",TLB miss 是"连东西放哪都不知道",后者查清放哪之后还可能碰上前者。
页表在哪
页表始终在内存里,从来不"完全在 CPU 缓存中"。
页表是内核用普通物理页构造的数据结构,CR3 寄存器里存的是顶级页表的物理地址。内核 alloc_page 出来的页,填上 PTE,就是页表。一个进程映射 1GB 内存,光最后一级页表就 2MB,四级加起来还不止,CPU 缓存放不下也不专门为它留位置。 Page walker 读页表项时,走的是普通的内存访问路径:发出物理地址,先查 L1D,miss 了查 L2、L3,再 miss 才到 DRAM。页表项对缓存来说就是普通数据,和你程序里的数组没区别。 所以"多数时候命中 L2/L3"的意思是:页表项碰巧因为最近被访问过而还留在缓存里。热数据附近的页表项自然热,遍历大块内存时上层页表项(PML4、PDPT 每项管 512GB / 1GB)几乎不会被逐出,最后一级 PTE 才是常 miss 的那级。 Intel 还有个 paging-structure cache(PDE cache 等),专门缓存上面几级的翻译中间结果,让 walk 可以跳过前两三级,直接从最后一级开始。这是 TLB 之外的一小块专用缓存,不是把页表整体放进去。
所以最坏情况是四级页表项全都不在缓存,四次串行 DRAM 访问,每次约 80 到 100ns,加起来几百 ns,这就是 TLB miss 比 cache miss 贵的来源。最好情况是四级全在 L1D,二三十个周期。
硬件 page walker 访问页表时用的是物理地址,不经过 TLB,也不经过页表翻译。否则就是鸡生蛋问题:翻译地址需要读页表,读页表又需要翻译地址,无限递归。
硬件这边怎么闭环
CR3 里存的是 PML4 表的物理地址。 每一级页表项里存的下一级表的地址,也是物理地址(物理页帧号)。 Page walker 从 CR3 出发,每级读到的都是物理地址,直接发到缓存/内存总线,全程不需要翻译。 所以从硬件角度,页表是"自举"的:起点是物理地址,每一步指向的还是物理地址。
内核这边怎么读写页表
内核用 C 代码修改页表时,用的是虚拟地址,这就需要翻译了。Linux 的做法是线性映射(direct map):把全部物理内存按固定偏移映射到内核虚拟地址空间的一段,x86-64 上从 page_offset_base(默认 0xffff888000000000)开始。
内核拿到一个页表页的物理地址,加上偏移就是它的虚拟地址,__va() / __pa() 宏干的就是这个。这段映射:
是内核自己在启动时建好的,用的是 1GB 或 2MB 大页,页表项很少,TLB 里基本常驻。 用它访问页表时,当然也要过 TLB 和页表,但翻译的是线性映射区的地址,跟被翻译的用户页表是两回事,不会递归。 一个容易混淆的点
有些架构(x86 的一些老操作系统、Windows 早期)用过递归页表的技巧:让 PML4 的某一项指向 PML4 自己,这样就能用一段固定的虚拟地址范围直接访问所有页表项。这是软件访问页表的另一种方式,和硬件 walker 不经翻译无关。Linux 不用这招,用线性映射。
暂无评论,欢迎留下第一条评论。