From: Matt Turner <mattst88@gmail.com>
To: qemu-devel@nongnu.org
Cc: richard.henderson@linaro.org, pbonzini@redhat.com,
philmd@mailo.com, zhao1.liu@intel.com, laurent@vivier.eu,
deller@gmx.de, pierrick.bouvier@oss.qualcomm.com,
Matt Turner <mattst88@gmail.com>
Subject: [RFC PATCH 6/8] RFC: accel/tcg: only poll for interrupts in blocks that can close a cycle
Date: Mon, 17 Aug 2026 15:00:36 -0400 [thread overview]
Message-ID: <20260817190038.580257-7-mattst88@gmail.com> (raw)
In-Reply-To: <20260817190038.580257-1-mattst88@gmail.com>
Every translation block begins by loading cpu->neg.icount_decr.u32,
testing it and branching to the exit path. That is three host instructions
at the top of every TB. Blocks are short, so this is expensive: an emulated
alpha gcc 16.2.0 compiling a 255k line translation unit executes 34.2
billion TBs at 6.04 guest instructions each. Forcing CF_NOIRQ on for the
whole run, which is not correct but bounds the prize, is worth 12.0% of all
instructions retired.
The check does not have to be in every block. Interrupt latency is bounded
as long as every cycle in the guest control flow graph passes through at
least one block that polls. Any such cycle must contain either an edge
whose destination is at or below the start of the block it leaves from, or
an edge whose destination is not known at translation time: take the block
with the lowest start address in the cycle, and the edge entering it comes
from a block at or above it.
So record, during translation, whether this TB has such an edge.
translator_use_goto_tb() already sees every statically known destination,
and every target that emits goto_tb reaches it, so a backward edge sets
DisasContextBase::needs_exit_check there. Indirect destinations are flagged
by tcg_gen_lookup_and_goto_ptr(). Blocks with neither cannot close a cycle
on their own and can skip the poll.
The check is therefore emitted retroactively in gen_tb_end(), using the
same emit_before_op mechanism the can_do_io stores use, and only when one
of the two flags is set. icount opts out and keeps the unconditional
counter.
Measured on an x86-64 host, LTO build, on top of the preceding patches:
before: 869,178,598,378 instructions
after: 809,988,851,304 instructions -6.81%
before: 80.49s wall clock
after: 78.00s wall clock -3.10%
That is 57% of the 12.0% ceiling, which is about right: roughly a quarter of
TB exits are indirect and are still polled, plus every loop back edge.
For the series as a whole, against an unmodified LTO build, instructions
retired fall from 1,647,901,588,726 to 809,988,851,304 (-50.85%) and wall
clock from 133.57s to 78.00s (-41.61%). The two do not match because what
the series removes is mostly cheap, well-predicted dispatch overhead: IPC
falls from 2.53 to 2.12 as the remaining work gets less regular.
tests/tcg/alpha/test-xpage-chain.c still passes, the emulated compiler still
produces byte-identical output, and a tight loop under alarm(1) is still
interrupted, after 897 million iterations.
RFC because:
- The soundness argument depends on every goto_tb destination passing
through translator_use_goto_tb(). No target in the tree bypasses it
today, but nothing enforces that.
- System mode interrupt latency now depends on guest control flow rather
than on block count. The bound is one straight-line run between cycles,
which should be fine, but timer-driven guests deserve a closer look than
I can give them.
Signed-off-by: Matt Turner <mattst88@gmail.com>
---
accel/tcg/translator.c | 57 ++++++++++++++++++++++++++++++++++++---
include/exec/translator.h | 2 ++
include/tcg/tcg.h | 2 ++
tcg/tcg-op.c | 2 ++
4 files changed, 60 insertions(+), 3 deletions(-)
diff --git ./accel/tcg/translator.c ./accel/tcg/translator.c
index 4921bf978c..ee61dec1c6 100644
--- ./accel/tcg/translator.c
+++ ./accel/tcg/translator.c
@@ -42,12 +42,29 @@ bool translator_io_start(DisasContextBase *db)
return true;
}
+/*
+ * Any cycle in the guest control flow graph must contain an edge whose
+ * destination is at or below the start of the block it leaves from, or an
+ * edge whose destination is not known at translation time. Only blocks with
+ * such an edge need the interrupt check, so defer the decision until the end
+ * of translation, when we know which edges this TB has.
+ *
+ * icount needs the counter unconditionally, so it opts out.
+ */
+static bool defer_exit_check(uint32_t cflags)
+{
+ return !(cflags & CF_USE_ICOUNT);
+}
+
static TCGOp *gen_tb_start(DisasContextBase *db, uint32_t cflags)
{
TCGv_i32 count = NULL;
TCGOp *icount_start_insn = NULL;
- if ((cflags & CF_USE_ICOUNT) || !(cflags & CF_NOIRQ)) {
+ tcg_ctx->exit_check_needed = false;
+
+ if ((cflags & CF_USE_ICOUNT) ||
+ (!(cflags & CF_NOIRQ) && !defer_exit_check(cflags))) {
count = tcg_temp_new_i32();
tcg_gen_ld_i32(count, tcg_env,
offsetof(CPUState, neg.icount_decr.u32) -
@@ -73,6 +90,12 @@ static TCGOp *gen_tb_start(DisasContextBase *db, uint32_t cflags)
*/
if (cflags & CF_NOIRQ) {
tcg_ctx->exitreq_label = NULL;
+ } else if (defer_exit_check(cflags)) {
+ /*
+ * Emitted retroactively by gen_tb_end(), but only if this TB can be
+ * part of a control flow cycle.
+ */
+ tcg_ctx->exitreq_label = gen_new_label();
} else {
tcg_ctx->exitreq_label = gen_new_label();
tcg_gen_brcondi_i32(TCG_COND_LT, count, 0, tcg_ctx->exitreq_label);
@@ -88,7 +111,8 @@ static TCGOp *gen_tb_start(DisasContextBase *db, uint32_t cflags)
}
static void gen_tb_end(const TranslationBlock *tb, uint32_t cflags,
- TCGOp *icount_start_insn, int num_insns)
+ TCGOp *icount_start_insn, int num_insns,
+ DisasContextBase *db, TCGOp *first_insn_start)
{
if (cflags & CF_USE_ICOUNT) {
/*
@@ -99,6 +123,23 @@ static void gen_tb_end(const TranslationBlock *tb, uint32_t cflags,
tcgv_i32_arg(tcg_constant_i32(num_insns)));
}
+ if (tcg_ctx->exitreq_label && defer_exit_check(cflags) &&
+ !(cflags & CF_NOIRQ)) {
+ if (db->needs_exit_check || tcg_ctx->exit_check_needed) {
+ TCGv_i32 count = tcg_temp_new_i32();
+ TCGOp *save = tcg_ctx->emit_before_op;
+
+ tcg_ctx->emit_before_op = first_insn_start;
+ tcg_gen_ld_i32(count, tcg_env,
+ offsetof(CPUState, neg.icount_decr.u32) -
+ sizeof(CPUState));
+ tcg_gen_brcondi_i32(TCG_COND_LT, count, 0, tcg_ctx->exitreq_label);
+ tcg_ctx->emit_before_op = save;
+ } else {
+ tcg_ctx->exitreq_label = NULL;
+ }
+ }
+
if (tcg_ctx->exitreq_label) {
gen_set_label(tcg_ctx->exitreq_label);
tcg_gen_exit_tb(tb, TB_EXIT_REQUESTED);
@@ -117,6 +158,14 @@ bool translator_use_goto_tb(DisasContextBase *db, vaddr dest)
return false;
}
+ /*
+ * A destination at or below the start of this TB can close a cycle, so
+ * this TB must poll for interrupts. See defer_exit_check().
+ */
+ if (dest <= db->pc_first) {
+ db->needs_exit_check = true;
+ }
+
#ifdef CONFIG_USER_ONLY
/*
* There are no page tables in user-only mode. Every mmap, mprotect and
@@ -153,6 +202,7 @@ void translator_loop(CPUState *cpu, TranslationBlock *tb, int *max_insns,
db->max_insns = *max_insns;
db->insn_start = NULL;
db->fake_insn = false;
+ db->needs_exit_check = false;
db->host_addr[0] = host_pc;
db->host_addr[1] = NULL;
db->record_start = 0;
@@ -219,7 +269,8 @@ void translator_loop(CPUState *cpu, TranslationBlock *tb, int *max_insns,
/* Emit code to exit the TB, as indicated by db->is_jmp. */
ops->tb_stop(db, cpu);
- gen_tb_end(tb, cflags, icount_start_insn, db->num_insns);
+ gen_tb_end(tb, cflags, icount_start_insn, db->num_insns, db,
+ first_insn_start);
/*
* Manage can_do_io for the translation block: set to false before
diff --git ./include/exec/translator.h ./include/exec/translator.h
index 978dee25ad..003926c7f0 100644
--- ./include/exec/translator.h
+++ ./include/exec/translator.h
@@ -74,6 +74,8 @@ struct DisasContextBase {
int max_insns;
bool plugin_enabled;
bool fake_insn;
+ /* Set when this TB can be part of a control flow cycle. */
+ bool needs_exit_check;
uint8_t code_mmuidx;
struct TCGOp *insn_start;
void *host_addr[2];
diff --git ./include/tcg/tcg.h ./include/tcg/tcg.h
index 7669dc1c2d..be9ce7a0e2 100644
--- ./include/tcg/tcg.h
+++ ./include/tcg/tcg.h
@@ -389,6 +389,8 @@ struct TCGContext {
struct TCGLabelPoolData *pool_labels;
TCGLabel *exitreq_label;
+ /* Set by goto_ptr emission: destination is not known statically. */
+ bool exit_check_needed;
#ifdef CONFIG_PLUGIN
/*
diff --git ./tcg/tcg-op.c ./tcg/tcg-op.c
index a3efc56a9a..367e96627c 100644
--- ./tcg/tcg-op.c
+++ ./tcg/tcg-op.c
@@ -2616,6 +2616,7 @@ void tcg_gen_lookup_and_goto_ptr(void)
return;
}
+ tcg_ctx->exit_check_needed = true;
plugin_gen_disable_mem_helpers();
ptr = tcg_temp_ebb_new_ptr();
gen_helper_lookup_tb_ptr(ptr, tcg_env);
@@ -2642,6 +2643,7 @@ void tcg_gen_lookup_and_goto_ptr_inline(TCGv_i64 pc, uint32_t flags,
return;
}
+ tcg_ctx->exit_check_needed = true;
plugin_gen_disable_mem_helpers();
QEMU_BUILD_BUG_ON(sizeof(((CPUJumpCache *)0)->array[0]) != 16);
--
2.54.0
next prev parent reply other threads:[~2026-08-17 19:03 UTC|newest]
Thread overview: 9+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-08-17 19:00 [RFC PATCH 0/8] accel/tcg: cut per-block dispatch overhead Matt Turner
2026-08-17 19:00 ` [RFC PATCH 1/8] accel/tcg: cache the result of curr_cflags() Matt Turner
2026-08-17 19:00 ` [RFC PATCH 2/8] accel/tcg: enlarge the TB jump cache to 64K entries Matt Turner
2026-08-17 19:00 ` [RFC PATCH 3/8] accel/tcg: skip the can_do_io stores in user-only builds Matt Turner
2026-08-17 19:00 ` [RFC PATCH 4/8] RFC: tcg: probe the TB jump cache inline instead of calling a helper Matt Turner
2026-08-17 19:00 ` [RFC PATCH 5/8] RFC: accel/tcg: allow cross-page goto_tb chaining in user-only builds Matt Turner
2026-08-17 19:00 ` Matt Turner [this message]
2026-08-17 19:00 ` [RFC PATCH 7/8] RFC: accel/tcg: poison the jump cache instead of polling for indirect exits Matt Turner
2026-08-17 19:00 ` [RFC PATCH 8/8] 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=20260817190038.580257-7-mattst88@gmail.com \
--to=mattst88@gmail.com \
--cc=deller@gmx.de \
--cc=laurent@vivier.eu \
--cc=pbonzini@redhat.com \
--cc=philmd@mailo.com \
--cc=pierrick.bouvier@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.