Files
rk65c02/test/bench_functional.c
Radosław KujawaandClaude Fable 5 4dbd0aea86 jit: chain branches inside blocks; native guarded loops
Conditional branches no longer end compiled blocks. The not-taken path
falls through into the next inline instruction; a taken branch whose
target is an instruction boundary inside the same block becomes a
native jump to that instruction's label (per-insn labels, patched via
jit_patch_at). Backward edges - loops - pass a guard that returns to
the dispatcher when the per-dispatch loop budget (JIT_LOOP_BUDGET,
default 256, -D-overridable) is exhausted, a host stop is requested,
or the IRQ line is asserted, bounding tick/stop/IRQ latency inside
native loops. Taken branches to external targets store PC and return
as before. No cross-block links are created, so SMC invalidation
still destroys exactly one self-contained Lightning function; the
differential SMC fuzzer and the whole suite stay green.

Blocks also end at STP/WAI now (bytes after them never execute before
a re-dispatch, and decoding past them extended covered spans into
adjacent data). The back-edge guard accumulates loop spans into
stats.insns_executed (exact for straight lines and back-edges,
approximate when an early-exit branch leaves mid-block).

The functional-test monitors needed hardening, not the JIT: PC-only
stability detection aliases with long loops sampled at dispatch
boundaries (chaining makes those PCs highly repetitive - at tick
interval 64 even the interpreter false-detected a trap). Traps are now
recognized only when the full register file is frozen AND the stable
PC holds a branch-to-self or jmp-to-self instruction.

Measured (medians of 3, tick 1024 for ROMs; baselines from the
attribution commit):
  6502_functional:  2.59x -> 4.28x  (26.5 insns/block, was 4.9; 102 MIPS)
  65C02_extended:   3.28x -> 5.36x  (27-32 insns/block; 141 MIPS)
  MS BASIC bench:   2.5x  -> 3.0x   (0.85s vs 2.57s; 8.96 insns/block)
  bench_loop:       5.0x  -> 8.1x   (whole benchmark is one native block)
  smcdata:          6.5x  -> 8.4x
  bench_mmu:        unchanged within noise

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-26 20:00:19 +02:00

255 lines
6.5 KiB
C

/*
* Standalone benchmark: run the Klaus Dormann functional ROMs with
* interpreter and with JIT, report elapsed time and JIT statistics.
* The functional ROMs contain real self-modifying code, so this is the
* primary workload for evaluating SMC handling under JIT.
*
* Build: make bench_functional (functional_tests must be built)
* Run: ./bench_functional (from the test/ directory)
*/
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <inttypes.h>
#ifdef __linux__
#include <time.h>
static double
now_sec(void)
{
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return ts.tv_sec + ts.tv_nsec / 1e9;
}
#else
#include <sys/time.h>
static double
now_sec(void)
{
struct timeval tv;
gettimeofday(&tv, NULL);
return tv.tv_sec + tv.tv_usec / 1e6;
}
#endif
#include "bus.h"
#include "device_ram.h"
#include "rk65c02.h"
#include "log.h"
#define ROM_LOAD_ADDR 0xC000
/* Success trap addresses, same as test_functional.c. */
#define PC_SUCCESS_6502_DECIMAL 0xC04B
#define PC_SUCCESS_6502_FUNCTIONAL 0xF089
#define PC_SUCCESS_65C02_EXTENDED 0xE111
struct functional_case {
const char *rom_name;
uint16_t success_pc;
uint64_t poll_limit;
};
static const struct functional_case cases[] = {
{ "functional_tests/6502_decimal_test.bin",
PC_SUCCESS_6502_DECIMAL, 600000000ULL },
{ "functional_tests/6502_functional_test.bin",
PC_SUCCESS_6502_FUNCTIONAL, 6000000000ULL },
{ "functional_tests/65C02_extended_opcodes_test.bin",
PC_SUCCESS_65C02_EXTENDED, 6000000000ULL },
};
struct functional_monitor {
uint16_t success_pc;
reg_state_t last_regs;
uint32_t stable_pc_count;
uint32_t stable_threshold;
uint64_t poll_count;
uint64_t poll_limit;
bool initialized;
bool passed;
bool finished;
};
/*
* A Dormann trap is a self-loop: a relative branch with offset $FE or a
* JMP absolute to itself. PC stability alone is not enough - long genuine
* loops alias with tick sampling (especially under JIT block chaining,
* where dispatch-boundary PCs repeat), so confirm the instruction.
*/
static bool
is_self_loop_insn(rk65c02emu_t *e, uint16_t pc)
{
uint8_t op = bus_read_1(e->bus, pc);
switch (op) {
case 0x10: case 0x30: case 0x50: case 0x70:
case 0x90: case 0xB0: case 0xD0: case 0xF0:
case 0x80:
return bus_read_1(e->bus, (uint16_t)(pc + 1)) == 0xFE;
case 0x4C: {
uint16_t t = bus_read_1(e->bus, (uint16_t)(pc + 1))
| (bus_read_1(e->bus, (uint16_t)(pc + 2)) << 8);
return t == pc;
}
default:
return false;
}
}
static void
functional_tick(rk65c02emu_t *e, void *ctx)
{
struct functional_monitor *m = ctx;
uint16_t pc;
pc = e->regs.PC;
m->poll_count++;
/*
* A genuine trap self-loop makes no progress: the whole register
* file is frozen, not just PC. Comparing only PC aliases with
* long loops sampled at their entry point (the sampled PC can even
* sit on a not-taken conditional trap instruction).
*/
if (!(m->initialized)
|| (pc != m->last_regs.PC) || (e->regs.A != m->last_regs.A)
|| (e->regs.X != m->last_regs.X) || (e->regs.Y != m->last_regs.Y)
|| (e->regs.SP != m->last_regs.SP) || (e->regs.P != m->last_regs.P)) {
m->initialized = true;
m->last_regs = e->regs;
m->stable_pc_count = 1;
} else {
m->stable_pc_count++;
}
if (m->stable_pc_count >= m->stable_threshold) {
if (!is_self_loop_insn(e, pc)) {
/* Sampling alias with a long loop, not a trap. */
m->stable_pc_count = 1;
return;
}
m->finished = true;
m->passed = (pc == m->success_pc);
rk65c02_request_stop(e);
return;
}
if (m->poll_count >= m->poll_limit) {
m->finished = true;
m->passed = false;
rk65c02_request_stop(e);
}
}
/* Tick interval for rk65c02_tick_set (0 = every check); argv[1]. */
static uint32_t tick_interval;
/* Returns elapsed seconds, negative on failure. */
static double
run_case(const struct functional_case *fcase, bool use_jit,
rk65c02_jit_stats_t *stats, bool *stats_valid)
{
rk65c02emu_t e;
bus_t b;
struct functional_monitor monitor = { 0 };
double t0, t1;
bool ok;
b = bus_init();
bus_device_add(&b, device_ram_init(0xDFFF), 0x0000);
bus_device_add(&b, device_ram_init(0x2001), 0xDFFF);
e = rk65c02_init(&b);
rk65c02_jit_enable(&e, use_jit);
rk65c02_jit_flush(&e);
rk65c02_jit_stats_reset(&e);
monitor.success_pc = fcase->success_pc;
monitor.stable_threshold = 256;
monitor.poll_limit = fcase->poll_limit;
if (!bus_load_file(&b, 0x0000, fcase->rom_name)) {
fprintf(stderr, "cannot load ROM: %s\n", fcase->rom_name);
bus_finish(&b);
return -1.0;
}
e.regs.PC = ROM_LOAD_ADDR;
e.regs.SP = 0xFF;
e.regs.A = 0x00;
e.regs.X = 0x00;
e.regs.Y = 0x00;
rk65c02_tick_set(&e, functional_tick, tick_interval, &monitor);
t0 = now_sec();
rk65c02_start(&e);
t1 = now_sec();
rk65c02_tick_clear(&e);
ok = (e.stopreason == HOST) && monitor.finished && monitor.passed;
if (stats != NULL)
*stats_valid = rk65c02_jit_stats_get(&e, stats);
bus_finish(&b);
if (!ok) {
fprintf(stderr, "%s (%s): did not pass (terminal pc=$%04x)\n",
fcase->rom_name, use_jit ? "jit" : "interp",
monitor.last_regs.PC);
return -1.0;
}
return t1 - t0;
}
int
main(int argc, char **argv)
{
size_t i;
int rv = 0;
if ((argc > 1) && (argv[1] != NULL))
tick_interval = (uint32_t)strtoul(argv[1], NULL, 0);
rk65c02_loglevel_set(LOG_ERROR);
printf("tick interval: %u\n", (unsigned)tick_interval);
for (i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) {
const struct functional_case *fc = &cases[i];
rk65c02_jit_stats_t st = { 0 };
bool st_valid = false;
double t_interp, t_jit;
t_interp = run_case(fc, false, NULL, NULL);
t_jit = run_case(fc, true, &st, &st_valid);
if ((t_interp < 0) || (t_jit < 0)) {
rv = 1;
continue;
}
printf("%s\n", fc->rom_name);
printf(" interpreter: %.3f s\n", t_interp);
printf(" JIT: %.3f s (speedup %.2f x)\n", t_jit,
(t_jit > 0) ? t_interp / t_jit : 0.0);
if (st_valid) {
printf(" jit stats: write_events=%" PRIu64
" blocks_invalidated=%" PRIu64
" blocks_compiled=%" PRIu64 "\n"
" blocks_executed=%" PRIu64
" pages_demoted=%" PRIu64
" run_jit_disables=%" PRIu64 "\n",
st.write_events, st.blocks_invalidated,
st.blocks_compiled, st.blocks_executed,
st.pages_demoted, st.run_jit_disables);
if (st.blocks_executed > 0)
printf(" insns_executed=%" PRIu64
" avg insns/block=%.2f jit MIPS=%.1f\n",
st.insns_executed,
(double)st.insns_executed / (double)st.blocks_executed,
(t_jit > 0) ? (double)st.insns_executed / t_jit / 1e6 : 0.0);
}
}
return rv;
}