* [RFC PATCH 0/1] KVM: x86: Skip empty TDP leaf page tables @ 2026-08-14 7:51 Hao Zhang 2026-08-14 7:55 ` [RFC PATCH 1/1] " Hao Zhang 0 siblings, 1 reply; 3+ messages in thread From: Hao Zhang @ 2026-08-14 7:51 UTC (permalink / raw) To: Sean Christopherson; +Cc: Paolo Bonzini, kvm From: Hao Zhang <zhanghao1@kylinos.cn> Hi, KVM intentionally keeps the TDP page table hierarchy when zapping only leaf SPTEs, because the backing memslot stays valid and future faults can reuse the existing paging structure. But when the same guest range is invalidated repeatedly, every later zap still descends through the retained 4K leaf page tables whose 512 SPTEs are all non-present. Each such descent is pure overhead: the walk scans 512 entries, zaps nothing, and on a contended host that can also force a yield/restart cycle. This pattern can occur when userspace repeatedly unmaps or discards the same guest range, e.g. ballooning or memory-discard/reset paths depending on userspace behavior: each notifier-driven unmap zaps the range, the guest refaults, and the next unmap zaps the same range again. The patch adds a TDP-MMU-only hint on 4K leaf page tables, set when a zap that covers the whole 2MiB region reaches the last entry of the page table, and cleared whenever a leaf SPTE is installed. If a later zap fully covers the corresponding 2MiB range and the hint is set, the iterator skips descending into that child page table. The hierarchy stays linked, so refaults can still reuse it. The hint is conservative: a false value only costs one extra descent, never a missed zap. Why not unlink the empty page table instead? Unlinking the 2MiB non-leaf SPTE requires a TLB flush and an RCU free of the page table, and the next fault then has to allocate and relink a fresh one. KVM retains empty page tables today regardless, so the hint does not change KVM's memory-ownership behavior; it only makes repeated zaps cheaper. The saved work is host-side only, i.e. not directly guest-visible; the value is CPU on the host during invalidation churn on overcommitted machines. Correctness rests on an existing invariant: gfn-range zaps run only while mmu_invalidate_in_progress is elevated or slots_lock is held (kvm_unmap_gfn_range() asserts exactly this), so no vCPU can install a leaf SPTE between the walk zapping an entry and the walk marking the page table empty. Every SPTE installation goes through __handle_changed_spte(), which clears the hint, so a stale-true hint is not possible; a stale-false hint is harmless. Scope notes: - Only 4K leaf page tables are tracked. The 2M-level page tables are walked anyway for any range that spans them, so a hint there would buy little; the expensive part is descending into 4K page tables. - Partial ranges are never skipped; the hint is only consumed when the zap range fully covers the 2MiB region of the page table. - Ranges smaller than a complete 2MiB region bypass the hint checks entirely. - The hint is TDP-MMU-only. - On the tested configuration, the new bool lives in existing padding of struct kvm_mmu_page. - Yield/restart never preserves a stale skip decision; the restarted walk re-evaluates page-table state from the root. Test setup: - Host: same x86 host for all runs, PREEMPT kernel, so that zaps of large ranges exercise the yield/restart path. - Guest: 4 vCPUs, 1GiB memory. - Workload: guest threads continuously fault and write guest memory while the host repeatedly invalidates guest memory with MADV_DONTNEED. - Measured paths: tdp_mmu_zap_leafs() duration and vCPU fault-side mmu_lock wait time, collected with perf probes. - Mechanism validation: kprobes on tdp_iter_skip_child() and tdp_mmu_zap_leafs(). Two invalidation patterns were used: - "same": repeatedly invalidate the same 16MiB or 4MiB host range. This stresses retained empty leaf page tables, because later zaps revisit page tables that earlier zaps emptied. - "sweep": invalidate rolling 16MiB, 4MiB, 2MiB and 1MiB chunks across the 1GiB guest. Less favorable to the hint, since guest faults often repopulate page tables before the next zap reaches the same range. Results from five matched repetitions on the same host, comparing the uninstrumented patched kernel against the original kernel: Pattern Original median Patched median Delta ---------- ---------------- ---------------- -------------------------- same16 330ms 35ms -89% same4 341ms 54ms -84% sweep16 1.456s 1.386s -5% sweep4 1.252s 1.244s roughly flat sweep2 1.197s 1.246s +4% sweep1 1.353s 1.138s -16% The same host, guest size, vCPU pinning, madvise rate, and chunk scenarios were used for both kernels. The result matches the intended scope: repeated same-range invalidation is where the retained-empty hint removes substantial zap-side work; rolling sweeps are mostly neutral, with small run-to-run variation. An intermediate version without the sub-2MiB gate ran the hint checks on every entry and regressed sweep1 to 1.64s zap total in one run. The final version bypasses the hint path for sub-2MiB ranges entirely; in the matched five-run comparison above, sweep1 was not worse than the original kernel. Fault-side mmu_lock wait showed no scenario with the earlier millisecond-scale regression. Median fault-wait p99.9 improved mainly in same-range cases, from 78us to 61us for same16 and from 62us to 38us for same4. Rolling sweep fault-wait p99.9 was effectively unchanged. Mechanism counters: - same16: tdp_iter_skip_child() was hit 2000 times over 867 zap calls in the final counter run. The misses are page tables repopulated between zaps, i.e. the hint-clear path working as intended. - sweep1: tdp_iter_skip_child() was never hit, confirming that sub-2MiB ranges bypass the hint path. - A writeback/refault variant reduced skip hits sharply: c16 dropped from 4177 skips over 290 zap calls without writeback to 187 skips over 245 zap calls with writeback. This confirms that installing a new leaf SPTE clears the hint and prevents skipping page tables that became non-empty again. The main RFC question is whether this style of retained-tree hint is acceptable for avoiding repeated empty 4K leaf-page-table walks, or whether KVM should keep treating retained empty TDP leaf page tables as intentionally reusable structure with no additional state. Hao Zhang (1): KVM: x86: Skip empty TDP leaf page tables arch/x86/kvm/mmu/mmu_internal.h | 2 ++ arch/x86/kvm/mmu/tdp_iter.c | 10 +++++- arch/x86/kvm/mmu/tdp_iter.h | 6 ++++ arch/x86/kvm/mmu/tdp_mmu.c | 74 ++++++++++++++++++++++++++++++++++++----- 4 files changed, 82 insertions(+), 10 deletions(-) base-commit: c21bb4193868a8de71fc4693fa741e195fdf5d86 -- 2.15.0 ^ permalink raw reply [flat|nested] 3+ messages in thread
* [RFC PATCH 1/1] KVM: x86: Skip empty TDP leaf page tables 2026-08-14 7:51 [RFC PATCH 0/1] KVM: x86: Skip empty TDP leaf page tables Hao Zhang @ 2026-08-14 7:55 ` Hao Zhang 2026-08-14 17:49 ` Sean Christopherson 0 siblings, 1 reply; 3+ messages in thread From: Hao Zhang @ 2026-08-14 7:55 UTC (permalink / raw) To: Sean Christopherson; +Cc: Paolo Bonzini, kvm From: Hao Zhang <zhanghao1@kylinos.cn> When KVM zaps only leaf SPTEs, the TDP page table hierarchy is intentionally retained so that subsequent faults can reuse it. However, a later zap of the same range still descends through retained 4K leaf page tables whose leaf SPTEs are all non-present. Track whether a retained 4K leaf page table contains any present leaf SPTEs. If a zap fully covers the corresponding 2MiB range and the page table is known to be empty, skip descending into it. Keep the page table hierarchy linked so that it can still be reused by future page faults. Installing a new leaf SPTE clears the empty hint. The hint checks are skipped for ranges smaller than a complete 2MiB leaf page table. The iterator restarts its walk from the root after yielding. Allow the restarted walk to mark an empty leaf page table again instead of permanently excluding the page table that contained the yield. Tested on a 4-vCPU, 1GB guest with five repetitions on the same host. The main improvement is seen for repeated same-range invalidation; rolling sweep workloads are mostly neutral. Signed-off-by: Hao Zhang <zhanghao1@kylinos.cn> --- arch/x86/kvm/mmu/mmu_internal.h | 2 ++ arch/x86/kvm/mmu/tdp_iter.c | 10 +++++- arch/x86/kvm/mmu/tdp_iter.h | 6 ++++ arch/x86/kvm/mmu/tdp_mmu.c | 74 ++++++++++++++++++++++++++++++++++++----- 4 files changed, 82 insertions(+), 10 deletions(-) diff --git a/arch/x86/kvm/mmu/mmu_internal.h b/arch/x86/kvm/mmu/mmu_internal.h index 73cdcbccc89e..5305aa3e688b 100644 --- a/arch/x86/kvm/mmu/mmu_internal.h +++ b/arch/x86/kvm/mmu/mmu_internal.h @@ -95,6 +95,8 @@ struct kvm_mmu_page { }; bool has_mapped_host_mmio; + /* TDP MMU only: no present leaf SPTEs exist in this leaf page table. */ + bool tdp_mmu_empty_leaf_pt; union { /* These two members aren't used for TDP MMU */ diff --git a/arch/x86/kvm/mmu/tdp_iter.c b/arch/x86/kvm/mmu/tdp_iter.c index 9e17bfa80901..1d87db14a6ae 100644 --- a/arch/x86/kvm/mmu/tdp_iter.c +++ b/arch/x86/kvm/mmu/tdp_iter.c @@ -23,6 +23,7 @@ static void tdp_iter_refresh_sptep(struct tdp_iter *iter) void tdp_iter_restart(struct tdp_iter *iter) { iter->yielded = false; + iter->skip_child = false; iter->yielded_gfn = iter->next_last_level_gfn; iter->level = iter->root_level; @@ -167,9 +168,11 @@ void tdp_iter_next(struct tdp_iter *iter) return; } - if (try_step_down(iter)) + if (!iter->skip_child && try_step_down(iter)) return; + iter->skip_child = false; + do { if (try_step_side(iter)) return; @@ -177,3 +180,8 @@ void tdp_iter_next(struct tdp_iter *iter) iter->valid = false; } +void tdp_iter_skip_child(struct tdp_iter *iter) +{ + WARN_ON_ONCE(iter->yielded); + iter->skip_child = true; +} diff --git a/arch/x86/kvm/mmu/tdp_iter.h b/arch/x86/kvm/mmu/tdp_iter.h index 364c5da6c499..47ac8bd43794 100644 --- a/arch/x86/kvm/mmu/tdp_iter.h +++ b/arch/x86/kvm/mmu/tdp_iter.h @@ -114,6 +114,11 @@ struct tdp_iter { * level instead of advancing to the next entry. */ bool yielded; + /* + * True if tdp_iter_next() should skip the child page table referenced by + * the current SPTE, instead of descending into it. + */ + bool skip_child; }; /* @@ -139,5 +144,6 @@ void tdp_iter_start(struct tdp_iter *iter, struct kvm_mmu_page *root, int min_level, gfn_t next_last_level_gfn, gfn_t gfn_bits); void tdp_iter_next(struct tdp_iter *iter); void tdp_iter_restart(struct tdp_iter *iter); +void tdp_iter_skip_child(struct tdp_iter *iter); #endif /* __KVM_X86_MMU_TDP_ITER_H */ diff --git a/arch/x86/kvm/mmu/tdp_mmu.c b/arch/x86/kvm/mmu/tdp_mmu.c index c1cbae65d239..84e18bd6e777 100644 --- a/arch/x86/kvm/mmu/tdp_mmu.c +++ b/arch/x86/kvm/mmu/tdp_mmu.c @@ -238,6 +238,7 @@ static void tdp_mmu_init_sp(struct kvm_mmu_page *sp, tdp_ptep_t sptep, sp->gfn = gfn; sp->ptep = sptep; sp->tdp_mmu_page = true; + sp->tdp_mmu_empty_leaf_pt = false; trace_kvm_mmu_get_page(sp, true); } @@ -527,6 +528,10 @@ static int __handle_changed_spte(struct kvm *kvm, struct kvm_mmu_page *sp, if (is_leaf) check_spte_writable_invariants(new_spte); + if (is_leaf && !was_leaf && sp->tdp_mmu_page && + sp->role.level == PG_LEVEL_4K) + WRITE_ONCE(sp->tdp_mmu_empty_leaf_pt, false); + /* * The only times a SPTE should be changed from a non-present to * non-present state is when an MMIO entry is installed/modified/ @@ -923,6 +928,39 @@ bool kvm_tdp_mmu_zap_possible_nx_huge_page(struct kvm *kvm, return true; } +static bool tdp_mmu_range_covers_leaf_pt(struct tdp_iter *iter, + gfn_t start, gfn_t end) +{ + struct kvm_mmu_page *sp; + + sp = sptep_to_sp(rcu_dereference(iter->sptep)); + return sp->gfn >= start && + sp->gfn + KVM_PAGES_PER_HPAGE(PG_LEVEL_2M) <= end; +} + +static bool tdp_mmu_can_skip_leaf_pt(struct tdp_iter *iter, + gfn_t start, gfn_t end) +{ + struct kvm_mmu_page *child_sp; + + if (iter->level != PG_LEVEL_2M || + !is_shadow_present_pte(iter->old_spte) || + is_last_spte(iter->old_spte, iter->level)) + return false; + + if (iter->gfn < start || + iter->gfn + KVM_PAGES_PER_HPAGE(PG_LEVEL_2M) > end) + return false; + + /* + * Skip retained empty 4K leaf page tables without unlinking them, so + * future faults can reuse the paging structure. + */ + child_sp = spte_to_child_sp(iter->old_spte); + return child_sp->role.level == PG_LEVEL_4K && + READ_ONCE(child_sp->tdp_mmu_empty_leaf_pt); +} + /* * If can_yield is true, will release the MMU lock and reschedule if the * scheduler needs the CPU or there is contention on the MMU lock. If this @@ -934,8 +972,10 @@ static bool tdp_mmu_zap_leafs(struct kvm *kvm, struct kvm_mmu_page *root, gfn_t start, gfn_t end, bool can_yield, bool flush) { struct tdp_iter iter; + bool may_skip_leaf_pts; end = min(end, tdp_mmu_max_gfn_exclusive()); + may_skip_leaf_pts = end - start >= KVM_PAGES_PER_HPAGE(PG_LEVEL_2M); lockdep_assert_held_write(&kvm->mmu_lock); @@ -948,18 +988,34 @@ static bool tdp_mmu_zap_leafs(struct kvm *kvm, struct kvm_mmu_page *root, continue; } - if (!is_shadow_present_pte(iter.old_spte) || - !is_last_spte(iter.old_spte, iter.level)) + if (may_skip_leaf_pts && + tdp_mmu_can_skip_leaf_pt(&iter, start, end)) { + tdp_iter_skip_child(&iter); continue; + } - tdp_mmu_iter_set_spte(kvm, &iter, SHADOW_NONPRESENT_VALUE); + if (is_shadow_present_pte(iter.old_spte) && + is_last_spte(iter.old_spte, iter.level)) { + tdp_mmu_iter_set_spte(kvm, &iter, + SHADOW_NONPRESENT_VALUE); - /* - * Zappings SPTEs in invalid roots doesn't require a TLB flush, - * see kvm_tdp_mmu_zap_invalidated_roots() for details. - */ - if (!root->role.invalid) - flush = true; + /* + * Zappings SPTEs in invalid roots doesn't require a TLB flush, + * see kvm_tdp_mmu_zap_invalidated_roots() for details. + */ + if (!root->role.invalid) + flush = true; + } + + if (may_skip_leaf_pts && + iter.level == PG_LEVEL_4K && + spte_index(rcu_dereference(iter.sptep)) == SPTE_ENT_PER_PAGE - 1 && + tdp_mmu_range_covers_leaf_pt(&iter, start, end)) { + struct kvm_mmu_page *sp; + + sp = sptep_to_sp(rcu_dereference(iter.sptep)); + WRITE_ONCE(sp->tdp_mmu_empty_leaf_pt, true); + } } rcu_read_unlock(); base-commit: c21bb4193868a8de71fc4693fa741e195fdf5d86 -- 2.15.0 ^ permalink raw reply related [flat|nested] 3+ messages in thread
* Re: [RFC PATCH 1/1] KVM: x86: Skip empty TDP leaf page tables 2026-08-14 7:55 ` [RFC PATCH 1/1] " Hao Zhang @ 2026-08-14 17:49 ` Sean Christopherson 0 siblings, 0 replies; 3+ messages in thread From: Sean Christopherson @ 2026-08-14 17:49 UTC (permalink / raw) To: Hao Zhang; +Cc: Paolo Bonzini, kvm On Fri, Aug 14, 2026, Hao Zhang wrote: > From: Hao Zhang <zhanghao1@kylinos.cn> > > When KVM zaps only leaf SPTEs, the TDP page table hierarchy is > intentionally retained so that subsequent faults can reuse it. However, > a later zap of the same range still descends through retained 4K leaf > page tables whose leaf SPTEs are all non-present. > > Track whether a retained 4K leaf page table contains any present leaf > SPTEs. If a zap fully covers the corresponding 2MiB range and the page > table is known to be empty, skip descending into it. > > Keep the page table hierarchy linked so that it can still be reused by > future page faults. Installing a new leaf SPTE clears the empty hint. > The hint checks are skipped for ranges smaller than a complete 2MiB leaf > page table. > > The iterator restarts its walk from the root after yielding. Allow the > restarted walk to mark an empty leaf page table again instead of > permanently excluding the page table that contained the yield. > > Tested on a 4-vCPU, 1GB guest with five repetitions on the same host. > The main improvement is seen for repeated same-range invalidation; rolling > sweep workloads are mostly neutral. > > Signed-off-by: Hao Zhang <zhanghao1@kylinos.cn> > --- Does the generic iterator solution I provided a (long) while back work for your use case? I would still strongly prefer a generic solution that doesn't rely on storing metadata in the MMU page. https://lore.kernel.org/all/ZxmGdhwr9BlhUQ_Y@google.com ^ permalink raw reply [flat|nested] 3+ messages in thread
end of thread, other threads:[~2026-08-14 17:49 UTC | newest] Thread overview: 3+ messages (download: mbox.gz follow: Atom feed -- links below jump to the message on this page -- 2026-08-14 7:51 [RFC PATCH 0/1] KVM: x86: Skip empty TDP leaf page tables Hao Zhang 2026-08-14 7:55 ` [RFC PATCH 1/1] " Hao Zhang 2026-08-14 17:49 ` Sean Christopherson
This is an external index of several public inboxes, see mirroring instructions on how to clone and mirror all data and code used by this external index.