You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
For benchmarking: --clock-mhz=N, report emulated time from ReadEClock() #102
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.
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
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)
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
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.
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.
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.
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.
Also worth knowing: --clock-mhz refuses --jitand--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.
Activity
[-]Report emulated time from ReadEClock, for benchmarking: --clock-mhz=N with --no-jit[/-][+]For benchmarking: --clock-mhz=N, report emulated time from ReadEClock()[/+]sidick commentedon Sep 29, 2026
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
Half true.
run_for_cyclesexists inm68k 0.14.3and does return a realcyclestotal — but volamos never calls it.Cpu::run(
crates/volamos-core/src/backend.rs) goes throughrun_batch, andBatchResultcarries onlyinstructions; there is no cycle field on that pathat all. And
run_for_cycles_inneropens withset_precise_bus(true)and stepsone 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-mhzis given". Still straightforward:CycleBatchExitmaps one-for-one onto the
BatchExitthe run loop already handles, and there'sonly one
Cpuimpl 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:precise_bus=true(what--clock-mhzwould use)--no-jit(run_batch(1),precise_bus=false+fast_mem)--jitSo roughly 5x slower than
--jit, 2x slower than today's--no-jit. Forreplacing 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-mhzthe slowest mode volamoshas, 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 areal A4000 with a 25 MHz A3640, using Copperline's own
timing-test/accelprobe.asmcaptures. Since Copperline drives the same crate,at your
clock_mhz = 25.0you should be comparing two consumers of onehardware-calibrated model rather than two independent guesses.
Caveats — please read these before trusting a ratio
volamos has no wait states.
impl AddressBus for FlatMemoryimplementsno
syncand no bus timing — guest memory is one flatVec<u8>. The 040timing 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.
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/unclassifiedcosts. If any of the nine BenchWork workloads lean on float code, treat that
arm as unvalidated.
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
CopyMemwill report an emulated time implying it spentnone.
On
--jitYou offered "refuse or warn". I'd rather refuse, and hard. Under
--jitthecycle count isn't approximate, it's absent —
run_batchproduces no cyclenumber to be approximate with. So
--clock-mhztogether with--jitwouldhave 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:
--sanitizeforces the JIT off for its owncorrectness reasons.
I'll make
--clock-mhzimply the precise path and error out if--jitwasasked for explicitly.
One design note
The E-clock rate in
D0stays at 709379 regardless of--clock-mhz. RealAmigas report the E-clock rate independently of CPU clock, so
--clock-mhzshould only govern the cycles-to-time conversion feeding
ev_hi/ev_lo, neverthe reported rate. That keeps every existing caller's interval arithmetic
correct and means
GetSysTime/DateStampcan stay on the host clock — youdon'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.
Address review: refuse --clock-mhz+--sanitize, fix perf claims/docs (#…
Add --clock-mhz: report emulated time from ReadEClock (issue #102) (#103
sidick commentedon Sep 29, 2026
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.
FlatMemoryimplements no
syncand no bus timing at all, while Copperline bills realChip/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 spentnone there), guest code reached via
Cpu::steprather thanCpu::rundoesn'taccumulate either (
RawDoFmtPutChProc callouts,Supervisorroutines), and anested
System()/Execute()gets a fresh counter, so bracketing a childprocess 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-mhzrefuses--jitand--sanitize, both ashard errors.
--jitbecauserun_batchtracks no cycle count whatsoever, sothere'd be nothing to approximate with;
--sanitizebecause the cycle-countedpath 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.
Bump version to 0.9.0 (#119)