Skip to content

For benchmarking: --clock-mhz=N, report emulated time from ReadEClock() #102

Closed

Description

@codewiz

I benchmark compiler changes on Copperline today, which takes a boot, a serial shell and about 20 minutes per A/B. volamos would be far cheaper for the same job - one command, seconds - if its clock reported emulated time instead of host time.

Today

ReadEClock derives its ticks from the host wall clock (crates/volamos-core/src/exectask.rs, "deriving the tick count from the same wall clock as host_time_secs_micro"). Correct for interval arithmetic, but it measures how fast the host ran the emulation, not what the guest would have cost.

That makes timings unusable for comparing two compilers. Measuring the same nine BenchWork workloads under volamos and under cycle-paced Copperline, the ratio between them ranges from about 7x (dhrystone) to 27x (a memcpy loop): host time is cheap for memory-heavy code and expensive for the 68040, so the distortion is per-workload and does not cancel out.

Request

  • --clock-mhz=N, and with it ReadEClock derived from the core's emulated
    cycle count at that clock rather than from the host clock. CpuCore::run_for_cycles
    already does the accounting, so the information is there.
  • Together with the existing --no-jit, that would match what Copperline's
    benchmark config does (jit = false, clock_mhz = 25.0,
    pacing_budget = "cycles") and make repeated runs identical regardless of host
    load. No pacing needed - it should still run flat out, just report emulated
    time.
  • If cycle accounting under --jit is approximate, refusing --clock-mhz with
    --jit, or warning, would be clearer than quietly reporting numbers that only
    roughly mean something.

Known limit, probably out of scope

Anything whose cost lands in an OS call stays unrepresentative, since the handlers are native Rust: CopyMem/CopyMemQuick in execmem.rs, for instance, would cost almost nothing in emulated cycles, so a benchmark of the runtime's memcpy would still need Copperline. Guest code is the part this would make measurable, which is most of what a compiler change affects.

