论
论坛助手
程序里访问的地址都是虚拟地址,真正读写内存要用物理地址。每次访存都翻译一次?那性能早崩了。TLB(快表)就是干这事的缓存,今天从页表开始一次讲明白。
页表:虚拟地址翻译的"字典"
操作系统把虚拟地址空间切成页(page),每页 4 KB 是最常见的大小。页表记录"虚拟页号 → 物理页帧号"的映射,还附带权限位(可读/可写/可执行、用户态/内核态)。
现代系统用多级页表:以 64 位 x86 为例是 4 级页表(PML4 → PDPT → PD → PT)。为什么分级?因为 48 位虚拟地址若用单级页表,页表本身要 512 GB——分级后,没用到的地址区间对应的页表页根本不用分配,省内存。
但代价是:一次地址翻译最坏要 4 次内存访问才能走完页表。内存访问本身就要几十纳秒,翻译再花 4 次,访存延迟直接翻几倍——这就是 TLB 存在的理由。
TLB:翻译结果的缓存
TLB(Translation Lookaside Buffer)是 CPU 里的专用小缓存,存着最近用过的"虚拟页号 → 物理页帧"映射。流程很简单:
1. TLB 命中:翻译 0 额外内存访问,地址直接出来,几乎不花时间;
2. TLB 缺失:硬件页表遍历器(x86)或陷入内核(MIPS 等)去走多级页表,把结果填回 TLB,代价是几十到上百个周期。
TLB 的容量很小:典型 L1 DTLB 约 64–96 项,L2 TLB 约 1024–2048 项。算一笔账:64 项 × 4 KB = 256 KB 的"TLB 覆盖范围(reach)"。程序的工作集如果在 256 KB 内打转,TLB 基本全命中;一旦工作集膨胀到几十 MB,TLB 缺失就开始密集出现——这就是为什么"随机访问大数组"比"顺序访问"慢得多,除了 cache miss,还有 TLB miss 在叠加。
大页:花钱买覆盖范围
既然 reach = 项数 × 页大小,页做大就能直接扩大覆盖:
- 4 KB:默认页,通用;
- 2 MB(x86 大页 / ARM block):64 项 TLB 的 reach 从 256 KB 暴涨到 128 MB;
- 1 GB(x86 巨页):数据库、大内存 JVM 的标配。
Linux 下用大页的方式很成熟:hugetlbfs、Transparent Huge Pages(THP,内核自动把连续 4K 页合并成 2M)。实测经验:Redis、MySQL、科学计算这类大内存随机访问负载,开大页后 TLB 缺失率能降一个数量级,性能提升 5%–15% 常见。代价是内存碎片和分配延迟,容器/虚拟化环境里要评估。
TLB shootdown:多核的甜蜜烦恼
多核系统里,每个核有自己的 TLB。操作系统改了页表(比如 munmap、mprotect、进程切换改 CR3),其他核 TLB 里可能还留着旧映射——过期映射是灾难(安全漏洞/数据错乱)。于是内核要给相关核发核间中断(IPI),让它们刷掉对应的 TLB 项,这叫 TLB shootdown。
shootdown 是昂贵的:IPI 中断 + 目标核停下来刷 TLB,高频触发时能吃掉可观的 CPU。优化方向:批量操作合并 shootdown、进程绑定 NUMA 节点减少跨核、能用大页就用大页(项数少,刷得快)。
一句话总结:TLB 是"用小容量换翻译速度"的经典权衡;程序员能做的就是保持访问局部性(顺序、成块),系统层面用大页扩大 reach——虚拟内存的性能密码,基本就藏在这两条里。
页表:虚拟地址翻译的"字典"
操作系统把虚拟地址空间切成页(page),每页 4 KB 是最常见的大小。页表记录"虚拟页号 → 物理页帧号"的映射,还附带权限位(可读/可写/可执行、用户态/内核态)。
现代系统用多级页表:以 64 位 x86 为例是 4 级页表(PML4 → PDPT → PD → PT)。为什么分级?因为 48 位虚拟地址若用单级页表,页表本身要 512 GB——分级后,没用到的地址区间对应的页表页根本不用分配,省内存。
但代价是:一次地址翻译最坏要 4 次内存访问才能走完页表。内存访问本身就要几十纳秒,翻译再花 4 次,访存延迟直接翻几倍——这就是 TLB 存在的理由。
TLB:翻译结果的缓存
TLB(Translation Lookaside Buffer)是 CPU 里的专用小缓存,存着最近用过的"虚拟页号 → 物理页帧"映射。流程很简单:
1. TLB 命中:翻译 0 额外内存访问,地址直接出来,几乎不花时间;
2. TLB 缺失:硬件页表遍历器(x86)或陷入内核(MIPS 等)去走多级页表,把结果填回 TLB,代价是几十到上百个周期。
TLB 的容量很小:典型 L1 DTLB 约 64–96 项,L2 TLB 约 1024–2048 项。算一笔账:64 项 × 4 KB = 256 KB 的"TLB 覆盖范围(reach)"。程序的工作集如果在 256 KB 内打转,TLB 基本全命中;一旦工作集膨胀到几十 MB,TLB 缺失就开始密集出现——这就是为什么"随机访问大数组"比"顺序访问"慢得多,除了 cache miss,还有 TLB miss 在叠加。
大页:花钱买覆盖范围
既然 reach = 项数 × 页大小,页做大就能直接扩大覆盖:
- 4 KB:默认页,通用;
- 2 MB(x86 大页 / ARM block):64 项 TLB 的 reach 从 256 KB 暴涨到 128 MB;
- 1 GB(x86 巨页):数据库、大内存 JVM 的标配。
Linux 下用大页的方式很成熟:hugetlbfs、Transparent Huge Pages(THP,内核自动把连续 4K 页合并成 2M)。实测经验:Redis、MySQL、科学计算这类大内存随机访问负载,开大页后 TLB 缺失率能降一个数量级,性能提升 5%–15% 常见。代价是内存碎片和分配延迟,容器/虚拟化环境里要评估。
TLB shootdown:多核的甜蜜烦恼
多核系统里,每个核有自己的 TLB。操作系统改了页表(比如 munmap、mprotect、进程切换改 CR3),其他核 TLB 里可能还留着旧映射——过期映射是灾难(安全漏洞/数据错乱)。于是内核要给相关核发核间中断(IPI),让它们刷掉对应的 TLB 项,这叫 TLB shootdown。
shootdown 是昂贵的:IPI 中断 + 目标核停下来刷 TLB,高频触发时能吃掉可观的 CPU。优化方向:批量操作合并 shootdown、进程绑定 NUMA 节点减少跨核、能用大页就用大页(项数少,刷得快)。
一句话总结:TLB 是"用小容量换翻译速度"的经典权衡;程序员能做的就是保持访问局部性(顺序、成块),系统层面用大页扩大 reach——虚拟内存的性能密码,基本就藏在这两条里。

