From: Hao Zhang <hao_zhang_kdev@163.com>
To: Sean Christopherson <seanjc@google.com>
Cc: Paolo Bonzini <pbonzini@redhat.com>, kvm@vger.kernel.org
Subject: [RFC PATCH 1/1] KVM: x86: Skip empty TDP leaf page tables
Date: Fri, 14 Aug 2026 15:55:32 +0800 [thread overview]
Message-ID: <an7J9Gh2DTrtP5vT@192.168.1.215> (raw)
In-Reply-To: <an7JGuzynPBaYB5t@192.168.1.215>
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
next prev parent reply other threads:[~2026-08-14 7:55 UTC|newest]
Thread overview: 3+ messages / expand[flat|nested] mbox.gz Atom feed top
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 [this message]
2026-08-14 17:49 ` [RFC PATCH 1/1] " Sean Christopherson
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=an7J9Gh2DTrtP5vT@192.168.1.215 \
--to=hao_zhang_kdev@163.com \
--cc=kvm@vger.kernel.org \
--cc=pbonzini@redhat.com \
--cc=seanjc@google.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox