joeylib2/PERF.md

12 KiB

JoeyLib cross-port performance (UBER benchmark)

Regenerated 2026-07-05 (PERF-AUDIT-PLAN.md Phase 8, #25b) from same-day captures of all four ports. This supersedes the 2026-07-04 Phase 0 baseline; every phase of the audit fix plan (0-8) has landed, so these are the numbers the library actually ships with.

2026-07-06 update: the IIgs sprite column is now COMPILED. Finding #77 (the loader/segment-BSS + malloc-heap reservation that forced the codegen arena into the app's own BSS) was fixed toolchain-side and JOEY_IIGS_SPRITE_CODEGEN was re-enabled; the four IIgs sprite rows were re-captured (bench-iigs.sh) and jumped 5-25x (SaveUnder 104->543, Draw 48->907, RestoreUnder 110->568, SaveAndDraw 15->376 ops/sec), bit-identical to the interpreter goldens (diff-uber-hashes 45/45). The ST and DOS columns are unchanged from the 2026-07-05 capture.

Also 2026-07-06: the Amiga jlpFillRect partial-width path was moved onto the blitter (BLTAFWM/BLTALWM hardware edge masks + masked minterm), which lifted jlFillRect 16x16 567->1153, 80x80 71->269 (both from below-floor to well above it), and -- because they build on fillRect -- jlDrawLine V 144->269 and jlDrawRect 100x100 122->203, all hash-identical (Amiga 45/45). The one remaining Amiga below-floor cell is jlFillCircle r=40 (74%); a whole-circle asm routine was tried and did NOT help (per-span dispatch is not the cost -- the 8-way-symmetry overlap fill dominates), so it stays on the C span walker for now.

Correctness statement backing this table: all four ports produce BYTE-IDENTICAL pixels on every measured op and every correctness check -- 45/45 surface hashes match the frozen goldens in tests/goldens/uber/ (34 timed ops + 11 hashed checks; the PASS/FAIL checks -- palette round-trip, PRNG golden sequence, sprite clip round-trip, arena churn, sprite-from-surface -- pass everywhere). The Phase 0-era divergences (#75 tilePasteMono three-way, #78 ST fillCircle/tileFill, #87 IIgs circle outline) are all fixed.

Measurement honesty (read before trusting any number)

Three clock artifacts have shaped this table's history. Two are fixed, one is inherent -- know them before comparing rows:

  1. Per-iteration poll tax -- FIXED (Phase 0). The pre-audit bench loop called jlFrameCount() between every op; on the IIgs that was a GetTick toolbox chain (~600-800 cycles), so every small-op number carried the harness cost, not the op cost. Ops now run in adaptive batches per poll. Small-op numbers jumped 2-10x on every port when this landed; never compare against pre-Phase-0 captures.
  2. SEI tick loss -- INHERENT on IIgs, rows tagged below. The benchmark clock is jlFrameCount = the VBL-interrupt-driven GetTick. An op that holds SEI while it runs suppresses those interrupts, the clock loses ticks, and the op over-reports. The three SEI-holding rows -- jlSurfaceClear, jlFillRect 320x200, and jlStagePresent full -- are CLOCK-UNRELIABLE in the IIgs column. The physics check: a full-screen op moves 32000 bytes at ~6 cyc/word, so ~29 ops/sec is the hard ceiling at 2.8 MHz. The present row's 68 and the clear row's 35 sit above it -- inflated; the honest values are at or below the ceiling.
  3. ST frame-rate miscalibration -- FOUND and FIXED 2026-07-06; the ST column below is the corrected (millis-clock) capture. Before the fix, ops/sec was iters * jlFrameHz() / actualFrames and the ST's jlpFrameHz() hardcoded 50 -- but Hatari runs the emulated ST at ~60 Hz VBL, so every ST number was scaled by 50/60 ~= 0.83 (~17% pessimistic). Caught by cross-checking against UBER's independent Timer-C 200 Hz millis clock (refresh-independent), which ran ~16-22% faster; the Amiga's two clocks agreed exactly (FS-UAE = PAL 50 Hz), confirming it was ST-only. Two fixes landed: (a) UBER now computes ops/sec from jlMillisElapsed (refresh-independent on all four ports; identical to the old formula on IIgs where millis is GetTick-derived), and (b) ST jlpFrameHz() now reads the shifter resolution + sync-mode registers to return the true 50/60/71 Hz. The per-op UBER-CLK: log line reports frame-vs-millis as a standing cross-check (they now agree). ST cells rose ~17% (e.g. jlSpriteDraw 503->585, jlFillRect 80x80 97->115); DOS/IIgs/Amiga unchanged.

Baseline (2026-07-05): absolute ops/sec per port

The IIgs is the reference and the perf floor (every other port must meet or beat it -- project directive). Non-IIgs cells show absolute ops/sec plus percentage of the IIgs number; sub-100% cells are bolded as below-floor. tools/uber-perf-table emits this table directly; regenerate after any re-capture (Amiga needs TIMEOUT=600 -- the default cuts off the final checks).

Op IIgs (ops/sec) Amiga (ops/sec, % of IIGS) Atari ST (ops/sec, % of IIGS) DOS (ops/sec, % of IIGS)
jlSurfaceClear 35 75 (214%) 44 (126%) 70 (200%)
jlPaletteSet 1623 11503 (709%) 5256 (324%) 29474 (1816%)
jlScbSetRange 163 3953 (2425%) 2418 (1483%) 8474 (5199%)
jlDrawPixel 3423 5853 (171%) 3322 (97%) 23524 (687%)
jlDrawLine H 1143 1703 (149%) 1341 (117%) 5674 (496%)
jlDrawLine V 99 269 (272%) 120 (121%) 494 (499%)
jlDrawLine diag 42 40 (95%) 42 (100%) 110 (262%)
jlDrawRect 100x100 90 203 (226%) 100 (111%) 399 (443%)
jlDrawCircle r=16 306 285 (93%) 178 (58%) 424 (139%)
jlDrawCircle r=80 70 63 (90%) 40 (57%) 96 (137%)
jlFillRect 16x16 603 1153 (191%) 585 (97%) 2874 (477%)
jlFillRect 80x80 110 269 (245%) 115 (105%) 380 (345%)
jlFillRect 320x200 21 66 (314%) 38 (181%) 56 (267%)
jlFillCircle r=40 42 31 (74%) 75 (179%) 153 (364%)
jlSamplePixel 5223 7253 (139%) 4862 (93%) 44244 (847%)
jlTileFill 2043 2303 (113%) 1516 (74%) 8894 (435%)
jlTileCopy 1443 1853 (128%) 1307 (91%) 8404 (582%)
jlTileCopyMasked 624 953 (153%) 677 (108%) 3084 (494%)
jlTilePaste 1803 2303 (128%) 1544 (86%) 9454 (524%)
jlTileSnap 2643 3503 (133%) 2907 (110%) 18624 (705%)
jlSpriteSaveUnder 543 567 (104%) 833 (153%) 8964 (1651%)
jlSpriteDraw 907 953 (105%) 585 (64%) 6094 (672%)
jlSpriteRestoreUnder 568 553 (97%) 655 (115%) 5744 (1011%)
jlSpriteSaveAndDraw 376 332 (88%) 318 (85%) 4204 (1118%)
jlStagePresent full 68 359 (528%) 56 (82%) 354 (521%)
jlInputPoll 243 4003 (1647%) 1425 (586%) 3084 (1269%)
jlKeyDown 12303 39753 (323%) 22826 (186%) 76514 (622%)
jlKeyPressed 14043 38003 (271%) 22826 (163%) 74064 (527%)
jlMouseX 30123 96703 (321%) 59354 (197%) 153864 (511%)
joeyJoyConnected 12303 39303 (319%) 22283 (181%) 77144 (627%)
jlAudioFrameTick 50223 56503 (113%) 11724 (23%) 58664 (117%)
jlAudioIsPlayingMod 13503 56403 (418%) 29870 (221%) 127334 (943%)
surfaceMarkDirtyRect (via jlFillRect 32x32) 323 853 (264%) 376 (116%) 1474 (456%)

IIgs full-screen ceiling (~29 ops/sec). A full-screen op moves 32000 bytes = 16000 word-stores x 6 cyc (STA long,X / PEI, both 3 cyc/byte) / 2.8 MHz ~= 34 ms ~= 29 ops/sec maximum. So jlSurfaceClear, jlFillRect 320x200, and jlStagePresent full cannot legitimately exceed ~29; a higher reading means the benchmark clock was perturbed. The clock is jlFrameCount = the VBL-interrupt- driven GetTick, so an op that holds SEI while it runs can over- report its ops/sec. Treat full-screen IIgs numbers above ~29 as inflated and cross-check against the cycle model.

Reading notes

  • Sprite rows measure different code per port. DOS runs COMPILED sprites (finding #74 fixed 2026-07-05). Amiga/ST run their interpreted planar paths (compiled save/restore emitters are stubs by design, Phase 4 #27). The IIgs now runs COMPILED sprites too (finding #77 fixed 2026-07-06; JOEY_IIGS_SPRITE_CODEGEN back on) -- the IIgs sprite rows jumped 5-25x and the huge non-IIgs ratios on these rows collapsed accordingly, which re-exposes a real 68k sprite gap (see below).
  • The below-floor cells are the standing punch list (ST column corrected +17% by the 2026-07-06 clock fix, artifact #3 above -- many ST cells crossed above the floor; what remains is genuinely below). ST circles (57-58%) are the worst honest graphics gap -- pure-68000 constraint, no blitter allowed (STF baseline). The IIgs circle-outline case is a known structural loss (3-register 65816 vs 16-register 68000 Bresenham; see feedback memory). With IIgs sprites now compiled, a 68k sprite gap re-opened: ST jlSpriteDraw 64%, ST jlSpriteSaveAndDraw 85%, ST jlTileFill 74%, and Amiga jlSpriteRestoreUnder 97% + jlSpriteSaveAndDraw 88% remain below the IIgs floor (they were masked while the IIgs interpreted; ST jlSpriteRestoreUnder is now 115%, above floor, after the clock fix). The ST jlSpriteDraw case was investigated 2026-07-06 and is ARCHITECTURAL, the same root as the ST circle gap: DRAW is already compiled (JIT, shifts 0/1), but the word-interleaved planar layout forces move.b #imm,(d16,An) (~18 cyc) per plane byte, which loses in wall-time to the IIgs chunky sta (~5 cyc @ 2.8 MHz) -- ST actually BEATS the IIgs on the block-copy SAVE (833 vs 543) and only trails on the c2p+mask DRAW. The one real DRAW win (merge the two tile columns into one move.w per plane word) exists only at shift 0; the UBER benchmark draws at x=40 = shift 1, where the columns fall in different interleaved groups, so it is byte-bound and the metric cannot move. jlSpriteSaveAndDraw (85%) is save+draw and is dominated by that same DRAW (SAVE is already fast). ST jlAudioFrameTick at 23% is the YM mixer cost, tracked separately. Amiga jlFillCircle r=40 (74%) was probed 2026-07-06 with a full whole-circle asm routine (Bresenham + inline 4-plane long-fill span): it held 45/45 hashes but did NOT move the number (34 vs 31, within the coarse 11-iter sample), so the per-span C dispatch is NOT the bottleneck -- the asm was reverted. The real cost is the midpoint fill's 8-way symmetry filling each row 2-4x (idempotent overlap): the true lever is a scanline rewrite (one span per row, no overlap), which would roughly halve the byte writes and stay hash-identical. NB: this op is timed at batch=1 (~29 ms each) by the 50 Hz frame counter, a coarse measurement -- the same ~4-5x model-vs-measured gap shows on ST sprite-draw, so part of these two cells may be measurement, not silicon.
  • jlRandom has no timed row. Phase 8 (#43) rewrote its xorshift32 update in 16-bit halves on the IIgs only (~2-3x fewer cycles per call on the 65816; 32-bit shifts there are per-bit helper loops). Output is bit-identical -- host-proven over the golden sequence and a 50M-state sweep, and gated by UBER's random-golden check on all four ports.
  • Input predicates stay functions (Phase 8 #40 decision): the post-clock-fix numbers (12K-77K ops/sec) put call overhead nowhere near a real frame budget; macro forms in the public header were rejected as not worth the API exposure.
  • IIgs libc memset/memcpy remain ~4x slow (finding #79, toolchain-owned): llvm-mos runtime byte loops vs ORCA's MVN. The jlScbSetRange and jlPaletteSet IIgs rows carry that tax; library-side mitigations landed (#16 unrolled word copy, #21), the libc fix is flagged to the llvm816 session.

History

Pre-2026-07-04 tables (ORCA-era IIgs column, artifact-inflated DOS numbers, un-batched loops) live in git history only -- every conclusion drawn from them was re-derived from Phase 0 onward. The Phase 0 baseline (2026-07-04) and per-phase deltas are recorded in PERF-AUDIT-PLAN.md's progress tracker; headline gains since Phase 0: IIgs interpreted spriteDraw 18 -> 48 ops/sec (Phase 4), IIgs tilePasteMono asm 3.35x (Phase 7 #3), ST flood plane hooks 5.6x (Phase 7 #4), DOS paletteSet +92% / tileSnap +42% (Phases 2/5), DOS compiled sprites 284 -> ~6100 ops/sec (#74).