trynix benchmarks

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

patch 0002cache=loose on the 9p mount

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

reseed.crdrand and virtio-rng in machine.json

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

patch 0003

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

xterm-pty patchioeventfd=off 路 DWARF stripped

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

patch 0004 路 guest CPU raised to x86-64-v3

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

patch 0006

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

patch 0007

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.

Appendix