All of lore.kernel.org
 help / color / mirror / Atom feed
From: Matt Turner <mattst88@gmail.com>
To: qemu-devel@nongnu.org
Cc: richard.henderson@linaro.org, pbonzini@redhat.com,
	philmd@oss.qualcomm.com, alex.bennee@linaro.org,
	zhao1.liu@intel.com, Matt Turner <mattst88@gmail.com>
Subject: [PATCH v4 5/9] accel/tcg: give the TB jump cache a second base pointer for generated code
Date: Thu, 27 Aug 2026 01:02:37 -0400	[thread overview]
Message-ID: <20260827050241.3713332-6-mattst88@gmail.com> (raw)
In-Reply-To: <20260822190818.1829249-1-mattst88@gmail.com>

Add CPUState::tb_jmp_cache_probe, a base pointer that only generated code
will read. Normally it is cpu->tb_jmp_cache. Pointing it instead at a
shared, permanently zero-filled CPUJumpCache makes every entry generated
code finds have a NULL tb, so every lookup done through it misses. The
real jump cache is not touched, so nothing is lost and recovery is a single
store.

Nothing reads it yet. The next patch probes the jump cache from generated
code, and that probe cannot check everything helper_lookup_tb_ptr() checks;
poisoning this pointer is how the conditions it cannot check force it back
into the helper. The one that matters here is breakpoints.
check_for_breakpoints() raises EXCP_DEBUG on an exact pc match and selects
CF_BP_PAGE cflags for the rest of the page, and inserting a breakpoint
deliberately invalidates no TB, so a block translated before the breakpoint
was set is still sitting in the jump cache and would be dispatched to
directly.

cpu_breakpoint_insert() poisons the target CPU, rather than leaving it to
that CPU's own main loop, because gdb inserts a breakpoint into every CPU
(tcg_insert_gdbstub_breakpoint()) and a thread already inside generated
code dispatching to itself need never return to its main loop. The main
loop puts the pointer back once the last breakpoint is gone, re-checking
after the store so that it loses a race with a concurrent insert in the
safe direction.

The poison cache is a plain static rather than a const one so that it lands
in .bss: a megabyte of const zeroes would be a megabyte of .rodata in every
emulator binary, whereas .bss costs nothing on disk and faults in only the
handful of pages a poisoned run happens to probe.

v4: Split out of "tcg: probe the TB jump cache inline instead of calling a
    helper". Requested by Richard Henderson.

v4: Make the poison a static object rather than allocating one on first
    use. Suggested by Richard Henderson, who asked for const; see above for
    why it is not.

Signed-off-by: Matt Turner <mattst88@gmail.com>
---
 accel/stubs/tcg-stub.c      |  6 ++-
 accel/tcg/cpu-exec.c        | 96 +++++++++++++++++++++++++++++++++++++
 accel/tcg/internal-common.h |  2 +
 cpu-common.c                | 11 +++++
 include/hw/core/cpu.h       |  9 ++++
 include/system/tcg.h        |  9 ++++
 6 files changed, 132 insertions(+), 1 deletion(-)

diff --git ./accel/stubs/tcg-stub.c ./accel/stubs/tcg-stub.c
index f9e1bd22d6..8298e4a1f5 100644
--- ./accel/stubs/tcg-stub.c
+++ ./accel/stubs/tcg-stub.c
@@ -1,6 +1,6 @@
 /*
  * Stubs for the TCG entry points in system/tcg.h, for binaries that link
- * cpu-target.c or the HMP command handlers but not TCG.
+ * cpu-target.c, cpu-common.c or the HMP command handlers but not TCG.
  *
  * SPDX-License-Identifier: GPL-2.0-or-later
  */
@@ -14,3 +14,7 @@ void tcg_update_cflags(CPUState *cpu)
 void tcg_update_all_cflags(void)
 {
 }
+
+void tcg_cpu_poison_jmp_cache(CPUState *cpu)
+{
+}
diff --git ./accel/tcg/cpu-exec.c ./accel/tcg/cpu-exec.c
index 148e0f583e..c2a9679cd7 100644
--- ./accel/tcg/cpu-exec.c
+++ ./accel/tcg/cpu-exec.c
@@ -752,6 +752,93 @@ static inline bool cpu_handle_exception(CPUState *cpu, int *ret)
     return false;
 }
 
