1985年,英特尔发布386处理器时,4KB的页面大小是一个合理的选择。那时候一台电脑的内存不过几兆字节,4KB页面既能保证内存利用率,又不会给页表带来太大压力。四十年过去了,服务器内存已经从兆字节增长到太字节,增长了百万倍,但页面大小依然是4KB。这个遗留设计正在成为高性能系统的隐形瓶颈。

TLB(Translation Lookaside Buffer)是CPU内部的一个小型缓存,专门存储虚拟地址到物理地址的映射关系。每次CPU访问内存,都需要先完成地址翻译,而TLB的存在避免了对内存中页表的频繁查询。问题在于,TLB的容量非常有限。以AMD Zen 4架构为例,L1数据TLB只有72个条目,L2 TLB有3072个条目。使用4KB页面时,L2 TLB最多只能覆盖约12MB的内存。当一个进程的工作集超过这个范围,TLB缺失就会急剧上升,每次缺失都需要进行页表遍历,代价是10到100个CPU周期。

graph TD
    A[CPU访问内存] --> B{TLB查询}
    B -->|命中| C[获取物理地址]
    B -->|缺失| D[页表遍历]
    D --> E[PGD查询]
    E --> F[PUD查询]
    F --> G[PMD查询]
    G --> H[PTE查询]
    H --> I[获取物理地址]
    I --> J[更新TLB]
    C --> K[访问内存数据]
    J --> K
    
    style B fill:#f9f,stroke:#333
    style C fill:#9f9,stroke:#333
    style D fill:#f99,stroke:#333

x86_64架构采用多级页表结构来管理地址翻译。传统的4级页表将48位虚拟地址分成四个9位的索引和一个12位的页内偏移。每次TLB缺失,内存管理单元(MMU)需要依次访问PGD、PUD、PMD、PTE四个层级,最坏情况下需要四次内存访问才能完成一次地址翻译。现代CPU虽然引入了Paging Structure Cache来缓存上层页表,但最后一层的PTE查询仍然无法避免。从Skylake开始,Intel引入了5级页表支持,虚拟地址位数扩展到57位,支持超过128PB的地址空间,但代价是又增加了一层遍历开销。

JabPerf的实测数据显示,5级页表确实会带来可测量的开销。单线程页面错误延迟从4级页表的105.637纳秒上升到5级页表的105.88纳秒,虽然差异只有约0.25纳秒,但统计学上是显著的。在多线程场景下,差距更为明显,因为页表更新时上层目录的spinlock竞争会加剧。这解释了为什么高性能交易系统普遍选择禁用5级页表——即使机器内存远小于64TB,也要避免这个"自动赠送"的额外开销。

大页内存的工作原理

大页内存的核心思想非常简单:用更大的页面大小来扩展TLB的覆盖范围。x86_64架构支持三种页面大小:标准的4KB、大页2MB和巨页1GB。当使用2MB页面时,同样的3072个TLB条目可以覆盖6GB内存;使用1GB页面时,覆盖范围更是扩展到3TB。

graph LR
    subgraph 4KB页面
    A1[TLB条目1] --> B1[4KB]
    A2[TLB条目2] --> B2[4KB]
    A3[...] --> B3[...]
    A4[TLB条目N] --> B4[4KB]
    end
    
    subgraph 2MB大页
    C1[TLB条目1] --> D1[2MB]
    C2[TLB条目2] --> D2[2MB]
    C3[...] --> D3[...]
    C4[TLB条目N] --> D4[2MB]
    end
    
    subgraph 1GB巨页
    E1[TLB条目1] --> F1[1GB]
    E2[TLB条目2] --> F2[1GB]
    E3[...] --> F3[...]
    E4[TLB条目N] --> F4[1GB]
    end
    
    style B1 fill:#fcc
    style D1 fill:#cfc
    style F1 fill:#ccf

Hudson River Trading的性能工程师做过一个非常直观的实验:分配4GB内存,然后随机读取其中的uint64值。在Intel Tiger Lake处理器上,使用2MB大页比4KB页面快2.9倍,使用1GB巨页则快3.1倍。这个差距主要来自于TLB缺失率的差异。使用4KB页面时,4GB的工作集远远超过了TLB的覆盖能力,几乎每次内存访问都会触发TLB缺失;而使用大页后,同样的TLB条目可以覆盖更大的内存范围,TLB命中率大幅提升。

