当前位置:首页>Linux>我把 Linux 页大小从 4KB 改成 2MB 后,性能开始离谱了

我把 Linux 页大小从 4KB 改成 2MB 后,性能开始离谱了

  • 2026-10-11 07:33:12
我把 Linux 页大小从 4KB 改成 2MB 后,性能开始离谱了

在上一篇文章CPU 真正怕的不是计算,而是“找数据”中,我们深入探讨了虚拟内存转换的底层逻辑,以及为什么 TLB 缓存缺失(TLB Miss)会对程序性能造成那么大的影响。

今天,我们来聊一个真正能改变内存访问性能的重量级优化:Huge Pages(大页内存)。

第一次接触这个,是在浏览知乎某篇文章的时候,而那时,我恰恰也在做一个DMP系统,里面存储着大量的数据,所以就将这个技巧应用了起来。后来逐渐了解到,它是很多高频交易系统、数据库、搜索引擎、内存缓存系统、DPDK、Redis、ClickHouse、JVM、大模型推理框架背后的核心优化之一。

在 64 位 x86 架构和 Linux 操作系统中,默认的物理内存是以 4KiB 为一个“页(Page)”进行切块管理的。也就是说,每次你的程序访问一个变量,CPU 都得通过多级页表(通常是 4 级页表,在现代 5 级页表下更甚)进行一次漫长的“页表漫游(Page Walk)”。

虽然 CPU 准备了 TLB(块表) 来缓存这些转换结果,但硬件的资源是极其奢侈且有限的。据了解,一个现代服务器 CPU 核心的 TLB 缓存槽位,满打满算也就几千个。

我们假设每个核的TLB可以缓存 2000 条记录,那么,在默认4KB大小的页面下:

覆盖范围

这意味着,一旦你的程序需要频繁读写的数据总量超过了 8MiB,TLB 就会开始疯狂地发生换入换出,而当前很多业务比如redis、RocksDB、ClickHouse以及大型数据存储数据量往往几十G甚至上T,因此,CPU 大量时间其实都浪费在找数据。

如果,我们将页大小改成2MB,那么:

覆盖范围

如果换成更大的页面,这个数字还会成倍飙升。

显然,通过切换到 2MiB 大页,单个 TLB 条目覆盖的内存直接暴涨了 512 倍!它不仅降低了页表的嵌套深度,更让TLB 能够轻松锁住数个 GB 的热内存。

通过设置大页的方式,不仅可以降低TLB miss,而且,降低 Page Walk 深度,使得页表层级减少,进而减少cache miss等。同时,降低页表开销:假设我们有1T内存,使用4KB页大小,那么页表本身都可能占用数GB。还可以提升CPU Prefetch。

在真正进入大页之前,我们要明白一个大前提:操作系统必须在物理内存中找到 512 个连续的 4KiB 物理页,才能拼凑出一个 2MiB 的大页。这在内存碎片严重的系统里可不是件容易事。

在 Linux 中大页分为两种:HugeTLBfs 和 Transparent Huge pages( 透明大页 ) 。

其中,Transparent Huge pages( 透明大页 )是 Linux 在十多年前引入的自动化机制。内核后台跑着一个叫 khugepaged 的守护进程,它会在合适的时候,偷偷摸摸地把普通的 4KiB 页合并升级为大页。它有两种模式:

  • • always 模式: 只要有机会,内核就会到处尝试用大页。听起来很省心,但你彻底失去了控制权。
  • • madvise 模式: 只有当明确用代码打上标记的内存区域,内核才会去尝试合并。

HugeTLBfs是一种更硬核、更受低延迟团队偏爱的机制,也是比较常用的方式。它在系统启动或初始化时,直接从内核划出一块专属的“大页池”。它要求应用程序开发者修改代码才能使用大页,例如,在 mmap() 中使用 MAP_HUGETLB 标志,或者在 shmget() 中使用 SHM_HUGETLB 标志。

下面是一个使用示例:

#include <iostream>#include <string_view>#include <system_error>#include <sys/mman.h>#include <unistd.h>namespace {constexpr std::size_t kHugePageSize = 2 * 1024 * 1024;class MMapRegion {public:    MMapRegion(std::size_t size, int prot, int flags)        : size_(size) {        ptr_ = mmap(nullptr, size_, prot, flags, -1, 0);        if (ptr_ == MAP_FAILED) {            ptr_ = nullptr;            throw std::system_error(                errno,                std::generic_category(),                "mmap failed");        }    }    ~MMapRegion() {        if (ptr_) {            munmap(ptr_, size_);        }    }    MMapRegion(const MMapRegion&) = delete;    MMapRegion& operator=(const MMapRegion&) = delete;    MMapRegion(MMapRegion&& other) noexcept        : ptr_(other.ptr_),          size_(other.size_) {        other.ptr_ = nullptr;        other.size_ = 0;    }    MMapRegion& operator=(MMapRegion&& other) noexcept {        if (this != &other) {            if (ptr_) {                munmap(ptr_, size_);            }            ptr_ = other.ptr_;            size_ = other.size_;            other.ptr_ = nullptr;            other.size_ = 0;        }        return *this;    }void* data() const noexcept{        return ptr_;    }private:    void* ptr_{nullptr};    std::size_t size_{0};};void allocate_hugetlbfs(){    std::cout << "\n[Hugetlbfs]\n";    try {        MMapRegion region(            kHugePageSize,            PROT_READ | PROT_WRITE,            MAP_PRIVATE | MAP_ANONYMOUS | MAP_HUGETLB);        std::cout            << "[+] Successfully allocated 2MiB huge page via hugetlbfs\n";    }    catch (const std::system_error& e) {        std::cerr            << "[-] Hugetlbfs allocation failed: "            << e.code().message()            << '\n';    }}void allocate_thp_madvise(){    std::cout << "\n[Transparent Huge Pages]\n";    try {        MMapRegion region(            kHugePageSize,            PROT_READ | PROT_WRITE,            MAP_PRIVATE | MAP_ANONYMOUS);        if (madvise(region.data(),                    kHugePageSize,                    MADV_HUGEPAGE) != 0) {            throw std::system_error(                errno,                std::generic_category(),                "madvise(MADV_HUGEPAGE) failed");        }        std::cout            << "[+] MADV_HUGEPAGE successfully applied\n";    }    catch (const std::system_error& e) {        std::cerr            << "[-] THP setup failed: "            << e.code().message()            << '\n';    }}} // namespaceint main(){    allocate_hugetlbfs();    allocate_thp_madvise();    return 0;}

最后,我们需要说明的是,大页(Huge Pages)不是万能的,对于那些高吞吐、动辄吞吐数十 GiB 数据的内存密集型低延迟应用来说,绝对是性能提升利器。

在使用一项技术优化性能的时候,记住那句老话:多用 perf 去审视硬件,选择最适合你业务的分配策略,永远不要用直觉去代替测量。

以上

如果对本文有疑问可以加笔者微信直接交流,笔者也建了C/C++相关的技术群,有兴趣的可以联系笔者加群。

推荐阅读点击标题可跳转

1、CPU 真正怕的不是计算,而是“找数据”

2、你以为 write() 在写文件,其实它什么都没写

3、你写下一个 write() 时,Linux 内核到底发生了什么

最新文章

随机文章