背景
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% 的提升。