Optimization levels¶
mforth compile exposes four optimization levels. The mforth optimizer
follows one priority: fast > small — inlining and speed win by default;
shrinking the program is a fallback for when it does not fit a processor.
The CLI default is -Ofast. (The library compile entry point defaults to
-O0 — see Why the library defaults to O0.)
| Flag | Tier | Passes |
|---|---|---|
-O0 |
— | none |
-O1 |
A | constant folding + dead-code elimination (AST); dead-copy elimination + peephole collapse (post-emit) |
-Ofast |
B | -O1 + common-subexpression elimination + loop-invariant code motion (AST) — default |
-Osize |
C | -Ofast + @counter subroutine emission (size fallback) |
The four flags are mutually exclusive. AST-stage passes run in the order fold → dce → cse → licm (fold first so DCE sees the literal flags it prunes on), after which the program is re-stack-checked — a transformed AST must stay stack-valid before it reaches the slot allocator. Post-emit passes (dead-copy elimination, peephole) then run over the emitted instruction stream. All passes are equivalence-preserving on the observable sink-event channel.
What each level is for¶
-O0— no transformation. The compiled output is byte/event-identical to what the host REPL does, instruction for instruction. This is the level the REPL ↔ mlog teaching-equivalence harness compiles at.-O1— the cheap, always-safe wins: collapse compile-time-constant arithmetic, drop dead branches and uncalled words, eliminate dead staging copies and reuse slots, and foldset/use round-trips.-Ofast(default) — adds the two speed-focused AST passes. CSE removes redundant recomputation; LICM hoists invariant work out of loops.-Osize— the only level that trades speed for footprint. When the fully-inlined program would exceed the per-processor instruction budget, eligible multi-call words are emitted once as@countersubroutines and call sites jump to them. For programs that already fit (the common case), nothing is promoted and-Osizeis-Ofastplus a trailingend.
Benchmarks¶
The numbers below come from the benchmark harness in
tests/bench/test_optimization_levels.py, which compiles each fixture at
every level and measures three things: static instructions emitted,
dynamic instruction dispatches per tick (under the in-repo mlog
interpreter), and distinct slot variables used. The harness regenerates
this table, so the committed doc and the live measurement never drift.
| Fixture | Level | Static instrs | Dynamic instrs/tick | Slots |
|---|---|---|---|---|
| arith_heavy | O0 | 37 | 37 | 2 |
| arith_heavy | O1 | 4 | 4 | 0 |
| arith_heavy | Ofast | 4 | 4 | 0 |
| arith_heavy | Osize | 5 | 5 | 0 |
| loop_heavy | O0 | 22 | 10 | 2 |
| loop_heavy | O1 | 15 | 9 | 2 |
| loop_heavy | Ofast | 15 | 9 | 2 |
| loop_heavy | Osize | 19 | 11 | 2 |
| sensor_heavy | O0 | 20 | 18 | 3 |
| sensor_heavy | O1 | 16 | 14 | 2 |
| sensor_heavy | Ofast | 16 | 14 | 2 |
| sensor_heavy | Osize | 21 | 19 | 3 |
| worddef_heavy | O0 | 27 | 27 | 3 |
| worddef_heavy | O1 | 21 | 21 | 2 |
| worddef_heavy | Ofast | 21 | 21 | 2 |
| worddef_heavy | Osize | 28 | 28 | 3 |
Reading the table¶
arith_heavyis the headline case for constant folding: a long chain of compile-time-constant arithmetic collapses from 37 dispatches/tick at-O0to 4 at-Ofast— an ~89% reduction, well past the project's ≥40% speed bar for arithmetic-heavy code.loop_heavyandsensor_heavyshow the more typical wins on controller-shaped code: the optimizer removes per-iteration staging copies and dead stores, trimming both footprint and per-tick work, but cannot fold what depends on runtime sensor reads or loop counters.worddef_heavyshows inlining: every user word is inlined at-O0through-Ofast.-Osizeis larger here because the program fits the budget, so subroutine promotion correctly does nothing (inline is faster) and the size column reflects only the subroutine path's trailingend.
-Ofast is never worse than -O0 on either static or dynamic count for any
fixture, and the sink event stream is identical across all four levels —
the optimizer changes the instruction stream, never the observable output.
Why the library defaults to O0¶
The headline mforth property is REPL ↔ mlog event-stream equivalence: the
same .fs source must produce the same observable events whether run through
the host REPL or compiled and executed through the in-repo mlog interpreter.
The interpreter emits VariableReadEvent / VariableWriteEvent for every
source VARIABLE access so compiled output matches the REPL's instrumentation
event-for-event.
CSE and LICM legitimately elide redundant @ fetches, so an optimized
program emits fewer instrumentation events than the REPL — a deliberate,
correct divergence on the instrumentation channel (project decision
mforth-ump, Option A). To keep the teaching guarantee exact, the strict
equivalence harness compiles at -O0, and so every library compile
entry point (mforth.optimize.compile_text) defaults to -O0. Only the
CLI opts into -Ofast.
Optimized levels are validated differently: the behavior-equivalence test
(tests/integration/test_optimized_equivalence.py) compiles representative
programs at -O0 and at each optimized level, runs both through the
interpreter, and asserts the sink event stream is identical while
allowing the VariableRead / VariableWrite counts to differ.