巨型页可以增加每条地址翻译缓存能够覆盖的内存范围,在合适负载下减少翻译开销。但效果取决于访问模式和硬件,不能仅因为申请了很大一块内存,就断定程序频繁发生 TLB 缺失。描述工作集需要的页数减少,也不表示所有翻译能够同时装入处理器的 TLB;页数算术、硬件容量与真实性能是三个不同问题。

页大小是一项输入

x86 常见基准页为 4 KiB,但 Linux 的页大小依赖架构与配置,应读取运行系统而不是把四千多字节当作普遍常量。支持的巨型页尺寸同样会变化;2 MiB 与 1 GiB 是常见例子,不是所有处理器的完整列表。跨机器比较时,应先确认单位和实际尺寸,再比较统计数字。

对理想化的连续 8 GiB 范围,4 KiB 粒度需要 2,097,152 个页面,2 MiB 粒度只需 4,096 个。这只是映射覆盖率计算,没有测量硬件 TLB 容量、缺失率、页表遍历延迟,或同时驻留的翻译数量。不同页面大小对应的翻译缓存结构还可能具有不同容量,因此不能把两种页数之差直接称为实际节省的 TLB 项。

显式池与透明分配

Hugetlb 提供显式申请的大页,并由内核维护相应池。持久池中的页面不能同时用于普通分配,也不能在内存压力下换出。池可以在启动时准备,也能在运行中调整,还可能允许额外的 surplus 页面。启动较早时通常更容易获得连续内存,但这并不表示大页只能一次性在开机前决定数量,之后永远无法改变。

透明巨型页采用另一套机制。依内核支持和政策,较大的匿名内存分配既可能发生在缺页时,也可能由后续合并产生。THP 不只是后台线程,也不要求管理员先准备 hugetlb 池。现代内核还可以支持多种 THP 尺寸,不能把每次透明分配都等同于一个两兆字节映射,更不能用 hugetlb 总数代替 THP 使用量。

灵活性也不同。THP 可以退回较小页面,并可为回收等操作拆分。显式 hugetlb 映射不会自动变成普通页映射,但应用在自身配置允许时,可以在失败后另行选择普通页分配。应区分内核映射契约与应用的回退策略,否则会把“必须使用大页”的应用和“优先尝试大页”的应用误写成同一种启动行为。

分别预算容量与位置

在 /proc/meminfo 中,HugePages_Total 表示默认尺寸池的页数,HugePages_Free 是尚未分配的页数,HugePages_Rsvd 是已作出承诺但还没有在缺页时分配的页数。总页数乘页大小得到池容量,不是 Rsvd 所表达的已承诺容量。看起来空闲的池中可能已有一部分被预订,不能把 Free 全部当成任何新申请都能使用的余量。Hugetlb 则统计各种尺寸池占用的总内存,细节应查看分尺寸 sysfs 目录。

稀疏映射可能消耗超过有用数据的常驻内存,池也会与普通内存需求竞争。应按应用文档估算容量,核对实际映射,并给其他分配保留空间。收益需要通过代表性吞吐、延迟及翻译相关测量建立,不能因为配置的大页池足够大就宣布优化已经生效;申请、映射、使用与性能改善是不同阶段。

坏掉的样子

扩大池时,内核可能找不到足够连续的物理内存,因而失败。但已经存在的空闲巨型页本来就是连续页,Free 大于零本身并不是碎片证据。页尺寸不匹配、已有预订、权限、NUMA 位置或 cgroup 限制,也可能解释为什么某个应用用不到看似可用的容量。应按具体失败路径调查,而不是看到内存不足就统一建议重启或重复 mmap。

NUMA 政策可能使页面远离使用它们的 CPU,或者只允许从部分节点分配。应检查分节点分布和访问局部性,再判断是否为地址翻译瓶颈。TLB 改善可以与更差的内存访问延迟同时存在;全局空闲数正常、应用能够启动,都不能证明放置策略合适,也不能推导每次访问一定走本地内存。

THP 分配、合并、整理和拆分会以不同方式影响延迟。thp_collapse_alloc 是合并过程中成功分配的计数,不是停顿时间计数。应将合适的计数增量与延迟共同观察。选择 madvise、always 或当前内核支持的分尺寸政策,需要真实负载依据,不能给所有数据库套用同一个模式,再把模式名称当作性能保证。

8 GiB 页数计算与 TLB 性能分开;hugetlb 池与 THP 不同;Total、Free、Rsvd 各有含义,NUMA 放置需要独立证据。 未为本图运行文章示例。
8 GiB 页数比较属于算术;hugetlb 预留与 THP 行为须分别解释。

快速验证

保存为 hugepage_check.py,运行 python3 hugepage_check.py。程序只读取计数和基准页大小,不创建大页工作负载,也不修改内核设置。缺少数据时明确打印跳过信息,并把假设值标成仅供算术演示。解析与边界断言使用合成字符串,既检查正常输入,也检查单位错误和零页大小,不会把无效数据悄悄解释为真实容量。

import os
import re
from pathlib import Path

WORKING_SET = 8 * 1024**3
FIELDS = ("HugePages_Total", "HugePages_Free", "HugePages_Rsvd",
          "HugePages_Surp", "Hugepagesize")


def parse_meminfo(text):
    values = {}
    for key in FIELDS:
        unit = r"\s+kB" if key == "Hugepagesize" else ""
        match = re.search(r"^" + key + r":\s+(\d+)" + unit + r"\s*$", text, re.M)
        if match:
            values[key] = int(match.group(1))
    return values


def pages_for(size, page):
    if size < 0 or page <= 0:
        raise ValueError("invalid byte count or page size")
    return (size + page - 1) // page


assert pages_for(4097, 4096) == 2
assert parse_meminfo("HugePages_Total: 8\nHugepagesize: 2048 kB\n")["Hugepagesize"] == 2048
assert parse_meminfo("HugePages_Total: bad\nHugepagesize: 2 MB\n") == {}
try:
    pages_for(1, 0)
except ValueError:
    pass
else:
    raise AssertionError("zero page size accepted")

try:
    values = parse_meminfo(Path("/proc/meminfo").read_text())
except OSError as error:
    values = {}
    print("SKIP live hugetlb counters:", type(error).__name__)
try:
    base = os.sysconf("SC_PAGE_SIZE")
    assert base > 0
    print("Observed base page size:", base, "bytes")
except (AttributeError, ValueError, OSError):
    base = 4096
    print("Assumed illustration base page size:", base, "bytes")

if all(key in values for key in FIELDS) and values["Hugepagesize"] > 0:
    huge = values["Hugepagesize"] * 1024
    print("Default-size hugetlb pool counters:", values)
    print("Total pool capacity:", values["HugePages_Total"] * huge, "bytes")
    print("Reserved but not allocated:", values["HugePages_Rsvd"] * huge, "bytes")
else:
    huge = 2 * 1024**2
    print("SKIP complete pool report; assuming 2 MiB for arithmetic only")
print("8 GiB page-count illustration:", pages_for(WORKING_SET, base),
      "base pages versus", pages_for(WORKING_SET, huge), "large pages")
print("No TLB miss rate, THP coverage, allocation success, or latency was measured")

若基准页为 4 KiB、默认大页为 2 MiB,预期看到前述两组页数。池总数为零是一种有效观察,不证明内核不支持大页。修订脚本已在 Linux、Python 3.12.14 下执行通过。它没有测量 THP 覆盖率、真实分配成功率或性能;这些结论需要另外设计与工作负载对应的实验,不能从一份只读容量报告直接推出。

延伸阅读

来源