背景

ZONE_DEVICE 内存(fsdax/devdax pmem、DEVICE_PRIVATE 等)的 struct page 数组,即 memmap,由 memmap_init_zone_device() 初始化:它逐 PFN 循环,对每个 struct page 描述符逐字段赋值。当区域很大(例如 100GB 的 namespace,对应上千万个 struct page),这趟初始化的开销就直接体现在 nd_pmem/dax_pmem 驱动的 bind 与 rebind 延迟上。

┌─────────────────────────────────────────┐
│ 大 ZONE_DEVICE 区域逐 PFN 初始化 memmap │
└────────────────────┬────────────────────┘
                     ▼
┌─────────────────────────────────────────┐
│       每个 struct page 逐字段赋值       │
└────────────────────┬────────────────────┘
                     ▼
┌─────────────────────────────────────────┐  写 1 次少复用的冷数据,污染 cache
│   cached 写把 memmap 行拉入数据 cache   │  又触发 write-allocate
└─────────────────────────────────────────┘

这里有 2 层浪费:

  • 相邻 page 的 struct page 描述符大多字段相同(pgmap、zone、nid、flags 等),真正随 PFN 变化的只有 section 位、page->virtual 这少数几项,逐字段赋值等于把同样的值一遍遍重写
  • 这些写都走普通 cached 路径:每写 1 个 cache 行就把 memmap 这段冷数据拉进数据 cache(write-allocate),把本来有用的热数据挤出去;而 memmap 是写 1 次、近期很少再读的数据,为它污染 cache 纯属白付代价

问题

  • 大 ZONE_DEVICE 区域逐 PFN 字段式初始化 memmap
  • 各 struct page 多字段相同仍逐字段重复赋值
  • cached 写触发 write-allocate 并污染数据 cache
  • memmap 写 1 次少复用,cache 代价纯属浪费

方案

2 层浪费对应 2 项机制:用模板复制消除重复赋值,用 non-temporal stores 绕过 cache。

旧:逐 PFN 字段式初始化
┌─────────────────────────────────────────┐
│     每个 PFN 单独初始化 struct page     │
└────────────────────┬────────────────────┘
                     ▼
┌─────────────────────────────────────────┐
│        逐字段赋值,多数字段重复         │
└────────────────────┬────────────────────┘
                     ▼
┌─────────────────────────────────────────┐
│ cached 写触发 write-allocate 污染 cache │
└─────────────────────────────────────────┘

新:模板复制配 non-temporal stores
┌──────────────────────────────────────┐
│    首页慢速初始化后播种可复用模板    │
└──────────────────┬───────────────────┘
                   ▼
┌──────────────────────────────────────┐
│     其余页仅刷差异字段再整块复制     │
└──────────────────┬───────────────────┘
                   ▼
┌──────────────────────────────────────┐
│ non-temporal stores 直达内存绕 cache │
└──────────────────────────────────────┘

模板 fast path 抓的是「字段大多相同」。memmap_init_zone_device 先用原有慢路径把第 1 个真实 struct page 初始化好,再把它 memcpy 进一个可复用的模板;从真实 page 取种,是因为既有的 page 初始化与 pageblock 辅助函数期待的是真正的 memmap 条目,而非栈上对象。此后每个 page 只需在模板里刷新随 PFN 变化的那几项(section 位、page->virtual),再把模板整块复制进目标 page,原本散在许多字段上的赋值收敛成 1 次结构化复制。compound tail page 用同样思路:从第 1 个 tail page 取种尾页模板,其余 tail 整块复制。

模板复制本身在 nd_pmem 场景带来约 30% 的改善,其余增益来自第 2 项。memmap 是写 1 次、低复用的访问模式,普通 cached 复制会带来 write-allocate 与 cache 污染。本系列为此新增通用接口 memcpy_nontemporal():请求 non-temporal store 把写入直达内存、绕过 cache;没有专用后端的架构以宏别名退化为普通 memcpy(),从而在调用点对象尺寸仍可见时保留 FORTIFY 覆盖。x86 后端复用现成的 memcpy_flushcache()(内部走 MOVNTI),并把它的 fixed-size fastpath 扩展到 32/48/64/80/96 字节,其中 64/80/96 正好匹配 struct page 的几种尺寸。把模板复制路径切到 memcpy_nontemporal() 本身增量有限(rebind 均时从约 155ms 量级到 150ms 量级),fixed-size fastpath 的内联 MOVNTI 生效后才降到 72ms 量级。这条路径不需要额外的 drain 排序:memcpy_nontemporal() 只作拷贝原语,memmap 初始化全程仍是同一 CPU 在 struct page 数组上的初始化序列,后续 compound 页设置等普通 store 并非告知别的 CPU 或设备去消费的发布写,因此无需 helper 层面的 drain;真正把它用于生产者-消费者或设备可见交接的调用方,须自行加上所需的排序。

这条 fast path 进一步改为无条件使用:既有的 page_ref_set tracepoint 与 KASAN/KMSAN 2 道 opt-out 被有意移除,template 路径在每个可复用模板播种之后始终启用。这里仍在分发 struct page 之前做初始化,保留这些仅服务于局部调试的特例只会让通用代码多出一条与主路径分叉的初始化路径;对 memcpy_nontemporal() 的 KASAN 特殊处理若有必要,也应落在底层 helper 而非 ZONE_DEVICE 调用点。不同 memory_type 与 hugetlb 几何仍交给编译器对 enum 的 switch 检查覆盖。

收益

作者提供 4 平台量化数据,均为 base v7.3-rc1 对比加上本系列,时间取自 memmap_init_zone_device() 的 pr_debug 输出,VM 的 rebind 数据为 30 次平均:

指标 Before After 改善
VM nd_pmem rebind 均时 221.07 ms 71.93 ms -67.5%
VM dax_pmem rebind 均时 191.20 ms 87.37 ms -54.3%
物理机 nd_pmem/fsdax rebind 205.90 ms 69.13 ms -66.4%
物理机 dax_pmem/devdax rebind 225.43 ms 90.67 ms -59.8%
物理机 test_hmm memremap 均时(每 64GB) ≈116.5 ms ≈46.7 ms -59.9%
arm64 VM nd_pmem rebind 均时 27.93 ms 14.53 ms -48.0%
arm64 VM dax_pmem rebind 均时 27.87 ms 16.27 ms -41.6%

rebind 均时更能隔离 memmap 初始化本身的改善。2 项机制贡献可分:

  • 模板复制单独约 -29.9%(nd_pmem 221.07→155.00ms)
  • memcpy_nontemporal() 切换本身增量有限(155.00→150.40ms),其余增益来自 x86 内联 MOVNTI fastpath(→71.93ms)

arm64 没有 x86 MOVNTI fast path,其改善全部来自通用模板复制路径,说明无专用 memcpy_nontemporal() 后端的架构同样受益。