Google在2021年发表的TCMalloc论文提供了更大规模的实证数据。他们让内存分配器感知大页,自动将内存对齐到2MB边界,结果在整个Google服务器集群上实现了7%的平均吞吐量提升。考虑到Google的服务器规模,这个数字换算成实际的计算能力和能源节省是惊人的。

大页内存的实现方式有两种:Transparent Huge Pages(THP)和Static Huge Pages。THP是Linux内核的一个特性,内核会在后台自动将连续的4KB页面合并成2MB大页,对应用程序完全透明。Static Huge Pages则需要在系统启动时预留,应用程序需要显式请求才能使用。

THP的优点是零代码修改、自动管理,但缺点也很明显。内核的khugepaged守护进程需要定期扫描内存,寻找可以合并的页面,这个过程会消耗CPU资源。更严重的是,当内存碎片化严重时,khugepaged可能无法找到足够的连续物理页面,反而会触发内存压缩,导致延迟飙升。历史上,THP刚引入时默认开启,造成了很多性能问题,Redis、MySQL、MongoDB等数据库都曾深受其害。最终Linux内核将默认策略从"always"改成了"madvise",只有应用程序明确请求时才使用大页。

Static Huge Pages虽然需要手动配置,但提供了更可控的性能。在系统启动时,通过内核参数hugepagesz=2M hugepages=1024预留1024个2MB大页,这些页面会被锁定,不会被其他进程使用。应用程序通过mmapMAP_HUGETLB标志或shmgetSHM_HUGETLB标志来申请使用。PostgreSQL、Oracle、MySQL等数据库都支持这种方式。

量化的性能影响

TLB缺失的代价到底有多大?这个问题可以从两个角度回答:理论分析和实测数据。

从理论上说,一次TLB缺失需要完成页表遍历。在4级页表结构下,最坏情况需要4次内存访问。假设每次内存访问的延迟是100纳秒(DDR4的典型值),那么一次页表遍历需要400纳秒。实际上,现代CPU有Paging Structure Cache,可以缓存上层页表,所以典型延迟会更低。LWN上的一篇文章测量了Skylake处理器的TLB缺失延迟,使用oprofile工具测得平均约19个时钟周期,约8.8纳秒。这个数字看起来不大,但要考虑到CPU的执行频率——一个3GHz的CPU每秒执行30亿次操作,每纳秒可以完成3次操作。8.8纳秒的延迟意味着近30次操作的时间被浪费了。

sequenceDiagram
    participant App as 应用程序
    participant CPU as CPU核心
    participant TLB as TLB
    participant MMU as 内存管理单元
    participant RAM as 内存
    
    App->>CPU: 访问虚拟地址0x7f1234567890
    CPU->>TLB: 查询地址映射
    TLB-->>CPU: 缺失
    CPU->>MMU: 触发页表遍历
    MMU->>RAM: 读取PGD条目
    RAM-->>MMU: 返回PUD地址
    MMU->>RAM: 读取PUD条目
    RAM-->>MMU: 返回PMD地址
    MMU->>RAM: 读取PMD条目
    RAM-->>MMU: 返回PTE地址
    MMU->>RAM: 读取PTE条目
    RAM-->>MMU: 返回物理地址
    MMU->>TLB: 更新缓存
    MMU-->>CPU: 返回物理地址
    CPU->>RAM: 访问物理地址
    RAM-->>CPU: 返回数据
    CPU-->>App: 完成访问
    
    Note over TLB,RAM: 4KB页面: 最坏4次内存访问
    Note over TLB,RAM: 2MB大页: 最坏3次内存访问
    Note over TLB,RAM: 1GB巨页: 最坏2次内存访问

实测数据更能说明问题。Anshad Ameen在文章中总结了不同工作负载下的大页性能提升:MySQL在NUMA优化和大页配置下可以获得2-3倍的性能提升;Redis使用大页后操作延迟降低15-30%;PostgreSQL的查询执行时间改善15-25%。这些数字的差异反映了不同应用的内存访问模式:内存访问越随机、工作集越大,大页的收益越明显。

Red Hat的工程师在虚拟机场景下做了详细的基准测试。他们在128GB内存的AMD Threadripper主机上运行两个12GB内存的虚拟机,一个使用THP,另一个使用1GB静态大页。在Sysbench内存测试中,1GB大页虚拟机比THP虚拟机快2.4%。在GDB编译测试中,快1.1%。差距看起来不大,但对于持续运行的构建服务器来说,每天可以多完成7次编译,积少成多就是显著的效率提升。

