背景

folio_zero_user 在清零透明大页时,process_huge_pages() 为了最大化访问时的缓存局部性,以 4KB 页为单位、按半随机顺序清零:先清缺页位置外周的页(向前或向后),再逐步向内汇聚到缺页页。

┌──────────────────────┐
│ 清大页时半随机跳着清 │
└──────────┬───────────┘
           ▼
┌──────────────────────┐
│ 先清外周再向缺页汇聚 │  为缓存局部性,却让处理器难以预取
└──────────┬───────────┘
           ▼
┌──────────────────────┐
│ 受限于单 4KB 页带宽  │  基线约 12 GB/sec
└──────────────────────┘

这种顺序对处理器来说难以预测。虽然单页内的清零是连续的,但页与页之间的跳跃让硬件预取器帮不上忙。结果清零带宽被限制在单 4KB 页可用水平。作者在 AMD Genoa(EPYC 9J13)上实测基线约 11.79 GB/sec(约 323ns/4KB),内存延迟约 100ns,留给硬件预取器的余量很小。

问题

  • 清大页时半随机跳着清(先外周后内汇聚)
  • 页间跳跃让处理器难以预取
  • 受限于单 4KB 页清零带宽
  • 基线约12 GB/sec

方案

改动很直接:不再非连续清页,改为按地址顺序连续清。

process_huge_pages 的半随机清零切换到 clear_user_highpage() 的顺序循环。

旧:半随机跳着清
┌──────────────────────┐
│ 页的顺序处理器难预测 │
└──────────┬───────────┘
           ▼
┌──────────────────────┐
│  受限于单页清零带宽  │
└──────────────────────┘

新:按地址顺序清
┌────────────────────────────┐
│ 连续清页,预取器能有效工作 │
└─────────────┬──────────────┘
              ▼
┌────────────────────────────┐
│ 缓存未命大幅下降,带宽翻倍 │  1GB 页本就顺序清,不受影响
└────────────────────────────┘

连续清零让处理器的 L2 预取器能有效工作:cache-miss 率从 10.96% 降到 0.73%,预取的 cacheline 增加约 22%。2MB 页的清零带宽因此翻倍。在核心的连续清零之外,配套还有按范围清零页(而非整个大页),以及缓存相邻页以减少重复清零;这套改动立在 treewide / x86 / highmem 的 clear_pages() / clear_user_pages() 基础设施之上,那部分是前置铺路。

收益

作者在 AMD Genoa(EPYC 9J13)上测试(64GB region-size,local node,2.56 GHz,boost=0):

指标 Before After 改善
2MB 页清零带宽 11.76 GB/s 23.58 GB/s +100.51%(翻倍)
cache-miss 率 10.96% 0.73% -93.3%
1GB 页清零带宽 24.85 GB/s 25.40 GB/s ~持平(本就顺序清)

2MB 页的清零带宽从 11.76 提升到 23.58 GB/sec,翻倍有余。cache-miss 率从近 11% 降到不到 1%,说明顺序清零让预取器充分发挥了作用。1GB 页此前本就用直接清零(顺序),所以不受影响。