+/*
+ * The inline jump cache probe reads cpu->tb_jmp_cache_probe and takes the
+ * slow path when the entry it finds has a NULL tb.  Pointing the probe at a
+ * region that is all zeroes therefore forces every indirect dispatch into
+ * helper_lookup_tb_ptr(), which does the full lookup the inline probe only
+ * approximates.  The real jump cache is untouched, so no contents are lost
+ * and recovery is a single store.
+ *
+ * Only ever read from, and only one entry per dispatch, so one shared
+ * zero-filled cache is enough for every CPU.  Not const: that would put a
+ * megabyte of zeroes in .rodata and so in the binary, where .bss costs
+ * nothing on disk and only faults in the handful of pages a poisoned run
+ * happens to probe.
+ */
+static CPUJumpCache tb_jmp_cache_poison;
+
+/*
+ * Whether the generated code may dispatch to the next block by itself.
+ *
+ * The inline probe matches on the destination pc and on the flags and
+ * cflags the dispatching block was translated with.  It does not consult
+ * cpu->breakpoints, so it must not run while one is set: setting a
+ * breakpoint deliberately invalidates nothing, and check_for_breakpoints()
+ * both raises EXCP_DEBUG on an exact match and picks CF_BP_PAGE cflags for
+ * the rest of the page.  A block translated before the breakpoint was set is
+ * therefore still in the jump cache, and dispatching to it inline would step
+ * straight over the breakpoint.
+ */
+static bool tcg_cpu_may_dispatch(CPUState *cpu)
+{
+    return QTAILQ_EMPTY(&cpu->breakpoints);
+}
+
+/*
+ * Poison @cpu's probe, from any thread.  Called when a breakpoint is
+ * inserted, which is what makes the poison take effect at the dispatch
+ * after the insert rather than whenever @cpu next reaches its main loop:
+ * a vCPU chaining indirectly need never reach it, and would run past a
+ * breakpoint another thread had just set.
+ *
+ * A plain store is enough.  The value only ever costs a slow path that is
+ * correct on its own, and the generated code re-reads the base on every
+ * dispatch.  Un-poisoning is tcg_cpu_sync_jmp_cache()'s job.
+ */
+void tcg_cpu_poison_jmp_cache(CPUState *cpu)
+{
+    if (qatomic_read(&cpu->tb_jmp_cache_probe) != NULL) {
+        qatomic_set(&cpu->tb_jmp_cache_probe, &tb_jmp_cache_poison);
+    }
+}
+
+/*
+ * Called from the main loop, which is the only context that can establish
+ * that no reason to be poisoned is left.  Cheap enough to call every time
+ * round: the common case is a load, a compare and no store at all.
+ */
+void tcg_cpu_sync_jmp_cache(CPUState *cpu)
+{
+    CPUJumpCache *want;
+
+    if (qatomic_read(&cpu->tb_jmp_cache_probe) == NULL) {
+        return;  /* not realized, or already unrealized */
+    }
+
+    want = tcg_cpu_may_dispatch(cpu)
+           ? cpu->tb_jmp_cache
+           : &tb_jmp_cache_poison;
+
+    if (qatomic_read(&cpu->tb_jmp_cache_probe) != want) {
+        qatomic_set(&cpu->tb_jmp_cache_probe, want);
+
+        if (want == cpu->tb_jmp_cache) {
+            /*
+             * Un-poisoning races a concurrent tcg_cpu_poison_jmp_cache():
+             * the reason may have appeared after tcg_cpu_may_dispatch() read
+             * it, and the poison may have landed before the store above.
+             * Order that store against the re-read below, so that the race
+             * is lost in the safe direction.
+             */
+            smp_mb();
+            if (!tcg_cpu_may_dispatch(cpu)) {
+                tcg_cpu_poison_jmp_cache(cpu);
+            }
+        }
+    }
+}
+
 void tcg_kick_vcpu_thread(CPUState *cpu)
 {
     /*
@@ -964,6 +1051,13 @@ cpu_exec_loop(CPUState *cpu, SyncClocks *sc)
                 break;
             }
 
+            /*
+             * Reaching here means the main loop has just re-evaluated
+             * everything the inline probe assumes, so this is where the
+             * probe is allowed to come back after a poison.
+             */
+            tcg_cpu_sync_jmp_cache(cpu);
+
             tb = tb_lookup(cpu, s);
             if (tb == NULL) {
                 CPUJumpCache *jc;
@@ -1072,6 +1166,7 @@ bool tcg_exec_realizefn(CPUState *cpu, Error **errp)
     tcg_update_cflags(cpu);
 
     cpu->tb_jmp_cache = g_new0(CPUJumpCache, 1);
+    qatomic_set(&cpu->tb_jmp_cache_probe, cpu->tb_jmp_cache);
     tlb_init(cpu);
 #ifndef CONFIG_USER_ONLY
     tcg_iommu_init_notifier_list(cpu);
@@ -1089,5 +1184,6 @@ void tcg_exec_unrealizefn(CPUState *cpu)
 #endif /* !CONFIG_USER_ONLY */
 
     tlb_destroy(cpu);
+    qatomic_set(&cpu->tb_jmp_cache_probe, NULL);
     g_free_rcu(cpu->tb_jmp_cache, rcu);
 }
diff --git ./accel/tcg/internal-common.h ./accel/tcg/internal-common.h
index 853d1b51ee..9d1f6712d6 100644
--- ./accel/tcg/internal-common.h
+++ ./accel/tcg/internal-common.h
@@ -144,6 +144,8 @@ void page_table_config_init(void);
 G_NORETURN void cpu_io_recompile(CPUState *cpu, uintptr_t retaddr);
 #endif /* CONFIG_USER_ONLY */
 
+void tcg_cpu_sync_jmp_cache(CPUState *cpu);
+
 void tb_phys_invalidate(TranslationBlock *tb, tb_page_addr_t page_addr);
 void tb_set_jmp_target(TranslationBlock *tb, int n, uintptr_t addr);
 
diff --git ./cpu-common.c ./cpu-common.c
index adb76b3a78..3aed0156e6 100644
--- ./cpu-common.c
+++ ./cpu-common.c
@@ -22,6 +22,7 @@
 #include "exec/cpu-common.h"
 #include "hw/core/cpu.h"
 #include "qemu/lockable.h"
+#include "system/tcg.h"
 #include "trace/trace-root.h"
 
 QemuMutex qemu_cpu_list_lock;
@@ -429,6 +430,16 @@ int cpu_breakpoint_insert(CPUState *cpu, vaddr pc, int flags,
         *breakpoint = bp;
     }
 
+    /*
+     * Nothing is invalidated here, so blocks translated before this point
+     * are still live and still dispatch to each other without consulting
+     * cpu->breakpoints.  Stop the ones that can: a TCG vCPU dispatching
+     * inline reads a base pointer that this poisons, so the next dispatch
+     * takes the slow path and sees the new breakpoint.  @cpu may be another
+     * thread, and may be running.
+     */
+    tcg_cpu_poison_jmp_cache(cpu);
+
     trace_breakpoint_insert(cpu->cpu_index, pc, flags);
     return 0;
 }
