Skip to content

engine: no hook or flush checks on the TB lookup hit path - #63

Draft
lacraig2 wants to merge 4 commits into
mainfrom
lookup-cost
Draft

lacraig2 wants to merge 4 commits into
mainfrom
lookup-cost

Conversation

@lacraig2

@lacraig2 lacraig2 commented Nov 5, 2026 •

Copy link
Copy Markdown
Member

The engine adds work to QEMU's TB lookup fast path and CPU loop that every run pays, with nothing subscribed. This moves each piece off the hit path, so a run that uses no address hooks and requests no flushes looks up blocks at upstream's cost.

Changes

  • 0034: the pc_bound check for a CF_PCREL block bound to its virtual address moves from tb_lookup's jump-cache hit path to where a block from the hash table is written into the jump cache.
  • 0035: tb_lookup() asks for the page's hook cflags (tcg_hooks_cflags()) only after a jump-cache miss, then looks again. helper_lookup_tb_ptr and the CPU loop no longer call it, and the paths that translate apply it before tb_gen_code.
    • The miss path is a function of its own, out of line, so the hit path stays inlined into its callers as upstream's is.
    • A hooks commit now empties every vCPU's jump cache (accel/tcg/hooks.c), so a hit never serves a block translated before its page gained a hook site.
  • 0081: panda_do_flush_tb() queues the flush as exclusive work on the requesting vCPU (from outside a vCPU, on the first) and asks every vCPU to leave its TBs, instead of setting a bit the CPU loop tests on every iteration. Every requester queues its own work item, so a requesting vCPU runs no more guest code until a flush that comes after its request; the first item to run flushes and the rest find nothing pending. PANDA_LOOP_FLUSH is removed.

Each patch is amended in place. The ORIGINS rows are unchanged, base.json's patchedTree is re-recorded, and the docs/vanilla-gates.toml hashes are refreshed.

Cost

Callgrind host instructions on the vCPU thread, guest getppid loop (scwork), nothing subscribed. Per-function counts are the evidence. Whole-window totals move by a few percent between runs with guest timer ticks.

x86_64 aarch64
helper_lookup_tb_ptr, per call: before 85.2 85.5
helper_lookup_tb_ptr, per call: after 79.1 79.2
helper_lookup_tb_ptr, per call: stock QEMU 11.1.2 built PIC 80.2 81.0
Lookup path and CPU loop, per syscall, vs before about −180 about −230

A jump-cache miss costs about 30 more instructions than before, because the hook check now runs there. Misses are 0.3 to 11 per syscall in these workloads.

Tests

  • engine/scripts/verify-series.sh 11.1.2 … --record: 101 patches apply with strict context.
  • tests/static/vanilla_inventory.py: PASS.
  • engine/tests/guest/hktest.sh on x86_64 and aarch64: all 11 cases PASS on both (count, direct, many, alias, dyn, multi, kern, cond, smp, rp, late).
  • tests/flush_smp.py (new): two vCPUs request a flush while the other's is pending; the second requester must not run its old blocks. Fails on the previous version of 0081, passes on x86_64, aarch64 and riscv64.
  • mem.py and api.py (the pandare2 flush tests that cover 0081) on x86_64 and aarch64 Debian 13: pass. The nix build of .#python and tests/smoke.py x86_64: pass.

@lacraig2
lacraig2 force-pushed the lookup-cost branch 3 times, most recently from 8dcd141 to 825fef5 Compare November 13, 2026 23:17
… filled

tb_lookup's jump-cache hit path is upstream's again. The check that a
block bound to a virtual address is used only at that address moves to
where a block found in the hash table is written into the jump cache, so
a hit costs nothing extra: about 2 host instructions per lookup, 60 to 90
per guest system call.
helper_lookup_tb_ptr and the CPU loop called tcg_hooks_cflags() before
every lookup. tb_lookup() now asks for the page's cflags only when the
jump cache misses, and looks again with them; the paths that translate
apply them before tb_gen_code. The miss path is a function of its own,
out of line, so the hit path stays inlined into its callers as upstream's
is. A hooks commit empties every vCPU's jump cache, so a hit never serves
a block translated before its page gained a hook site. A hit on a page
with no hook sites costs what upstream's does.
…op check

panda_do_flush_tb() set a loop bit that every CPU-loop iteration tested.
It now queues the flush as exclusive work on the requesting vCPU (from
outside a vCPU, on the first) and asks every vCPU to leave its TBs. The
flush still comes before any more guest code runs, a requesting vCPU runs
none until a flush that follows its request, and the loop tests nothing
for it. A work item finding the flush already done by another does
nothing. PANDA_LOOP_FLUSH is gone; the loop's remaining PANDA bit is the
before_handle_exception gate.
With two vCPUs, vCPU 0 asks for a flush from an insn_exec callback and
stays in it, so its flush cannot run yet; vCPU 1 then changes what is
instrumented and asks for a flush too. The test checks that vCPU 1 runs
no more guest code until a flush, and that its next block is translated
again (insn_exec fires at the newly instrumented address). Runs on a
mkbusybox.py image booted with -smp 2; guest programs for x86_64, aarch64
and riscv64.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant