背景

可执行文件映射(VM_EXEC)的缺页走 do_sync_mmap_readahead,本有望用 large folio 预读一次性搬入更多页、削减指令 TLB 未命中。但这条路径上有两道约束卡住了 large folio 预读,且都在 arm64 64K base page 配置下最致命:那里 HPAGE_PMD_ORDER 为 13(512MB),超出 MAX_PAGECACHE_ORDER(11);fault_around_pages 又塌缩为 1,使 should_fault_around 失效。

┌───────────────────────────────────┐
│      可执行映射缺页触发预读       │
└─────────────────┬─────────────────┘
                  ▼
┌───────────────────────────────────┐  该计数为节流投机预读而设,却管到了有针对
│      受投机预读节流计数约束       │  性的可执行预读
└─────────────────┬─────────────────┘
                  ▼
┌───────────────────────────────────┐  PMD 阶超出页缓存阶上限的配置下,large folio
│ 受强制大页预读的 PMD 阶硬门槛约束 │  路径完全用不上
└─────────────────┬─────────────────┘
                  ▼
┌───────────────────────────────────┐  本该靠 large folio 降低指令 TLB 未命中,
│ 可执行内存拿不到 large folio 预读 │  却退回基页预读
└───────────────────────────────────┘

第一道是 mmap_miss 这个投机节流计数器。它本是为节流“投机”readahead(看着像随机访问就少预读)设计的,却被一并套到了 VM_EXEC readahead 上。可 exec readahead 是有针对性的,并非投机。计数器一旦超过禁用阈值,exec readahead(含 exec_folio_order 请求的 large folio 阶)就被掐断。更糟的是它只在缓存命中时才递减:异步预读,以及把已缓存页映射上时。而 fault_around 失效时这条命中路径根本走不到,计数器只增不减,exec readahead 在前 100 次缺页后就被永久禁用。

第二道是 force_thp_readahead 的 PMD-order 硬门槛。这条路径只在 HPAGE_PMD_ORDER <= MAX_PAGECACHE_ORDER 时启用,且恒以 HPAGE_PMD_ORDER 请求。arm64 64K base page 下 HPAGE_PMD_ORDER 为 13、超出上限,VM_HUGEPAGE 映射根本进不了这条路径,退回基页 readahead,哪怕该映射本支持远低于上限的有用 large folio。

问题

  • mmap_miss 投机节流被误套到有针对性的 exec readahead
  • fault_around 失效时计数器只增不减,exec readahead 被永久禁用
  • force_thp_readahead 的 PMD 阶门槛把超大 PMD 配置完全挡在门外
  • arm64 64K 大基页下 exec 内存退回基页预读,iTLB 未命中高企

方案

两道约束各由一个 patch 解开。

修复本身架构无关,只是恰好在 arm64 64K 上暴露得最明显。

约束一:可执行预读绕过投机节流计数
┌─────────────────────────────────────┐
│     可执行映射跳过该计数的增减      │
└──────────────────┬──────────────────┘
                   ▼
┌─────────────────────────────────────┐  可执行预读本就有针对性(VMA 限定、阶可
│ 可执行 large folio 预读不再被误判为 │  选、无后续预读),不该受投机节流管辖
│ 投机而禁用                          │
└─────────────────────────────────────┘

约束二:强制预读请求映射支持的 largest folio 阶
┌───────────────────────────────────────┐
│ 去掉 PMD 阶硬门槛,改请求映射 largest │
│ folio 阶                              │
└───────────────────┬───────────────────┘
                    ▼
┌───────────────────────────────────────┐  所得 folio 合并进单个 TLB 表项,降低
│  上限 2M,大基页下正好是连续 PTE 块   │  预读路径指令 TLB 压力;最终阶仍被钳制
└───────────────────────────────────────┘

让 exec readahead 绕过 mmap_miss

让 VM_EXEC 映射跳过 mmap_miss 的增减,与既有的 VM_SEQ_READ 待遇一致、保持计数对称。理由是 exec readahead 与普通 mmap read-around 本就不同:它被 VMA 限定、用 exec_folio_order 选对可执行映射有用的阶、async_size 置 0 不产生后续 readahead;若还有 VM_HUGEPAGE,那更是用户显式 opt-in 的大预读。把这些有针对性的预读当成"投机"去节流,是用错了启发式。绕过后,exec readahead 不再因计数器累积而被掐断。

forced 预读请求映射支持的 largest folio 阶

force_thp_readahead 的门槛从"PMD 阶不超页缓存上限"放宽为"映射支持 large folio",请求的阶也从写死的 HPAGE_PMD_ORDER 改为映射支持的 largest folio 阶,上限 2M。2M 这个上限有双重理由:它既是 x86_64 与 arm64 4K base page 的 PMD 大小,size 与内存压力的权衡已被充分理解;又正好是 arm64 16K/64K base page 的连续 PTE(contpte)块大小,所得 folio 会合并进单个 TLB 表项,直接削减预读路径的 TLB 压力。最终分配阶仍会被 page_cache_ra_order 按映射与请求几何钳制,不会失控。

两处合起来,让 exec 内存重新拿到本该属于它的 large folio 预读,大基页配置上受益最明显。

收益

作者自建 benchmark:mmap 一个大可执行文件、madvise 为 huge、在页偏移处调 RET-stub 函数。测试环境为 Neoverse V2(Grace)、arm64 64K base page、512MB 可执行文件、ext4,取 3 次平均。"Cold" 测缺页加预读开销;"Random" 先顺序 fault 全部页(不计),再测随机偏移执行,隔离分散执行的 iTLB 未命中开销:

阶段 基线 打补丁后 改善
Cold fault(缺页 + 预读开销) 83.4 ms 41.3 ms 快 50%
Random(随机偏移执行,隔离 iTLB 未命中) 76.0 ms 58.3 ms 快 23%

Cold fault 大幅改善来自 large folio 预读一次性搬入更多页、减少缺页与预读往返;Random 的改善则来自更多指令落进 large folio、iTLB 未命中减少。