Cache the notdirty_write region lookup: 3.1% faster loading

notdirty_write called uc->memory_mapping() on every guest write, before
testing whether the region is executable, and used the result for that one
bit. A probe counted over a million such calls per 3 seconds with
exec_region=0 - the full find_memory_mapping -> address_space_translate ->
flatview_translate chain running and the answer then discarded.

The map is static once CreateConfiguredEngine has mapped its five spans, so
a 512-entry direct-mapped page -> MemoryRegion cache removes all but cold
misses.

    interleaved A/B, engine start to first OnCarLoaded
      cache on:  31.27 / 31.34 / 31.22 s   mean 31.28
      cache off: 32.27 / 32.12 / 32.44 s   mean 32.28

3.1%, ranges fully separate, spreads under 0.35s. The profile confirms the
mechanism rather than just the outcome: flatview_translate 4.46% -> 3.17%,
find_memory_mapping 3.02% -> 2.36%.

THE FIRST VERSION OF THIS CACHE WAS SLOWER, and the measurement is why it
was caught. It used `static __thread`, which on Android resolves through
emulated TLS: __emutls_get_address appeared at 8.11% and pthread_getspecific
at 3.07% - together more than the 7.5% being removed. Storage now lives in
uc_struct, which is already per-thread here (one uc_engine per host thread),
so no TLS is involved. Do not reintroduce __thread on this path.

INVARIANT: the cache assumes mappings and permissions are fixed after engine
setup. Anyone changing them at runtime must clear mr_cache_ready.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-09-20 13:26:43 +03:00
co-authored by Claude
parent 5e246fc8db
commit 80768652ea
2 changed files with 37 additions and 23 deletions
@@ -361,6 +361,17 @@ struct uc_struct {
// linked lists containing hooks per type
struct list hook[UC_HOOK_MAX];
struct list hooks_to_del;
// NFSMW arm64-poc: page -> MemoryRegion cache for notdirty_write, which
// otherwise calls memory_mapping() on EVERY guest write just to test one
// permission bit (see cputlb.c). Lives here rather than in a __thread
// variable because this struct is already per-thread in that engine (one
// uc_engine per host thread) AND because emulated TLS on Android turned
// out to cost more than the lookup it replaced: __emutls_get_address
// 8.11% + pthread_getspecific 3.07%, measured, against the 7.5% being
// removed.
uint64_t mr_cache_tag[512];
void *mr_cache_val[512];
int mr_cache_ready;
int hooks_count[UC_HOOK_MAX];
// hook to count number of instructions for uc_emu_start()
@@ -19,7 +19,7 @@
#include "qemu/osdep.h"
#define UC_MR_CACHE_ENTRIES 512
#define UC_MR_CACHE_ENABLED 0
#define UC_MR_CACHE_ENABLED 1
#include "cpu.h"
#include "exec/exec-all.h"
#include "exec/memory.h"
@@ -1220,36 +1220,39 @@ static void notdirty_write(CPUState *cpu, vaddr mem_vaddr, unsigned size,
// permissions at RUNTIME must clear this cache, or stale permissions will
// be used. It is per-thread, so no locking is needed.
//
// NOT YET VERIFIED. Written and built, but the Pixel 6a became
// unmeasurable (screen dozing within seconds, NotificationShade holding
// focus) and the A9 cannot run this build at all - Android kills it with
// "failed to attach / start timeout" because startup work exceeds the
// attach deadline on that hardware. Defaults OFF so an unmeasured change
// does not reach the hot path. To evaluate: set to 1, then interleaved
// A/B with loadtime (engine start -> first OnCarLoaded) plus a profile
// check that flatview_translate and find_memory_mapping actually fall.
// A profile-share drop alone is NOT a speedup - that has been established
// three separate times in this project.
// VERIFIED 2026-09-20, interleaved A/B on a rebooted Pixel 6a, engine
// start to first OnCarLoaded:
// cache on: 31.27 / 31.34 / 31.22 s mean 31.28
// cache off: 32.27 / 32.12 / 32.44 s mean 32.28
// 3.1% faster, ranges fully separate. Profile confirms the mechanism:
// flatview_translate 4.46% -> 3.17%, find_memory_mapping 3.02% -> 2.36%.
//
// The first attempt at this cache used `static __thread` and was SLOWER,
// because emulated TLS on Android routes every access through
// __emutls_get_address (8.11%) + pthread_getspecific (3.07%) - more than
// the 7.5% it was removing. Hence the storage living in uc_struct, which
// is already per-thread here (one uc_engine per host thread). Do not
// reintroduce __thread in this path.
MemoryRegion *mr;
if (!UC_MR_CACHE_ENABLED) {
mr = cpu->uc->memory_mapping(cpu->uc, tlbe->paddr | (mem_vaddr & ~TARGET_PAGE_MASK));
} else {
struct uc_struct *ucc = cpu->uc;
target_ulong page = tlbe->paddr | (mem_vaddr & ~TARGET_PAGE_MASK);
target_ulong tag = page >> TARGET_PAGE_BITS;
uint64_t tag = (uint64_t)(page >> TARGET_PAGE_BITS);
unsigned idx = (unsigned)(tag & (UC_MR_CACHE_ENTRIES - 1));
static __thread target_ulong mr_cache_tag[UC_MR_CACHE_ENTRIES];
static __thread MemoryRegion *mr_cache_val[UC_MR_CACHE_ENTRIES];
static __thread int mr_cache_ready;
if (!mr_cache_ready) {
for (unsigned i = 0; i < UC_MR_CACHE_ENTRIES; i++) mr_cache_tag[i] = (target_ulong)-1;
mr_cache_ready = 1;
if (!ucc->mr_cache_ready) {
for (unsigned i = 0; i < UC_MR_CACHE_ENTRIES; i++) {
ucc->mr_cache_tag[i] = (uint64_t)-1;
}
ucc->mr_cache_ready = 1;
}
if (mr_cache_tag[idx] == tag) {
mr = mr_cache_val[idx];
if (ucc->mr_cache_tag[idx] == tag) {
mr = (MemoryRegion *)ucc->mr_cache_val[idx];
} else {
mr = cpu->uc->memory_mapping(cpu->uc, page);
mr_cache_tag[idx] = tag;
mr_cache_val[idx] = mr;
mr = ucc->memory_mapping(ucc, page);
ucc->mr_cache_tag[idx] = tag;
ucc->mr_cache_val[idx] = mr;
}
}