Re: [REGRESSION] mm/mprotect: shared-dirty base-page toggle slower since v6.17

From: Chengfeng Lin

Date: Thu Jul 23 2026 - 13:59:52 EST


Hi,

Thanks for the comments.

First, the exact direct parent/child test showed that cac1db8c3aad
("mm: optimize mprotect() by PTE batching") made this workload 39.77% slower.
A separate matched Pedro v3 test did not help: on v7.1.3, full-v3 was 6.20%
slower than the midpoint of the no-v3 controls. I do not treat that small
difference as a separate regression.

For context, this remains the same narrow synthetic microbenchmark: a 64 MiB
shared anonymous mapping, dirty 4 KiB base pages with no THP, and repeated
full-range protect, restore, and write-touch cycles.

Exact-commit decomposition
--------------------------

I ran a nine-point, fresh-boot nested ablation around the exact parent/child
pair, returning through matched controls to limit time drift. It reproduced a
36.51% child slowdown. Measured as a fraction of that gap:

diagnostic change gap recovered
parent-style single-PTE update/flush path 43.06%
skip normal-path batch discovery no measurable effect
additionally skip the normal-path folio lookup 44.47%
all three diagnostic changes combined 87.29%

The first diagnostic kept the child's lookup and batch discovery unchanged,
restoring the parent-style update/flush path only when discovery returned one
PTE. It recovered 43.06% of the gap, placing a substantial cost after
discovery in the generic single-PTE handling, though not at a specific
instruction. In the nested sequence, the intermediate discovery bypass had
no measurable effect; additionally skipping the normal-path folio lookup
recovered another 44.47%, so lookup contributes a similarly large share.
These costs occur in sequence, and both the protect and restore walks pay
both.

The combined probe was still 4.64% slower than the parent, leaving 12.71% of
the original gap unexplained. All 135 measured processes passed the semantic
and 4 KiB/no-THP checks; the maximum CV was 1.56%.

v7.1.3 lookup safety check
--------------------------

The exact sequence above uses the cac1db8c3aad child, which calls
vm_normal_folio(). This separate check uses unmodified v7.1.3, where the
corresponding path calls vm_normal_page() followed by page_folio(); its
percentages are not part of the exact-commit decomposition.

For a shared, non-NUMA PTE with pte_batch_hint() == 1, a diagnostic that skips
the current page/folio lookup and batch discovery was 17.36% faster on the
4 KiB workload. A paired trace removed exactly 2,048 vm_normal_page() calls,
but this is not a standalone timing of that function.

The same diagnostic was 65.80% slower for a verified shared 2 MiB folio mapped
by 512 PTEs, because on x86 the hint is also one and the diagnostic disables
folio batching.

All tests used fresh boots on an i7-12700KF with 32 GiB RAM. Within each
comparison, builds used matched configs and GCC 15.2.0. The benchmark used
P-core CPU 2, performance governor and EPP, Turbo off, and preempt=none.

Thus, the generic single-PTE update/flush path and the lookup account for
about 87% of the exact-commit gap. The v7.1.3 bypass is unsafe for PTE-mapped
large folios, so I am not proposing it as a fix.

Pedro did not observe the same lookup cost on his desktop. That difference
may be machine-specific, but I have not tested enough systems to establish
why. I have also not measured application-level impact.

Compact evidence bundle:

https://github.com/lcf0399/linux-regression-evidence/tree/ea3ec2d41702502336b9854b9da78c43840b86e8/mprotect-shared-dirty-toggle/bare-metal

Standalone reproducers:

https://github.com/lcf0399/linux-regression-evidence/tree/ea3ec2d41702502336b9854b9da78c43840b86e8/mprotect-shared-dirty-toggle/reproducer

Thanks,
Chengfeng