A benchmark of the QEMU emulator behind trynix, measured across its engine releases to check that each change to it was an improvement.
Real packages, cold and warm
Wall seconds to run each package in a fresh guest, first cold and then again warm, plus the time to boot to a shell.
Instruction classes
Millions of guest instructions per second on a fixed assembly loop per mechanism the emulator pays for, so a change names a mechanism rather than a workload. Higher is better.
What moved the numbers
The changes behind the releases, in order. performance.md and engine-execution.md carry the profiles and the dead ends.
The store share
first ruby -e 1: about 35 s to about 15 s
Packages reach the guest over a 9p share. The guest now caches the share instead of asking the host on every lookup, and resolves a path in one syscall instead of one per component.
Entropy
first run of any binary: about 50 s to 2.5 s
The snapshot is taken before the kernel has seeded its random pool, so the first program to ask for randomness spun in a jitter loop for most of a minute. The guest now has a hardware random source and reseeds at boot.
The clock
guest time was 3.3x slow; sleep 10 took 33 s, now 10
The snapshot was taken on a 3.29 GHz cycle counter and resumed on a 1 GHz one, so the guest's clock ran 3.3x slow. Both builds now count the same clock, and instruction-class numbers from before are corrected by the measured ratio.
Idle, and the engine's size
idle guest 1.45 to 0.39 cores; engine download 41 MB to 13 MB
The main loop never slept because the pty poll returned at once; it now waits, with 9p off that loop so the wait is safe. Stripping debug info from the engine cut the download to a third.
Correctness
no speed change; opencode runs at all
The newer CPU model exposed a POPCNT that answered from a stale register,
now fixed and checked in CI. opencode needs SSE4.2 and crashed until that fix, so its
chart starts here.
The backend
opencode --version 408 s to 171 s; one compiled block 349 to 1163 mips
Blocks now compile in batches of 64 after 32 runs instead of one module per block after 1500, tail-call their successor instead of returning to a C dispatcher, and keep the guest registers in wasm locals. What remains is generated code spread over about 48,000 blocks, cache-missing on every transition.
The transition
two- and four-block loops 1.5x to 1.9x; opencode --version warm 182 s to
173 s here, cold within noise
A jump now caches its successor's function index in its own block, a successor in the
same batch is called directly behind a guard on the live jump target, chained entries skip
the rewind check only dispatcher entries need, and 64-bit multiplies are emitted inline
instead of through a helper. The single hot block and the memory tests do not move; the
cold-code rows do because they carry an imul per block.