mirror of
https://github.com/HeyPuter/gex.git
synced 2026-11-13 20:12:43 -10:00
fcache: a probe cache entry holds the whole address of code below 64 GiB
An entry kept the address's low 32 bits and the index above them, and every probe compared the whole target with those bits. In a 64-bit memory, where Wine puts the images near the top of the 16 GiB, no call into them hit: each went to gex.lookup and the function table's lock (Hades gameplay: ftab lookups about 15% of the render thread's samples, 113M lookups in 740 s on one thread). And an entry written for an address above 4 GiB was taken for the address 4 GiB below it, which has the same slot and the same low bits: tests/browser-mem64-fcache.sh, functions at 0x20000000 and 0x120000000 called in turn from the function JIT's code, summed 199999001 where 100100000 is right. An entry is now the address's low 36 bits (gex_abi::FCACHE_TAG_BITS) and a 28-bit index above them: gex_abi::fcache_pack/fcache_probe, the kernel's writes and clears, cgen's C and wasm probes, the function JIT's two, gex-split's stubs, and the two providers' C probes. An address or index an entry cannot hold leaves the slot to miss. UNIT_ABI_VERSION goes to 2: every unit's inline probe reads the new form, so every unit is rebuilt. ?fcachestats=1 (GEX_FCACHE_STATS=1 under run-node): each Worker's probe misses at most every 10 s, how many above 4 GiB, and the addresses that missed most. Off by default, and nothing is counted then. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_40f26e8988a74ea098d8bc80ff68287c
This commit is contained in:
1 parent
085fdc23fe
commit
3bb8eaff18
15 files changed
+215
-32
No files matched your search
@@ -613,11 +613,13 @@ pub const BODY_LIMIT: u64 = BODY_LIMITS;
|
||||
|
||||
/// The function table's probe cache (docs/merge-plan.md §9.8): guest address
|
||||
/// → [`ftab`] index, direct-mapped, shared by every thread of the process.
|
||||
/// Entry `fcache_slot(a)` is one `u64`: the address's low 32 bits in the low
|
||||
/// half (guest code is below 4 GiB), the index in the high half. Generated
|
||||
/// code probes it inline on an indirect transfer; a miss (or a stale entry)
|
||||
/// is index 0, the resolve stub, which asks the host (`gex.lookup`) and
|
||||
/// writes the entry. All zero at start: address 0 maps to the resolve stub.
|
||||
/// Entry `fcache_slot(a)` is one `u64`: the address's low
|
||||
/// [`FCACHE_TAG_BITS`] bits (all of it, for code below 64 GiB: a memory64
|
||||
/// process's images sit near the top of its 16 GiB) and the index above
|
||||
/// them. Generated code probes it inline on an indirect transfer; a miss (or
|
||||
/// a stale entry) is index 0, the resolve stub, which asks the host
|
||||
/// (`gex.lookup`) and writes the entry. All zero at start: address 0 maps to
|
||||
/// the resolve stub.
|
||||
pub const FCACHE: u64 = PATCHED_PAGES - FCACHE_BYTES;
|
||||
/// `log2` of the probe cache's entries.
|
||||
pub const FCACHE_BITS: u32 = 15;
|
||||
@@ -640,10 +642,32 @@ pub const fn fcache_entry(a: u64) -> u64 {
|
||||
FCACHE + 8 * fcache_slot(a)
|
||||
}
|
||||
|
||||
/// A probe cache entry for `a` at index `idx`.
|
||||
/// The address bits a probe cache entry holds, below its index: the whole
|
||||
/// address of code below 64 GiB. A target at or past that never hits (the
|
||||
/// entry cannot name it), and a probe compares the whole target, so an
|
||||
/// entry never stands for another address with the same low bits.
|
||||
pub const FCACHE_TAG_BITS: u32 = 36;
|
||||
/// The address part of a probe cache entry.
|
||||
pub const FCACHE_TAG_MASK: u64 = (1 << FCACHE_TAG_BITS) - 1;
|
||||
/// One past the largest index an entry holds (its other 28 bits).
|
||||
pub const FCACHE_IDX_LIMIT: u64 = 1 << (64 - FCACHE_TAG_BITS);
|
||||
|
||||
/// A probe cache entry for `a` at index `idx`; `None` for an address or
|
||||
/// index an entry cannot hold (the slot is then left to miss).
|
||||
#[inline]
|
||||
pub const fn fcache_pack(a: u64, idx: u32) -> u64 {
|
||||
(a & 0xffff_ffff) | ((idx as u64) << 32)
|
||||
pub const fn fcache_pack(a: u64, idx: u32) -> Option<u64> {
|
||||
if a > FCACHE_TAG_MASK || idx as u64 >= FCACHE_IDX_LIMIT {
|
||||
return None;
|
||||
}
|
||||
Some(a | ((idx as u64) << FCACHE_TAG_BITS))
|
||||
}
|
||||
|
||||
/// The index probe cache entry `e` holds for target `t`, or the resolve stub
|
||||
/// ([`ftab::RESOLVE`]) when it holds another address: what every emitter's
|
||||
/// inline probe computes.
|
||||
#[inline]
|
||||
pub const fn fcache_probe(e: u64, t: u64) -> u32 {
|
||||
if e & FCACHE_TAG_MASK == t { (e >> FCACHE_TAG_BITS) as u32 } else { ftab::RESOLVE }
|
||||
}
|
||||
|
||||
/// Where the thread contexts must end: the runtime's fixed words
|
||||
|
||||
@@ -44,6 +44,7 @@ pub fn header() -> String {
|
||||
let _ = writeln!(w, "#define GEX_FCACHE 0x{:x}u", gex_abi::FCACHE);
|
||||
let _ = writeln!(w, "#define GEX_FCACHE_BITS {}u", gex_abi::FCACHE_BITS);
|
||||
let _ = writeln!(w, "#define GEX_FCACHE_MUL 0x{:x}u", gex_abi::FCACHE_MUL);
|
||||
let _ = writeln!(w, "#define GEX_FCACHE_TAG_BITS {}u", gex_abi::FCACHE_TAG_BITS);
|
||||
let _ = writeln!(w, "#define GEX_FTAB_RESOLVE {}u", gex_abi::ftab::RESOLVE);
|
||||
let _ = writeln!(w);
|
||||
|
||||
@@ -386,8 +387,9 @@ GEX_INLINE uint64_t gex_fma64(int ctx, uint64_t ab, uint64_t bb_, uint64_t cb, u
|
||||
/* The function table (docs/merge-plan.md §9.8.1). A unit is linked
|
||||
position-independent, so its function pointers are indices into the
|
||||
process's table, and a call through one is `call_indirect` on it. The
|
||||
index for a guest address comes from the probe cache: the entry's low half
|
||||
is the address, its high half the index; a miss is the resolve stub (0),
|
||||
index for a guest address comes from the probe cache: the entry's low
|
||||
GEX_FCACHE_TAG_BITS bits are the address, the bits above them the index;
|
||||
a miss is the resolve stub (0),
|
||||
which asks the host. Every call through the table stores its target in
|
||||
ctx.LAST_TARGET first (the resolve stub, and any slot a thread has not
|
||||
filled, read it). */
|
||||
@@ -395,7 +397,7 @@ typedef int (*gex_body_t)(int ctx, int32_t depth, uint32_t need, uint64_t ret);
|
||||
GEX_INLINE uint32_t gex_ftab_idx(uint64_t t) {
|
||||
uint32_t slot = ((uint32_t)t * GEX_FCACHE_MUL) >> (32u - GEX_FCACHE_BITS);
|
||||
uint64_t e = __atomic_load_n((gex_a64 *)GEX_P(GEX_FCACHE + 8u * slot), __ATOMIC_RELAXED);
|
||||
return (e & 0xffffffffull) == t ? (uint32_t)(e >> 32) : GEX_FTAB_RESOLVE;
|
||||
return (e & ((1ull << GEX_FCACHE_TAG_BITS) - 1u)) == t ? (uint32_t)(e >> GEX_FCACHE_TAG_BITS) : GEX_FTAB_RESOLVE;
|
||||
}
|
||||
#define GEX_FTAB(i) ((gex_body_t)(uintptr_t)(i))
|
||||
|
||||
|
||||
@@ -42,8 +42,10 @@ pub const UNIT_INPUTS: &str = env!("GEX_CGEN_UNIT_INPUTS");
|
||||
|
||||
/// What a unit embeds from outside gex-cgen and gex-abi (gex-emit's FMA
|
||||
/// constants, in `gex_abi.h`), which the unit key does not hash: bump it
|
||||
/// whenever [`unit_abi_fingerprint`] changes (its test says when).
|
||||
pub const UNIT_ABI_VERSION: u32 = 1;
|
||||
/// whenever [`unit_abi_fingerprint`] changes (its test says when). 2: the
|
||||
/// probe cache's wide entries (`gex_abi::FCACHE_TAG_BITS`), which every
|
||||
/// unit's inline probe reads.
|
||||
pub const UNIT_ABI_VERSION: u32 = 2;
|
||||
|
||||
/// The identity of an image's recovery (`gex_key::recovery_key` with
|
||||
/// [`RECOVERY_INPUTS`]): where the recovery is cached.
|
||||
@@ -932,7 +934,7 @@ mod abi_tests {
|
||||
fn unit_abi_version_pins_the_embedded_constants() {
|
||||
assert_eq!(
|
||||
(super::UNIT_ABI_VERSION, super::unit_abi_fingerprint().as_str()),
|
||||
(1, "fma 134217729.0 523 1523")
|
||||
(2, "fma 134217729.0 523 1523")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -105,14 +105,14 @@ pub fn emit_probe(f: &mut Function, t: u32, e: u32) {
|
||||
crate::splice::widen_addr(f);
|
||||
f.instruction(&I::I64Load(m));
|
||||
f.instruction(&I::LocalTee(e));
|
||||
// hit ? e >> 32 : 0, where hit compares the whole target with the low
|
||||
// half (a target past 4 GiB never hits).
|
||||
f.instruction(&I::I64Const(32));
|
||||
// hit ? e >> TAG_BITS : 0, where hit compares the whole target with the
|
||||
// entry's address bits (`gex_abi::fcache_probe`).
|
||||
f.instruction(&I::I64Const(gex_abi::FCACHE_TAG_BITS as i64));
|
||||
f.instruction(&I::I64ShrU);
|
||||
f.instruction(&I::I32WrapI64);
|
||||
f.instruction(&I::I32Const(ftab::RESOLVE as i32));
|
||||
f.instruction(&I::LocalGet(e));
|
||||
f.instruction(&I::I64Const(0xffff_ffff));
|
||||
f.instruction(&I::I64Const(gex_abi::FCACHE_TAG_MASK as i64));
|
||||
f.instruction(&I::I64And);
|
||||
f.instruction(&I::LocalGet(t));
|
||||
f.instruction(&I::I64Eq);
|
||||
|
||||
@@ -621,12 +621,12 @@ fn probe_const(fb: &mut gex_emit::wasm::FuncBody, t: u64, e: u32) {
|
||||
fb.addr_const(0);
|
||||
fb.load(0x29, 3, gex_abi::fcache_entry(t) as u32); // i64.load
|
||||
fb.local_tee(e);
|
||||
fb.i64_const(32);
|
||||
fb.i64_const(gex_abi::FCACHE_TAG_BITS as i64);
|
||||
fb.byte(0x88); // i64.shr_u
|
||||
fb.byte(0xa7); // i32.wrap_i64
|
||||
fb.i32_const(gex_abi::ftab::RESOLVE as i32);
|
||||
fb.local_get(e);
|
||||
fb.i64_const(0xffff_ffff);
|
||||
fb.i64_const(gex_abi::FCACHE_TAG_MASK as i64);
|
||||
fb.byte(0x83); // i64.and
|
||||
fb.i64_const(t as i64);
|
||||
fb.byte(0x51); // i64.eq
|
||||
@@ -1177,12 +1177,12 @@ impl E<'_, '_> {
|
||||
fb.addr_i32();
|
||||
fb.load(0x29, 3, gex_abi::FCACHE as u32); // i64.load
|
||||
fb.local_tee(e);
|
||||
fb.i64_const(32);
|
||||
fb.i64_const(gex_abi::FCACHE_TAG_BITS as i64);
|
||||
fb.byte(0x88); // i64.shr_u
|
||||
fb.byte(0xa7); // i32.wrap_i64
|
||||
fb.i32_const(gex_abi::ftab::RESOLVE as i32);
|
||||
fb.local_get(e);
|
||||
fb.i64_const(0xffff_ffff);
|
||||
fb.i64_const(gex_abi::FCACHE_TAG_MASK as i64);
|
||||
fb.byte(0x83); // i64.and
|
||||
self.b.push_operand(o);
|
||||
let fb = self.b.f();
|
||||
|
||||
@@ -343,21 +343,27 @@ pub fn translator() -> Option<&'static dyn Translator> {
|
||||
TRANSLATOR.get().map(|b| &**b)
|
||||
}
|
||||
|
||||
/// Write `addr → idx` into the probe cache in guest memory `mem`.
|
||||
/// Write `addr → idx` into the probe cache in guest memory `mem`. An
|
||||
/// address or index no entry can hold (`gex_abi::fcache_pack`) clears the
|
||||
/// slot instead, so it is left to miss.
|
||||
pub fn fcache_write(mem: &mut [u8], addr: u64, idx: u32) {
|
||||
let at = gex_abi::fcache_entry(addr) as usize;
|
||||
let v = gex_abi::fcache_pack(addr, idx).unwrap_or(FCACHE_EMPTY);
|
||||
if let Some(e) = mem.get_mut(at..at + 8) {
|
||||
e.copy_from_slice(&gex_abi::fcache_pack(addr, idx).to_le_bytes());
|
||||
e.copy_from_slice(&v.to_le_bytes());
|
||||
}
|
||||
}
|
||||
|
||||
/// An empty probe cache entry: address 0, the resolve stub.
|
||||
const FCACHE_EMPTY: u64 = 0;
|
||||
|
||||
/// Empty the whole probe cache: every entry the resolve stub. For a memory
|
||||
/// whose cache names another table's indices -- an `execve`'s new program,
|
||||
/// or a fork's child, whose low region (and the cache in it) is a copy of
|
||||
/// the parent's while its table is its own.
|
||||
pub fn fcache_reset(mem: &mut [u8]) {
|
||||
let (lo, hi) = (gex_abi::FCACHE as usize, (gex_abi::FCACHE + gex_abi::FCACHE_BYTES) as usize);
|
||||
let empty = gex_abi::fcache_pack(0, RESOLVE).to_le_bytes();
|
||||
let empty = FCACHE_EMPTY.to_le_bytes();
|
||||
if let Some(c) = mem.get_mut(lo..hi) {
|
||||
for e in c.chunks_exact_mut(8) {
|
||||
e.copy_from_slice(&empty);
|
||||
@@ -371,8 +377,8 @@ pub fn fcache_clear(mem: &mut [u8], addr: u64) {
|
||||
let at = gex_abi::fcache_entry(addr) as usize;
|
||||
if let Some(e) = mem.get_mut(at..at + 8) {
|
||||
let v = u64::from_le_bytes(e.try_into().unwrap());
|
||||
if v & 0xffff_ffff == addr & 0xffff_ffff {
|
||||
e.copy_from_slice(&gex_abi::fcache_pack(0, RESOLVE).to_le_bytes());
|
||||
if v & gex_abi::FCACHE_TAG_MASK == addr {
|
||||
e.copy_from_slice(&FCACHE_EMPTY.to_le_bytes());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -548,7 +554,7 @@ mod tests {
|
||||
fcache_write(&mut mem, 0x40_1234, 99);
|
||||
let at = gex_abi::fcache_entry(0x40_1234) as usize;
|
||||
let v = u64::from_le_bytes(mem[at..at + 8].try_into().unwrap());
|
||||
assert_eq!((v as u32, (v >> 32) as u32), (0x40_1234, 99));
|
||||
assert_eq!(gex_abi::fcache_probe(v, 0x40_1234), 99);
|
||||
// Another address sharing the slot does not clear it.
|
||||
fcache_clear(&mut mem, 0x40_1235);
|
||||
assert_eq!(u64::from_le_bytes(mem[at..at + 8].try_into().unwrap()), v);
|
||||
@@ -556,6 +562,37 @@ mod tests {
|
||||
assert_eq!(u64::from_le_bytes(mem[at..at + 8].try_into().unwrap()), 0);
|
||||
}
|
||||
|
||||
/// A 64-bit memory's code above 4 GiB: an entry names its whole address,
|
||||
/// so a function at t + 4 GiB (the same slot, the same low 32 bits) is
|
||||
/// never taken for the one at t, and the one above 4 GiB hits.
|
||||
#[test]
|
||||
fn probe_cache_entries_above_4_gib() {
|
||||
let mut mem = vec![0u8; (gex_abi::FCACHE + gex_abi::FCACHE_BYTES) as usize];
|
||||
let (t, high) = (0x2000_0000u64, 0x1_2000_0000u64);
|
||||
assert_eq!(gex_abi::fcache_entry(t), gex_abi::fcache_entry(high));
|
||||
let at = gex_abi::fcache_entry(t) as usize;
|
||||
let entry = |mem: &[u8]| u64::from_le_bytes(mem[at..at + 8].try_into().unwrap());
|
||||
fcache_write(&mut mem, high, 7);
|
||||
assert_eq!(gex_abi::fcache_probe(entry(&mem), high), 7);
|
||||
assert_eq!(gex_abi::fcache_probe(entry(&mem), t), RESOLVE);
|
||||
fcache_write(&mut mem, t, 8);
|
||||
assert_eq!(gex_abi::fcache_probe(entry(&mem), t), 8);
|
||||
assert_eq!(gex_abi::fcache_probe(entry(&mem), high), RESOLVE);
|
||||
// A Wine image near the top of a 16 GiB memory.
|
||||
fcache_write(&mut mem, 0x3_ffdd_1ce4, 9);
|
||||
let at = gex_abi::fcache_entry(0x3_ffdd_1ce4) as usize;
|
||||
assert_eq!(gex_abi::fcache_probe(u64::from_le_bytes(mem[at..at + 8].try_into().unwrap()), 0x3_ffdd_1ce4), 9);
|
||||
// The other address clearing the slot leaves it; its own clears it.
|
||||
fcache_clear(&mut mem, 0x3_ffdd_1ce4 + (1 << 32));
|
||||
assert_ne!(u64::from_le_bytes(mem[at..at + 8].try_into().unwrap()), 0);
|
||||
fcache_clear(&mut mem, 0x3_ffdd_1ce4);
|
||||
assert_eq!(u64::from_le_bytes(mem[at..at + 8].try_into().unwrap()), 0);
|
||||
// What no entry can hold misses rather than wraps.
|
||||
fcache_write(&mut mem, 1 << 40, 3);
|
||||
let at = gex_abi::fcache_entry(1 << 40) as usize;
|
||||
assert_eq!(u64::from_le_bytes(mem[at..at + 8].try_into().unwrap()), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn custom_sections_parse() {
|
||||
// A module with only a `gex.funcs` section listing two addresses.
|
||||
|
||||
@@ -426,12 +426,12 @@ fn build(u: &Unit, plan: &Plan) -> Result<Vec<u8>> {
|
||||
s.instruction(&I::I32Const(probe as i32));
|
||||
s.instruction(&I::I64Load(wasm_encoder::MemArg { offset: gex_abi::FCACHE, align: 3, memory_index: 0 }));
|
||||
s.instruction(&I::LocalTee(4));
|
||||
s.instruction(&I::I64Const(32));
|
||||
s.instruction(&I::I64Const(gex_abi::FCACHE_TAG_BITS as i64));
|
||||
s.instruction(&I::I64ShrU);
|
||||
s.instruction(&I::I32WrapI64);
|
||||
s.instruction(&I::I32Const(gex_abi::ftab::RESOLVE as i32));
|
||||
s.instruction(&I::LocalGet(4));
|
||||
s.instruction(&I::I64Const(0xffff_ffff));
|
||||
s.instruction(&I::I64Const(gex_abi::FCACHE_TAG_MASK as i64));
|
||||
s.instruction(&I::I64And);
|
||||
s.instruction(&I::I64Const(addr as i64));
|
||||
s.instruction(&I::I64Eq);
|
||||
|
||||
@@ -463,6 +463,8 @@ const host = new ProcessHost({
|
||||
trapStack: process.env.GEX_TRAP_STACK ? 1 : undefined,
|
||||
// GEX_DOOR_STATS: wineserver door call counts every 10 s.
|
||||
doorStats: process.env.GEX_DOOR_STATS ? Number(process.env.GEX_DOOR_STATS) || 1 : undefined,
|
||||
// GEX_FCACHE_STATS=1: the function probe cache's misses, every 10 s.
|
||||
fcacheStats: process.env.GEX_FCACHE_STATS ? 1 : undefined,
|
||||
// GEX_HEARTBEAT: say where the guest is every N safepoints and every 200
|
||||
// dispatch misses, so a slow run can be told from a stuck one.
|
||||
heartbeat: process.env.GEX_HEARTBEAT ? Number(process.env.GEX_HEARTBEAT) : 0,
|
||||
|
||||
@@ -1069,6 +1069,7 @@ async function handle(msg) {
|
||||
if (msg.phases || msg.opts?.phases) gex.shim.exports.gex_set_phases?.(msg.phases || msg.opts.phases);
|
||||
if (msg.inPlace === 0) gex.shim.exports.gex_set_in_place?.(0);
|
||||
if (msg.excTrace) gex.shim.exports.gex_set_exc_trace?.(1);
|
||||
if (msg.fcacheStats) gex.shim.exports.gex_set_fcache_stats?.(1);
|
||||
if (msg.winsize) {
|
||||
gex.shim.exports.gex_set_winsize?.(msg.winsize.rows, msg.winsize.cols);
|
||||
}
|
||||
|
||||
@@ -1748,6 +1748,9 @@ pub extern "C" fn gex_ftab_size() -> u32 {
|
||||
#[no_mangle]
|
||||
pub extern "C" fn gex_ftab_lookup(_ctx: i32, addr: i64) -> i32 {
|
||||
let addr = addr as u64;
|
||||
if FCACHE_STATS.load(Ordering::Relaxed) {
|
||||
fcache_stat(addr);
|
||||
}
|
||||
// Filler no function has (`STRAY_PC`): the fallback's.
|
||||
if addr == STRAY_PC {
|
||||
return gex_abi::ftab::FALLBACK as i32;
|
||||
@@ -1804,6 +1807,51 @@ pub extern "C" fn gex_ftab_lookup(_ctx: i32, addr: i64) -> i32 {
|
||||
idx as i32
|
||||
}
|
||||
|
||||
/// `?fcachestats=1` (GEX_FCACHE_STATS=1): the probe cache's misses (every
|
||||
/// `gex.lookup` is one), said by each Worker at most every 10 s, with how
|
||||
/// many were above 4 GiB and the addresses that missed most. A debugging
|
||||
/// opt-in: nothing is counted with it off.
|
||||
static FCACHE_STATS: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn gex_set_fcache_stats(on: i32) {
|
||||
FCACHE_STATS.store(on != 0, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
/// This Worker's misses since its last line: (since ns, above 4 GiB, by address).
|
||||
static FCACHE_MISSES: std::cell::RefCell<(u64, u64, std::collections::HashMap<u64, u64>)> = Default::default();
|
||||
}
|
||||
|
||||
fn fcache_stat(addr: u64) {
|
||||
FCACHE_MISSES.with(|m| {
|
||||
let mut m = m.borrow_mut();
|
||||
let now = crate::now_ns();
|
||||
if m.0 == 0 {
|
||||
m.0 = now;
|
||||
}
|
||||
m.1 += (addr >> 32 != 0) as u64;
|
||||
*m.2.entry(addr).or_insert(0) += 1;
|
||||
let secs = now.saturating_sub(m.0) as f64 / 1e9;
|
||||
if secs < 10.0 {
|
||||
return;
|
||||
}
|
||||
let n: u64 = m.2.values().sum();
|
||||
let mut top: Vec<(u64, u64)> = m.2.iter().map(|(a, c)| (*c, *a)).collect();
|
||||
top.sort_unstable_by(|a, b| b.cmp(a));
|
||||
let top: Vec<String> = top.iter().take(5).map(|(c, a)| format!("{a:#x} {c}")).collect();
|
||||
crate::warn_line(&format!(
|
||||
"[gex] fcache: pid {} {n} probe misses in {secs:.1} s ({:.0}/s), {} above 4 GiB, {} addresses; most: {}\n",
|
||||
crate::gex_my_pid(),
|
||||
n as f64 / secs,
|
||||
m.1,
|
||||
m.2.len(),
|
||||
top.join(", ")
|
||||
));
|
||||
*m = (now, 0, Default::default());
|
||||
});
|
||||
}
|
||||
|
||||
/// The table's index 1: code no function body holds, under the `F_body`
|
||||
/// contract (the call-out, docs/function-tier.md §7.3, as natively): tier-1
|
||||
/// runs the target nested -- JavaScript's loop inside this call -- until the
|
||||
|
||||
Executable
+64
@@ -0,0 +1,64 @@
|
||||
#!/bin/sh
|
||||
# The function table's probe cache in a 64-bit memory (gex_abi::FCACHE): two
|
||||
# functions 4 GiB apart, at t and t + 4 GiB, share a slot (the slot hashes the
|
||||
# low 32 bits) and their low 32 bits. Called in turn through pointers from
|
||||
# the function JIT's code, each call must reach its own function: an entry
|
||||
# that kept only the low 32 bits sent every call at t to the function at
|
||||
# t + 4 GiB (and a 64-bit memory's images, near the top of its 16 GiB, never
|
||||
# hit at all).
|
||||
#
|
||||
# tests/browser-mem64-fcache.sh
|
||||
#
|
||||
# Needs zig, node, and in web/public/gex the 64-bit memory's x86 shim and
|
||||
# seed (GEX_ARCH=x86 GEX_SHARED=1 GEX_MEM64=1 shims/browser/build.sh).
|
||||
set -eu
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
WORK="${TMPDIR:-/tmp}/gex-mem64-fcache-$$"
|
||||
export GEX_ARCH=x86
|
||||
P="$ROOT/web/public/gex"
|
||||
for need in "$P/gex_browser.x86.m64.shared.wasm" "$P/seed.x86.m64.shared.wasm"; do
|
||||
[ -e "$need" ] || { echo "skip: $need is not built" >&2; exit 0; }
|
||||
done
|
||||
command -v zig >/dev/null || { echo "skip: no zig to build the guest" >&2; exit 0; }
|
||||
mkdir -p "$WORK"
|
||||
[ -n "${KEEP:-}" ] || trap 'rm -rf "$WORK"' EXIT
|
||||
|
||||
cat > "$WORK/fc.c" <<'EOF2'
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <sys/mman.h>
|
||||
typedef int (*fn)(void);
|
||||
static void *code_at(uint64_t a, int v) {
|
||||
unsigned char *p = mmap((void *)a, 4096, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED_NOREPLACE, -1, 0);
|
||||
if (p == MAP_FAILED || (uint64_t)p != a) return 0;
|
||||
p[0] = 0xb8; memcpy(p + 1, &v, 4); p[5] = 0xc3;
|
||||
mprotect(p, 4096, PROT_READ | PROT_EXEC);
|
||||
return p;
|
||||
}
|
||||
__attribute__((noinline)) static long calls(fn *f, int n) {
|
||||
long s = 0;
|
||||
for (int i = 0; i < n; i++) s += f[i & 1]();
|
||||
return s;
|
||||
}
|
||||
int main(void) {
|
||||
setvbuf(stdout, 0, _IONBF, 0);
|
||||
uint64_t t = 0x20000000ull;
|
||||
fn f[2] = { (fn)code_at(t, 1), (fn)code_at(t + (1ull << 32), 1000) };
|
||||
if (!f[0] || !f[1]) { printf("FAIL: no code at %#llx and %#llx\n", (unsigned long long)t, (unsigned long long)(t + (1ull << 32))); return 1; }
|
||||
long want = 0, got = 0;
|
||||
for (int r = 0; r < 200; r++) { got += calls(f, 1000); want += 500L * 1 + 500L * 1000; }
|
||||
printf("%s: %ld calls through functions at %#llx and %#llx (4 GiB apart): sum %ld, want %ld\n",
|
||||
got == want ? "ok" : "FAIL", 200L * 1000, (unsigned long long)t, (unsigned long long)(t + (1ull << 32)), got, want);
|
||||
return got != want;
|
||||
}
|
||||
EOF2
|
||||
zig cc -target x86_64-linux-musl -O2 -static -o "$WORK/fc" "$WORK/fc.c"
|
||||
GEX_SHARED=1 GEX_MEM64=1 GEX_FJIT=1 timeout "${TMO:-180}" node "$ROOT/shims/browser/js/run-node.mjs" --file /g/fc="$WORK/fc" --argv0 /g/fc "$P/seed.x86.m64.shared.wasm" "$WORK/fc" > "$WORK/out" 2> "$WORK/err" || true
|
||||
got=$(grep -E '^(ok|FAIL)' "$WORK/out" || true)
|
||||
case $got in
|
||||
ok:*) echo "$got"; exit 0 ;;
|
||||
esac
|
||||
echo "FAIL: the probe cache in a 64-bit memory: ${got:-no result}" >&2
|
||||
tail -8 "$WORK/err" >&2
|
||||
exit 1
|
||||
@@ -23,7 +23,7 @@ static uint32_t ftab_idx(uint64_t t)
|
||||
}
|
||||
uint64_t slot = (uint32_t)((uint32_t)t * 0x9e3779b1u) >> (32 - fcache_bits);
|
||||
uint64_t e = __atomic_load_n((uint64_t *)(uintptr_t)(fcache + 8 * slot), __ATOMIC_RELAXED);
|
||||
return (e & 0xffffffffull) == (t & 0xffffffffull) ? (uint32_t)(e >> 32) : 0;
|
||||
return (e & ((1ull << 36) - 1)) == t ? (uint32_t)(e >> 36) : 0 /* gex_abi::FCACHE_TAG_BITS, 36 */;
|
||||
}
|
||||
|
||||
static uint64_t ctx_v;
|
||||
|
||||
@@ -106,7 +106,7 @@ static uint32_t ftab_idx(uint64_t t)
|
||||
}
|
||||
uint64_t slot = (uint32_t)((uint32_t)t * 0x9e3779b1u) >> (32 - fcache_bits);
|
||||
uint64_t e = __atomic_load_n((uint64_t *)(uintptr_t)(fcache + 8 * slot), __ATOMIC_RELAXED);
|
||||
return (e & 0xffffffffull) == (t & 0xffffffffull) ? (uint32_t)(e >> 32) : 0; /* RESOLVE */
|
||||
return (e & ((1ull << 36) - 1)) == t ? (uint32_t)(e >> 36) : 0 /* gex_abi::FCACHE_TAG_BITS, 36 */; /* RESOLVE */
|
||||
}
|
||||
|
||||
void gexp_call_guest(uint64_t fn, gexp_call *c)
|
||||
|
||||
@@ -964,6 +964,8 @@ export function setGlobals(get, b = {}) {
|
||||
globalThis.GEX_ARCH = b.arch === 'aarch64' ? 'aarch64' : 'x86';
|
||||
if (get('trapstack')) globalThis.GEX_TRAP_STACK = 1;
|
||||
if (get('doorstats')) globalThis.GEX_DOOR_STATS = Number(get('doorstats')) || 1;
|
||||
// fcachestats=1: the function probe cache's misses, every 10 s (cgen.rs `fcache_stat`).
|
||||
if (get('fcachestats')) globalThis.GEX_FCACHE_STATS = 1;
|
||||
if (get('exittrace')) globalThis.GEX_EXIT_TRACE = 1;
|
||||
if (get('callouttrace')) globalThis.GEX_CALLOUT_TRACE = 1;
|
||||
// straytrap=1: a return to native Wine's pad past open call-outs stays a breakpoint (cgen.rs STRAY_TRAP).
|
||||
|
||||
@@ -1081,6 +1081,7 @@ async function runProgramHere(
|
||||
// `?doorstats=1`: wineserver door call counts every 10 s (docs/wineserver-kernel.md).
|
||||
// (a number past 1: the line's period in ms)
|
||||
doorStats: globalThis.GEX_DOOR_STATS || undefined,
|
||||
fcacheStats: globalThis.GEX_FCACHE_STATS || undefined,
|
||||
// `?timescale=N&fixedtime=S` (benchmarks only): the monotonic clocks
|
||||
// N times real speed, the wall clock starting at Unix time S, as
|
||||
// pewasm's PEWASM_TIME_SCALE and PEWASM_FIXED_TIME.
|
||||
|
||||
Reference in new issue
Block a user