From 06d2f9ebbec486e0dfcb8a578211648f788aeb17 Mon Sep 17 00:00:00 2001 From: Irmen de Jong Date: Thu, 15 Oct 2026 22:08:00 +0200 Subject: [PATCH] when->on..goto: calibrate the jump-table sizeLimit per target --- .../prog8/compiler/astprocessing/AstChecker.kt | 3 --- .../src/prog8/ast/statements/AstStatements.kt | 18 ++++++++++++------ docs/source/todo.rst | 2 -- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/compiler/src/prog8/compiler/astprocessing/AstChecker.kt b/compiler/src/prog8/compiler/astprocessing/AstChecker.kt index bbf1daab2..09614823d 100644 --- a/compiler/src/prog8/compiler/astprocessing/AstChecker.kt +++ b/compiler/src/prog8/compiler/astprocessing/AstChecker.kt @@ -2442,9 +2442,6 @@ internal class AstChecker(private val program: Program, if(whenStmt.condition.constValue(program)!=null) errors.warn("when-value is a constant and will always result in the same choice", whenStmt.condition.position) - if(whenStmt.onGotoIndexOffset(program, options)!=null) - errors.info("when statement can be replaced with on..goto", whenStmt.position) - super.visit(whenStmt) } diff --git a/compilerAst/src/prog8/ast/statements/AstStatements.kt b/compilerAst/src/prog8/ast/statements/AstStatements.kt index 3ad63126c..f35864eb2 100644 --- a/compilerAst/src/prog8/ast/statements/AstStatements.kt +++ b/compilerAst/src/prog8/ast/statements/AstStatements.kt @@ -1614,10 +1614,15 @@ class When(var condition: Expression, * a jump table isn't possible or not worthwhile here. */ fun onGotoIndexOffset(program: Program, compilerOptions: CompilationOptions): Int? { - val sizeLimit = when { - compilerOptions.compTarget.cpu == CpuType.CPU65C02 -> 4 - compilerOptions.compTarget.cpu.is68k -> 5 - else -> 6 + // Jump-table cutoff per CPU family, picked by the resource that family is short of: + // 6502 by size (11; on plain 6502 the table is not even faster below ~15, since its + // dispatch lacks jmp (abs,x)), m68k/virtual by speed (6/4, well under the m68k size + // break-even), 65C02 keeps 4 (cheap jmp (abs,x); short hot dispatches like gfx_hires). + val (sizeLimit, spanFactor) = when { + compilerOptions.compTarget.cpu == CpuType.CPU65C02 -> 4 to 2 + compilerOptions.compTarget.cpu == CpuType.CPU6502 -> 11 to 2 + compilerOptions.compTarget.cpu.is68k -> 6 to 4 + else -> 4 to 4 } if(choices.size >= sizeLimit) { if (condition.inferType(program).isUnsignedByte) { @@ -1647,8 +1652,9 @@ class When(var condition: Expression, val span = sortedValues.last() - lowest + 1 // cap the table so that a sparse set of values can't blow the table up out of all // proportion to the number of choices; within the cap the O(1) dispatch wins even - // when the table itself is a bit bigger than the compare chain it replaces - if(span <= sortedValues.size * 2) + // when the table itself is a bit bigger than the compare chain it replaces. + // the cap is looser on m68k/virtual where a bigger table costs no scarce memory + if(span <= sortedValues.size * spanFactor) return lowest } } diff --git a/docs/source/todo.rst b/docs/source/todo.rst index 3042797ee..851df1122 100644 --- a/docs/source/todo.rst +++ b/docs/source/todo.rst @@ -64,8 +64,6 @@ Optimizations - various optimizers skip stuff if compTarget.name==VMTarget.NAME. Once new 6502 codegen is done from IR code, those 6502 only optimizations should probably be removed - M68K: replace prog8 routines in math library by optimized 68000 assembly versions. - M68K stack/frame follow-ups: the stack-based memory model for locals and parameters is implemented, including frame slots for locals, parameters in the caller-pushed incoming area (word-packed argument slots are now the default convention), per-activation virtual registers for re-entrant subroutines, prologue zero-store coalescing, and diagnostics/polish items. Remaining low-pri optimization work: reserved outgoing-argument area (allocate the max call-site arg size once in the caller's ``link`` frame; benchmark-driven, likely marginal) and loop-aware liveness for frame slot reuse (needs real backward live-in/live-out dataflow over the CFG; frame-size-driven). Register argument passing lives in ``ideas/m68k-register-allocation.md`` §9; recursion-sound defers is an IR/VM item above. See the user-facing description in ``technical.rst``. -- ``on..goto``: the index is loaded twice per dispatch. The index is read once for the ``index < numlabels`` bounds check and again for the table lookup, on every backend (cx16 emits ``lda sel`` twice with ``cmp #6``/``bcs`` in between; m68k emits ``move.b sel,p8_regfile+0`` then ``move.b sel,p8_regfile+2``). Making ``CodeDesugarer.after(OnGoto)`` always use a temp var is *not* the fix: measured on cx16 that grows a 6-way dispatch from 13 to 16 bytes, since it replaces the first load with ``lda``/``sta`` and still reloads the temp. The value is genuinely needed twice at the AST level, so this needs register reuse in ``IRPeepholeOptimizer``: ``removeDoubleLoadsAndStores`` only works within one chunk, whereas these two loads sit either side of a ``bge``. Note the file's own comment there already lists "detect multiple loads to the same target registers, only keep first (if source is not I/O memory)" as a known un-implemented optimization. A cross-chunk version must first prove the fall-through is the only way into the second chunk, since another branch or jump there would leave the register undefined; there is no liveness data in the IR to lean on (``RegisterPool`` just hands out the next free register). -- Calibrate the ``when`` → ``on..goto`` ``sizeLimit`` per target instead of the current empirical 4 (65C02) / 5 (m68k) / 6 (other), and pick each target's limit by what that target is actually short of. The 6502 targets are severely memory-constrained, so their limit is a **size** decision: on cx16 (6502, 16-bit pointers) the table already wins from 4 choices up (348 vs 353 bytes at 4 choices, 362 vs 375 at 6), growing ~7 bytes per choice against ~14 for the compare chain, so the cx16 limit of 4 is about right. The m68k targets have plenty of RAM (an Amiga is not short of the C64/X16's few kilobytes), so their limit is a **speed** decision, not a size one: the table is ~30% fewer VM instructions for the dispatch at every size measured, and on qemu68k the 32-bit table slots make it larger until ~20 choices (+103 bytes at 6, +63 at 8, break-even at ~12), but that larger table is still the faster dispatch. So the m68k limit should stay low (use the table eagerly) for the speed win, *not* be raised to the ~12-20 size break-even. The "only use the table when it is smaller" rule applies to the 6502 targets only. The sparse-value span cap (``span <= values.size * 2``) is the same kind of size heuristic and should be relaxed or skipped on m68k for the same reason. Dead Code Elimination bug in 64tass, for nested subroutines