Skip to content

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.

mforth compile <source.fs> -o <out.mlog> [-O0 | -O1 | -Ofast | -Osize]

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 fold set/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 @counter subroutines and call sites jump to them. For programs that already fit (the common case), nothing is promoted and -Osize is -Ofast plus a trailing end.

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_heavy is the headline case for constant folding: a long chain of compile-time-constant arithmetic collapses from 37 dispatches/tick at -O0 to 4 at -Ofast — an ~89% reduction, well past the project's ≥40% speed bar for arithmetic-heavy code.
  • loop_heavy and sensor_heavy show 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_heavy shows inlining: every user word is inlined at -O0 through -Ofast. -Osize is 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 trailing end.

-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.