背景

mprotect 改一段内存的页保护位,最终落到 change_protection / change_pte_range 这组函数上。它们在一个极其紧密的循环里逐 PTE 处理,被调用成百上千甚至 10 万次。这种热循环里,哪怕一点小低效都会被放大得很明显。

┌─────────────────────────────────┐
│ mprotect 改保护位走极紧密热循环 │
└────────────────┬────────────────┘
                 ▼
┌─────────────────────────────────┐
│ 即便 order-0 常见也走通用大循环 │  循环逻辑庞大,CPU 处理不好
└────────────────┬────────────────┘
                 ▼
┌─────────────────────────────────┐
│ 小低效在成千上万次循环里被放大  │
└─────────────────────────────────┘

change_pte_range 的循环逻辑相当庞大,一部分是为 large folio 批量处理保留的通用路径。问题在于,最常见的 order-0(单页)情况也要走这套通用大循环,而 CPU 对这种庞大、带很多分支的循环逻辑处理得并不好。

问题

  • change_pte_range 是 mprotect 的极紧密热循环
  • order-0 常见情况也走通用大循环
  • 循环逻辑庞大、分支多,CPU 处理不佳
  • 小低效在成千上万次循环里被放大

方案

核心是给最常见的 order-0 情况单独的分支,让它绕开通用大循环。

旧:order-0 也走通用大循环
┌──────────────────────────┐
│ 所有情况都进庞大循环逻辑 │
└────────────┬─────────────┘
             ▼
┌──────────────────────────┐
│ 循环逻辑大,CPU 处理不佳 │
└──────────────────────────┘

新:order-0 单独分支并强制内联
┌─────────────────────────────────────┐
│     order-0 常见情况走独立分支      │
└──────────────────┬──────────────────┘
                   ▼
┌─────────────────────────────────────┐
│ 强制内联批量 PTE 逻辑、消解常量分支 │  softleaf 代码也移出主函数,循环更紧凑
└─────────────────────────────────────┘

优化分 2 步。先做铺垫:把 softleaf 相关的 change_pte_range 代码移出主函数,让主循环更小、更易读,也为后续优化腾出空间。接着为 order-0(small folio)单独开 1 条 fast path,跳过那套为批量 large folio 准备的繁复循环逻辑;同时针对性地加上 __always_inline,鼓励编译器把批量 PTE 处理逻辑内联、把常量分支在编译期消解掉,让生成的热循环更紧凑。

作者还指出,按他的 profile,这条路径随后的大瓶颈是 modify_prot_start_ptes 里 x86 的 xchg()ptep_get_and_clear 依赖 D 位、代价高),那是需要单独攻克的另一难题。

收益

作者用 google/benchmark(g++ -O2)跑 mprotect_bench,并用 libmicro/mprot_tw4m 复测。

mprotect_bench(google/benchmark,g++ -O2):

指标 Baseline Patched 变化
每次 mprotect 耗时 85,967 ns 70,684 ns ~-18%

libmicro/mprot_tw4m(Luke Yang 复测):

指标 范围
提升 5%~55%,多数约 25%

对那些平时少见、却频繁调用 mprotect 的工作负载,这套优化带来约 18% 的端到端加速;libmicro 在更贴近真实场景的测量下,多数也有约 25% 的提升。