[PATCH v2 2/8] x86/smp: Remove unnecessary WBINVD from mwait_play_dead()

From: Pasha Tatashin

Date: Sun Oct 18 2026 - 21:15:55 EST


When a logical CPU goes offline via mwait_play_dead(), it executes
WBINVD before entering the MWAIT loop. On processors with large shared
last-level caches (such as Intel Xeon 6985P-C with 504 MB L3 per
socket), each WBINVD takes ~85-115 ms, flushes and invalidates the
entire socket's shared caches, and stalls the physical core (including
any online SMT sibling on that core) for the duration of the flush.

mwait_play_dead() places the logical processor into an architectural
MWAIT C-state—the same hardware C-states entered by intel_idle during
normal idle without executing WBINVD—where C-states deeper than C1/C1E
flush core-private L1/L2 caches to the coherent LLC on C-state entry
and maintain cache coherency while the package remains powered.

As long as at least one CPU sharing the same LLC on the package remains
online, system-wide cache flushes (wbinvd_on_all_cpus()), system sleep,
and kexec will still flush that shared LLC via the online CPU(s).
However, when the last online CPU in an LLC goes offline, no online CPU
remains to service wbinvd_on_all_cpus() or flush the package before
S3/S4 power removal. Similarly, if the target MWAIT state is C1/C1E
(which does not flush core-private caches) or cache_state_incoherent is
set (e.g., SME), the CPU must flush its caches before entering MWAIT.

Skip WBINVD in mwait_play_dead() when entering a C-state deeper than
C1/C1E while another CPU sharing the same package and LLC remains
online (unless cache_state_incoherent is set).

2-socket, 480-CPU Intel Xeon 6985P-C (N=479, mean of 10 runs):
Offline: 40.46 s -> 11.42 s (-71.8%)
Online: 11.14 s -> 11.08 s (-0.5%)
Total: 51.60 s -> 22.50 s (-56.4%)

Signed-off-by: Pasha Tatashin <pasha.tatashin@xxxxxxxxxx>
---
arch/x86/kernel/smpboot.c | 46 +++++++++++++++++++++++++++++++++------
1 file changed, 39 insertions(+), 7 deletions(-)

diff --git a/arch/x86/kernel/smpboot.c b/arch/x86/kernel/smpboot.c
index ba01a9e919b7..0dccd2f927bb 100644
--- a/arch/x86/kernel/smpboot.c
+++ b/arch/x86/kernel/smpboot.c
@@ -1414,10 +1414,28 @@ void play_dead_common(void)
local_irq_disable();
}

-/*
- * We need to flush the caches before going to sleep, lest we have
- * dirty data in our caches when we come back up.
- */
+static bool mwait_play_dead_needs_wbinvd(unsigned int eax_hint)
+{
+ unsigned int cpu = smp_processor_id();
+ u32 pkg_id = topology_physical_package_id(cpu);
+ u32 llc_id = per_cpu_llc_id(cpu);
+ unsigned int i;
+
+ if (cpuhp_tasks_frozen || MWAIT_HINT2CSTATE(eax_hint) < 2 ||
+ llc_id == BAD_APICID || this_cpu_read(cache_state_incoherent)) {
+ return true;
+ }
+
+ for_each_online_cpu(i) {
+ if (topology_physical_package_id(i) == pkg_id &&
+ per_cpu_llc_id(i) == llc_id) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
void __noreturn mwait_play_dead(unsigned int eax_hint)
{
struct mwait_cpu_dead *md = this_cpu_ptr(&mwait_cpu_dead);
@@ -1426,15 +1444,29 @@ void __noreturn mwait_play_dead(unsigned int eax_hint)
md->status = CPUDEAD_MWAIT_WAIT;
md->control = CPUDEAD_MWAIT_WAIT;

- wbinvd();
+ /*
+ * Entering MWAIT C-states deeper than C1/C1E/C2 writes back and
+ * invalidates core-private L1/L2 caches into the shared LLC before
+ * power-gating the core. While the CPU is still in C0 prior to MWAIT,
+ * its L1/L2 caches remain coherent and snoop external cache flushes,
+ * and once in MWAIT its L1/L2 caches are empty and all dirty lines
+ * reside in the shared LLC. Thus, as long as at least one CPU sharing
+ * this LLC remains online, any concurrent or subsequent
+ * wbinvd_on_all_cpus(), system sleep, or kexec will flush the shared
+ * LLC to memory.
+ *
+ * Only flush here during system sleep (cpuhp_tasks_frozen), when
+ * entering C1/C1E/C2, when the last online CPU in the LLC goes
+ * offline, or if the CPU's cache state is marked incoherent (e.g. SME).
+ */
+ if (mwait_play_dead_needs_wbinvd(eax_hint))
+ wbinvd();

while (1) {
/*
* The CLFLUSH is a workaround for erratum AAI65 for
* the Xeon 7400 series. It's not clear it is actually
* needed, but it should be harmless in either case.
- * The WBINVD is insufficient due to the spurious-wakeup
- * case where we return around the loop.
*/
mb();
clflush(md);
--
2.56.0.176.g3575e65682-goog