TLB Shootdown:隐藏的性能杀手

大页内存并非没有代价。其中一个容易被忽视的问题是TLB shootdown。

当内核修改页表(比如释放内存)时,必须确保所有CPU核心的TLB都不再持有旧的映射。由于TLB是每个核心私有的,没有硬件的一致性机制,内核必须通过软件方式来同步:向其他核心发送处理器间中断(IPI),让它们主动刷新TLB。这个过程叫做TLB shootdown。

stateDiagram-v2
    [*] --> 运行中
    运行中 --> 接收IPI: 其他核心释放内存
    接收IPI --> 暂停执行: IPI中断
    暂停执行 --> 刷新TLB: 执行中断处理
    刷新TLB --> 恢复执行
    恢复执行 --> 运行中
    
    note right of 接收IPI: 代价:暂停当前工作
    note right of 刷新TLB: 代价:丢失有用的TLB条目

GitHub上的一个研究项目详细测量了TLB shootdown的影响。实验设置是这样的:一个线程在CPU A上持续写入一块内存,另一个线程在CPU B上释放另一块不相关的内存。结果显示,当CPU B释放内存后,CPU A上的写入延迟瞬间翻倍。原因是CPU B在释放内存时触发了TLB shootdown,向CPU A发送了IPI,CPU A不得不暂停当前工作来处理中断。

这个实验揭示了一个反直觉的事实:同一个进程内的线程,即使各自操作完全独立的内存区域,也会因为TLB shootdown相互干扰。对于使用大页的应用来说,这个问题更加严重,因为一个大页覆盖的内存范围更大,释放一个大页需要刷新的范围也更广。

Linux内核的开发者一直在努力优化这个问题。2025年初的一个补丁提出,将TLB刷新操作从逐页批量改为按PMD批量,一次IPI可以处理整个2MB范围的刷新,而不是逐页发送。对于频繁释放内存的工作负载,这可以显著减少中断数量。

NUMA架构下的额外考量

现代多路服务器都是NUMA架构,每个CPU插槽有自己的本地内存,访问其他插槽的内存延迟更高。在这种环境下使用大页,需要额外注意内存的位置。

NUMA系统的内存访问延迟差异显著:本地内存访问约100-200纳秒,远程内存访问约200-400纳秒。如果一个进程的线程在CPU A上运行,但分配的内存在CPU B的节点上,每次内存访问都要跨插槽,性能损失可能抵消大页带来的所有收益。

正确的做法是配合NUMA策略使用大页。Linux提供了多种NUMA内存分配策略:MPOL_BIND将内存绑定到特定节点,MPOL_INTERLEAVE将内存分散到所有节点,MPOL_PREFERRED优先使用指定节点。对于数据库这类工作负载,通常选择MPOL_BIND,将Buffer Pool绑定到与CPU同节点的内存上。

graph TD
    subgraph NUMA节点0
        CPU0[CPU Socket 0]
        MEM0[本地内存<br/>大页池]
    end
    
    subgraph NUMA节点1
        CPU1[CPU Socket 1]
        MEM1[本地内存<br/>大页池]
    end
    
    CPU0 -->|快速访问| MEM0
    CPU1 -->|快速访问| MEM1
    CPU0 -.->|慢速访问<br/>跨QPI/UPI| MEM1
    CPU1 -.->|慢速访问<br/>跨QPI/UPI| MEM0
    
    style CPU0 fill:#9cf
    style CPU1 fill:#9cf
    style MEM0 fill:#fc9
    style MEM1 fill:#fc9

大页的预留也需要考虑NUMA拓扑。Linux内核支持在特定NUMA节点上预留大页:echo 1024 > /sys/devices/system/node/node0/hugepages/hugepages-2048kB/nr_hugepages。这样可以确保每个节点有足够的大页供本地进程使用。

MySQL的InnoDB提供了NUMA相关的配置选项。innodb_numa_interleave选项会让Buffer Pool使用交错分配,数据均匀分布在所有节点上。但这不一定是最优选择——对于点查询为主的工作负载,将数据和查询线程绑定到同一节点可能更好。

不同CPU架构的页表支持

大页并非x86独有,不同CPU架构对页面大小的支持各有特点。

ARM64架构支持可配置的页面大小:4KB、16KB和64KB。这在系统启动时由内核配置决定。Ampere的文档指出,使用64KB页面时,同样48个L1 TLB条目可以覆盖3MB内存(而不是4KB页面的192KB),1280个L2 TLB条目可以覆盖80MB(而不是5MB)。对于数据库、虚拟化、AI推理等内存密集型工作负载,64KB页面可以带来可观的性能提升。代价是内存利用率:存储7KB数据时,4KB页面需要8KB(87.5%利用率),64KB页面需要64KB(11%利用率)。

RISC-V架构通过Svnapot扩展支持64KB页面,类似于ARM的Contiguous Hint机制。但由于RISC-V生态相对年轻,软件支持还不如ARM成熟。

架构差异带来了软件移植的挑战。很多软件假设页面大小是4KB,这在x86上是成立的,但在ARM64默认使用64KB页面的系统上会出问题。Go语言运行时、.NET、Chrome、jemalloc都曾因此出现兼容性问题。这也是为什么大多数Linux发行版在ARM64上默认使用4KB页面,尽管64KB页面的性能更好。

graph LR
    subgraph x86_64
        X1[4KB - 默认]
        X2[2MB - 大页]
        X3[1GB - 巨页]
    end
    
    subgraph ARM64
        A1[4KB - 可选]
        A2[16KB - 可选]
        A3[64KB - 可选]
        A4[2MB - 大页]
        A5[1GB - 巨页]
    end
    
    subgraph RISC-V
        R1[4KB - 默认]
        R2[2MB - 大页]
        R3[1GB - 巨页]
        R4[64KB - Svnapot扩展]
    end

实战配置指南

了解了原理和权衡后,如何在生产环境中正确配置大页?

对于数据库服务器,推荐使用静态大页。PostgreSQL的配置最为直接:在postgresql.conf中设置huge_pages = on,然后确保系统预留了足够的大页。计算大页数量:(shared_buffers + 预留) / 2MB。例如,32GB的shared_buffers需要约16384个2MB大页。在/etc/default/grub中添加hugepagesz=2M hugepages=16384,然后更新grub并重启。

MySQL的配置稍复杂,因为InnoDB的Buffer Pool默认不使用大页。需要设置innodb_use_sys_malloc = 0large-pages = 1来启用。Oracle数据库则更直接,只要系统预留了大页,SGA就会自动使用。

对于Redis这类内存数据库,THP通常是更好的选择。Redis的内存访问模式相对随机,工作集大小动态变化,静态大页可能导致内存浪费。在/sys/kernel/mm/transparent_hugepage/enabled中设置为madvise,让Redis通过madvise(MADV_HUGEPAGE)自行决定哪些内存区域使用大页。

虚拟机场景下,1GB巨页能提供更稳定的性能。但要注意,1GB巨页的分配对内存连续性要求极高,通常需要在系统启动时预留。运行时动态分配几乎不可能成功,因为内存早已碎片化。

大模型推理框架vLLM默认就会尝试使用大页。PagedAttention机制管理KV Cache的方式与大页的内存对齐天然契合。如果系统没有预留大页,vLLM会自动降级到4KB页面,但性能会有损失。在部署大模型推理服务时,建议预留足够的大页,并监控/proc/meminfo中的HugePages_TotalHugePages_Free指标。

监控大页使用情况是运维的重要部分。/proc/meminfo提供了基本指标:HugePages_Total是系统预留的大页总数,HugePages_Free是未分配的数量,HugePages_Rsvd是被进程预订但尚未使用的数量。注意HugePages_Free高并不一定表示浪费,因为这些页面是锁定的,不会被其他用途占用。真正要关注的是HugePages_Rsvd是否持续为零,这表示大页供不应求。

何时不要使用大页

大页不是万能药,有些场景下使用大页反而有害。

内存碎片化严重的系统不适合使用THP。khugepaged尝试合并页面时会触发内存压缩,这个过程会锁定内存、移动页面,导致不可预测的延迟尖峰。如果系统已经运行了很长时间,内存碎片化严重,建议关闭THP或者重启系统后再开启。

工作集远小于TLB覆盖范围的应用也用不上大页。如果一个应用的活跃数据只有几兆字节,使用4KB页面完全可以被L2 TLB覆盖,启用大页没有收益,反而增加管理开销。

延迟敏感型应用需要谨慎评估TLB shootdown的影响。如果一个线程频繁释放内存,会导致其他线程收到IPI中断,增加延迟抖动。对于高频交易系统,可能需要选择对象池等替代方案,避免频繁的内存分配和释放。

内存受限的环境不适合静态大页。预留的大页不能用于其他用途,如果预留过多但实际使用不足,会浪费宝贵的内存资源。在这种情况下,THP的按需分配模式更合适。

技术演进与未来方向

大页内存技术本身也在演进。Linux内核持续优化THP的实现,减少延迟尖峰。内存压缩算法不断改进,更高效地整理碎片。一个新的方向是异构内存管理:在DRAM、持久内存(PMEM)、高带宽内存(HBM)等不同介质上使用不同的页面大小,让数据放置更智能。

硬件方面,CPU的TLB也在增长。Intel的Sapphire Rapids处理器的L2 TLB已经有几千个条目,未来还会继续扩大。但这只是缓解问题,不能根本解决——内存容量的增长速度永远快于TLB容量。

另一个值得关注的方向是软件感知大页的内存分配器。Google的TCMalloc已经实现了,jemalloc和mimalloc也在跟进。当内存分配器自动将小对象打包到大页中,应用程序无需任何修改就能获得性能提升。这可能是大页技术走向普及的关键。

在云原生时代,大页的配置正变得越来越复杂。容器的内存限制、Kubernetes的资源配额、虚拟机的内存气球,都会影响大页的可用性。如何在多层虚拟化环境中正确传递大页资源,仍然是一个开放问题。

最后的权衡

大页内存本质上是空间换时间的权衡:用略低的内存利用率换取更少的TLB缺失和更快的内存访问。这个权衡在什么情况下划算,取决于具体的工作负载和硬件环境。

对于内存密集型的数据库、虚拟机、AI推理服务,大页几乎总是值得的。实测数据表明,5-30%的性能提升是可预期的。对于小工作集、低内存压力的应用,大页的收益有限,维护成本可能超过收益。

配置大页不是一劳永逸的操作。需要监控系统指标,调整预留数量,评估THP的延迟影响,平衡NUMA布局。这是一项需要持续关注的基础设施优化工作。

四十年前的4KB页面大小决定,在今天看来确实是一个需要优化的问题。但优化不是无脑开启所有开关,而是理解底层原理,测量实际效果,在特定场景下做出合理的选择。大页内存只是一个工具,用好它需要技术功底和工程经验。


参考资料

  1. Hudson River Trading. “Huge Pages.” https://www.hudsonrivertrading.com/hftchat/huge-pages/
  2. LWN. “How fast is the TLB?” https://lwn.net/Articles/379735/
  3. JabPerf. “5-level vs 4-level Page Tables: Does It Matter?” https://www.jabperf.com/5-level-vs-4-level-page-tables-does-it-matter/
  4. Red Hat Developer. “Benchmarking transparent versus 1GiB static huge page performance in Linux virtual machines.” https://developers.redhat.com/blog/2021/04/27/benchmarking-transparent-versus-1gib-static-huge-page-performance-in-linux-virtual-machines
  5. Evan Jones. “Huge Pages Are a Good Idea.” https://www.evanjones.ca/hugepages-are-a-good-idea.html
  6. Anshad Ameen. “NUMA, Huge Pages, and Memory Compaction in Production Systems.” https://anshadameenza.com/blog/technology/2025-01-22-memory-management-numa-huge-pages-compaction/
  7. GitHub. “TLB Shootdowns.” https://github.com/bitcharmer/tlb_shootdowns
  8. Linux Kernel Documentation. “Transparent Hugepage Support.” https://docs.kernel.org/admin-guide/mm/transhuge.html
  9. Ampere Computing. “Understanding Memory Page Sizes on Arm64.” https://amperecomputing.com/tuning-guides/understanding-memory-page-sizes-on-arm64
  10. Oracle Blogs. “Minimizing struct page overhead.” https://blogs.oracle.com/linux/minimizing-struct-page-overhead
  11. USENIX ATC. “Optimizing the TLB Shootdown Algorithm with Page Access Tracking.” https://www.usenix.org/conference/atc17/presentation/amit
  12. Intel. “Intel 64 and IA-32 Architectures Software Developer’s Manual.”
  13. Phoronix. “Very Promising Linux Patch Optimizes TLB Flushes.” https://www.phoronix.com/news/Linux-Optimize-TLB-Flush-Reclaim