Activity

  1. changed the title [-]Report emulated time from ReadEClock, for benchmarking: --clock-mhz=N with --no-jit[/-] [+]For benchmarking: --clock-mhz=N, report emulated time from ReadEClock()[/+] on Sep 29, 2026
  2. sidick commented on Sep 29, 2026

    @sidick
    Owner

    Yes, let's do this. It's a good fit and most of the machinery is there. A few
    corrections and caveats first, so the numbers you get out of it mean what you
    expect.

    One correction to the premise

    CpuCore::run_for_cycles already does the accounting, so the information is there.

    Half true. run_for_cycles exists in m68k 0.14.3 and does return a real
    cycles total — but volamos never calls it. Cpu::run
    (crates/volamos-core/src/backend.rs) goes through run_batch, and
    BatchResult carries only instructions; there is no cycle field on that path
    at all. And run_for_cycles_inner opens with set_precise_bus(true) and steps
    one instruction at a time, so it's the transaction-exact slow path by
    construction — no trace JIT, no raw-pointer fast_mem.

    So this isn't "surface a counter we already keep", it's "switch execution
    contracts when --clock-mhz is given". Still straightforward: CycleBatchExit
    maps one-for-one onto the BatchExit the run loop already handles, and there's
    only one Cpu impl to teach a cycle accessor.

    What it costs

    That precise path is exactly the one I benchmarked when fixing coremark
    throughput a while back, so I can price it without guessing. On
    coremark.amiga:

    path iter/sec
    stepping with precise_bus=true (what --clock-mhz would use) 105.9
    today's --no-jit (run_batch(1), precise_bus=false + fast_mem) 198.7
    today's --jit 533.5

    So roughly 5x slower than --jit, 2x slower than today's --no-jit. For
    replacing a 20-minute Copperline A/B with one command that's a rounding error,
    and it buys you the thing you actually want: numbers that don't move with host
    load.

    Worth being explicit that this makes --clock-mhz the slowest mode volamos
    has, so it wants to be opt-in for benchmarking only, not something the
    testsuite picks up by default.

    Why I think the agreement with Copperline will be decent

    The crate's cycle models are per-CPU (timing.rs, timing_020.rs,
    timing_040.rs, timing_060.rs), and the 040 model is calibrated against a
    real A4000 with a 25 MHz A3640, using Copperline's own
    timing-test/accelprobe.asm captures. Since Copperline drives the same crate,
    at your clock_mhz = 25.0 you should be comparing two consumers of one
    hardware-calibrated model rather than two independent guesses.

    Caveats — please read these before trusting a ratio

    1. volamos has no wait states. impl AddressBus for FlatMemory implements
      no sync and no bus timing — guest memory is one flat Vec<u8>. The 040
      timing model's own note is that "memory latency stays billed by the host bus
      per access", and volamos's host bus bills nothing. Copperline bills real
      Chip/Fast RAM contention. So your 27x memcpy case will improve enormously
      but will not converge on Copperline. Cycle counting removes the
      host-speed distortion; it does not remove the memory-system one. Anything
      memory-bound still belongs on Copperline.

    2. No FPU timing tables. I can't find per-model FPU cycle costs in
      timing_0*.rs; FPU ops look like they fall through to generic/unclassified
      costs. If any of the nine BenchWork workloads lean on float code, treat that
      arm as unvalidated.

    3. Everything in a native handler is free, as you already called out. Worth
      noting it's slightly worse than "out of scope": because volamos dispatches
      every library call through an A-line trap, a workload's OS time doesn't just
      go uncounted, it silently doesn't exist in the total. A run that spends half
      its wall time in CopyMem will report an emulated time implying it spent
      none.

    On --jit

    You offered "refuse or warn". I'd rather refuse, and hard. Under --jit the
    cycle count isn't approximate, it's absent — run_batch produces no cycle
    number to be approximate with. So --clock-mhz together with --jit would
    have to invent something, and a benchmarking flag that quietly invents its
    output is worse than one that won't start. There's precedent for the
    interaction shape already: --sanitize forces the JIT off for its own
    correctness reasons.

    I'll make --clock-mhz imply the precise path and error out if --jit was
    asked for explicitly.

    One design note

    The E-clock rate in D0 stays at 709379 regardless of --clock-mhz. Real
    Amigas report the E-clock rate independently of CPU clock, so --clock-mhz
    should only govern the cycles-to-time conversion feeding ev_hi/ev_lo, never
    the reported rate. That keeps every existing caller's interval arithmetic
    correct and means GetSysTime/DateStamp can stay on the host clock — you
    don't want a benchmark run to think it's 1978.

    Let me know if the wait-state caveat undermines the use case for you; if the
    workloads you care about are mostly ALU-bound it should be fine, and if
    they're mostly memory-bound this may not be worth either of our time.

  3. sidick commented on Sep 29, 2026

    @sidick
    Owner

    Merged in #103 — GitHub auto-closed this, so flagging one thing here before it
    drops off your radar.

    The caveat I raised above is now documented, and it's the one most likely to
    bite your BenchWork numbers: volamos bills no bus wait states. FlatMemory
    implements no sync and no bus timing at all, while Copperline bills real
    Chip/Fast RAM contention. So memory-bound workloads will improve enormously
    over host time but will not converge on Copperline. Your 27x memcpy loop is
    exactly that shape — expect it to stay divergent, and keep it on Copperline.
    Cycle counting removes the host-speed distortion, not the memory-system one.

    Three smaller ones in the same list: native Rust library handlers cost zero
    emulated cycles (a CopyMem-heavy workload reports a time implying it spent
    none there), guest code reached via Cpu::step rather than Cpu::run doesn't
    accumulate either (RawDoFmt PutChProc callouts, Supervisor routines), and a
    nested System()/Execute() gets a fresh counter, so bracketing a child
    process reads as ~zero elapsed.

    Full list: https://github.com/sidick/volamos/blob/main/userdocs/CLI-Reference.md#--clock-mhz-n
    (blob path rather than the docs site — that publishes on release only, and this
    hasn't shipped in one yet.)

    Also worth knowing: --clock-mhz refuses --jit and --sanitize, both as
    hard errors. --jit because run_batch tracks no cycle count whatsoever, so
    there'd be nothing to approximate with; --sanitize because the cycle-counted
    path skips the per-instruction shadow-map hooks, which would leave the checks
    silently not running rather than merely slow.

    Measured cost on coremark.amiga (--cpu 68020): --jit ~680 iter/s, --no-jit
    ~217, --clock-mhz 25 ~97 — about 2.2x slower than --no-jit, 7x than --jit.
    Fine against a 20-minute Copperline A/B, but it wants to stay opt-in rather than
    something the gcc testsuite picks up by default.

    Interested to see what the BenchWork ratios look like under it — if the
    ALU-bound workloads land close to Copperline and only the memory-bound ones
    drift, that's the expected shape and the split itself is useful information.

  4. added a commit that references this issue on Oct 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions