when->on..goto: calibrate the jump-table sizeLimit per target

This commit is contained in:
Irmen de Jong committed 2026-10-15 22:10:52 +02:00
1 parent 3a5f2c33ab
commit 06d2f9ebbe
3 files changed
+12 -11

No files matched your search

@@ -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)
}
@@ -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
}
}
-2
View File
@@ -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