diff --git ./include/hw/core/cpu.h ./include/hw/core/cpu.h
index 81af7b9ee1..bd2cdd2a0b 100644
--- ./include/hw/core/cpu.h
+++ ./include/hw/core/cpu.h
@@ -519,6 +519,15 @@ struct CPUState {
     MemoryRegion *memory;
 
     struct CPUJumpCache *tb_jmp_cache;
+    /*
+     * @tb_jmp_cache_probe: base the inline jump cache probe reads.
+     *
+     * Normally @tb_jmp_cache.  Pointed at a shared page of zeroes to force
+     * every inline dispatch to miss and fall back to helper_lookup_tb_ptr();
+     * see tcg_cpu_sync_jmp_cache().  NULL before tcg_exec_realizefn() and
+     * after tcg_exec_unrealizefn().
+     */
+    struct CPUJumpCache *tb_jmp_cache_probe;
 
     GArray *gdb_regs;
     int gdb_num_regs;
diff --git ./include/system/tcg.h ./include/system/tcg.h
index 2c2dbc753b..bf05db1329 100644
--- ./include/system/tcg.h
+++ ./include/system/tcg.h
@@ -29,6 +29,15 @@ extern bool tcg_allowed;
 void tcg_update_cflags(CPUState *cpu);
 void tcg_update_all_cflags(void);
 
+/*
+ * Force @cpu's generated code back into the slow dispatch path, which
+ * re-checks everything the inline jump cache probe assumes.  Safe to call
+ * from any thread, and a no-op for a CPU that is not running TCG.  Call
+ * whenever something the probe cannot see changes under a running vCPU;
+ * the main loop undoes it once the reason is gone.
+ */
+void tcg_cpu_poison_jmp_cache(CPUState *cpu);
+
 /**
  * qemu_tcg_mttcg_enabled:
  * Check whether we are running MultiThread TCG or not.
-- 
2.54.0



  parent reply	other threads:[~2026-08-27  5:04 UTC|newest]

Thread overview: 47+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-08-22 19:08 [PATCH v3 0/7] accel/tcg: cut per-block dispatch overhead Matt Turner
2026-08-22 19:08 ` [PATCH v3 1/7] accel/tcg: fold the dynamic cflags into CPUState::tcg_cflags Matt Turner
2026-08-25 21:47   ` Richard Henderson
2026-08-27  4:57     ` Matt Turner
2026-08-26  7:46   ` Alex Bennée
2026-08-27  4:57     ` Matt Turner
2026-08-22 19:08 ` [PATCH v3 2/7] accel/tcg: enlarge the TB jump cache to 64K entries Matt Turner
2026-08-25 21:50   ` Richard Henderson
2026-08-27  4:57     ` Matt Turner
2026-08-22 19:08 ` [PATCH v3 3/7] accel/tcg: skip the can_do_io stores in user-only builds Matt Turner
2026-08-22 19:08 ` [PATCH v3 4/7] RFC: tcg: probe the TB jump cache inline instead of calling a helper Matt Turner
2026-08-25 22:28   ` Richard Henderson
2026-08-27  5:00     ` Matt Turner
2026-08-22 19:08 ` [PATCH v3 5/7] RFC: accel/tcg: allow cross-page goto_tb chaining in user-only builds Matt Turner
2026-08-26  7:51   ` Alex Bennée
2026-08-27  4:57     ` Matt Turner
2026-08-22 19:08 ` [PATCH v3 6/7] RFC: accel/tcg: poison the jump cache instead of polling for indirect exits Matt Turner
2026-08-22 19:08 ` [PATCH v3 7/7] RFC: tcg: fold a guest displacement into the host addressing mode Matt Turner
2026-08-25 22:52   ` Richard Henderson
2026-08-27  4:57     ` Matt Turner
2026-08-27  5:02 ` [PATCH v4 0/9] accel/tcg: cut per-block dispatch overhead Matt Turner
2026-09-01  3:47   ` [PATCH v5 " Matt Turner
2026-09-01  3:48     ` [PATCH v5 1/9] accel/tcg: fold the dynamic cflags into CPUState::tcg_cflags Matt Turner
2026-09-01  3:48     ` [PATCH v5 2/9] accel/tcg: enlarge the TB jump cache to 64K entries Matt Turner
2026-09-01  3:48     ` [PATCH v5 3/9] accel/tcg: skip the can_do_io stores in user-only builds Matt Turner
2026-09-01  3:48     ` [PATCH v5 4/9] tcg: add tcg_gen_goto_jc_{i32,i64,tl}() Matt Turner
2026-09-01  3:48     ` [PATCH v5 5/9] accel/tcg: add CF_NO_GOTO_JC, set while a breakpoint is present Matt Turner
2026-09-01  3:48     ` [PATCH v5 6/9] RFC: tcg: probe the TB jump cache inline instead of calling a helper Matt Turner
2026-09-01  3:48     ` [PATCH v5 7/9] RFC: accel/tcg: allow cross-page goto_tb chaining in user-only builds Matt Turner
2026-09-01  3:48     ` [PATCH v5 8/9] RFC: accel/tcg: poison the jump cache instead of polling for indirect exits Matt Turner
2026-09-01  3:48     ` [PATCH v5 9/9] RFC: tcg: fold a guest displacement into the host addressing mode Matt Turner
2026-08-27  5:02 ` [PATCH v4 1/9] accel/tcg: fold the dynamic cflags into CPUState::tcg_cflags Matt Turner
2026-08-27 18:51   ` Richard Henderson
2026-08-27  5:02 ` [PATCH v4 2/9] accel/tcg: enlarge the TB jump cache to 64K entries Matt Turner
2026-08-27  5:02 ` [PATCH v4 3/9] accel/tcg: skip the can_do_io stores in user-only builds Matt Turner
2026-08-27  5:02 ` [PATCH v4 4/9] tcg: pass the destination to tcg_gen_lookup_and_goto_ptr() Matt Turner
2026-08-27 23:12   ` Richard Henderson
2026-09-01  2:55     ` Matt Turner
2026-08-27  5:02 ` Matt Turner [this message]
2026-08-27 20:03   ` [PATCH v4 5/9] accel/tcg: give the TB jump cache a second base pointer for generated code Richard Henderson
2026-09-01  2:55     ` Matt Turner
2026-08-27  5:02 ` [PATCH v4 6/9] RFC: tcg: probe the TB jump cache inline instead of calling a helper Matt Turner
2026-08-27 23:34   ` Richard Henderson
2026-09-01  2:55     ` Matt Turner
2026-08-27  5:02 ` [PATCH v4 7/9] RFC: accel/tcg: allow cross-page goto_tb chaining in user-only builds Matt Turner
2026-08-27  5:02 ` [PATCH v4 8/9] RFC: accel/tcg: poison the jump cache instead of polling for indirect exits Matt Turner
2026-08-27  5:02 ` [PATCH v4 9/9] RFC: tcg: fold a guest displacement into the host addressing mode Matt Turner

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=20260827050241.3713332-6-mattst88@gmail.com \
    --to=mattst88@gmail.com \
    --cc=alex.bennee@linaro.org \
    --cc=pbonzini@redhat.com \
    --cc=philmd@oss.qualcomm.com \
    --cc=qemu-devel@nongnu.org \
    --cc=richard.henderson@linaro.org \
    --cc=zhao1.liu@intel.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 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.