From 94a0566ce38b4d8f898838436302016d4595ec2f Mon Sep 17 00:00:00 2001 From: Oleksandr Kozachuk <201152+ok2@users.noreply.github.com> Date: Tue, 11 Aug 2026 17:24:04 +0200 Subject: [PATCH] release: 0.2.9 --- CHANGELOG.md | 78 +++++++++++++++++++++++++ CLAUDE.md | 4 +- Cargo.lock | 6 +- Cargo.toml | 2 +- README.md | 129 +++++++++++++++++++++++++----------------- crates/cli/Cargo.toml | 2 +- crates/web/Cargo.toml | 2 +- docs/OPTIMIZATIONS.md | 84 ++++++++++++++++----------- 8 files changed, 215 insertions(+), 92 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 869b6d9..2ae2fe1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,83 @@ All notable changes to WAFER are documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.2.9] - 2026-08-10 + +### Fixed + +- **A word that never recurses no longer gets a typed entry it cannot use.** + Every word with a statically known stack effect was given the typed + wrapper + fast-entry pair. In the JIT path the function-table slot holds + the wrapper and the only caller that can reach the fast entry is + `RECURSE`, so for any other word a cross-word call went + `call_indirect` -> wrapper -> fast entry: one hop more for exactly the + same memory traffic. On a 300k-iteration loop over a callee too big to + inline that cost **1569 µs against 1067 with the convention off** -- an + optimisation making things worse. It is now emitted only when the body + calls itself, which is where it is worth 4x (Fibonacci 242 µs typed + against 636 untyped). `CONSOLIDATE` and the AOT export are unaffected; + they solve their effects separately. Present in 0.2.7 and 0.2.8. + +- **The inliner's loop guard has never actually fired.** 0.2.7 added a rule + that a loop-bearing callee must not be inlined into a caller that can + never be promoted, since the loop then loses its registers -- a 7x + pessimisation applied by an optimisation pass. The check ran _before_ + inlining, where the caller is nothing but calls: `DROP` is + `Call(WordId(2))`, `CR` is `Call(WordId(38))`. Since the check looks + through calls by design, it called nearly every caller promotable and + the guard did nothing. Inlining now happens in two passes -- loop-free + callees first, then the question, then the rest. + +### Added + +- **A sixth benchmark, `CrossCalls(300K)`, that measures what `CONSOLIDATE` + does.** The other five have no cross-word call left in their hot loop: + four have their callee inlined away and Fibonacci is self-recursive. So + the `CONSOL` column measured nothing, which is how both bugs above stayed + hidden. With a real call in the loop, consolidation is worth 2.8-4x. + +### Changed + +- **The benchmark harness stops reporting noise.** It took the median of three + timed repetitions inside one process, and a `samples` field that was never + read. Each measurement is now the mean of the three fastest of seven + repetitions, and that whole process runs three times with the fastest kept. + Benchmark noise is one-sided -- a scheduling hiccup or a busy SMT sibling can + only make a run slower -- so the fastest runs are the honest ones, and only a + fresh process resamples core placement and code layout. On a shared 16-vCPU + box the run-to-run spread went from 20-79% to 1-6%, and Fibonacci after + `CONSOLIDATE` stopped being bimodal (413-419 µs on three reports and 712-770 + on two, with nothing in between; now 412-426 across four). + +- **Every benchmark is now sized to run about 10 ms**, from the 0.2-2 ms most + of them took. Not for the usual reason -- the timing wrapper already excludes + start-up and compilation, and in the measurements shorter benchmarks were if + anything the _steadier_ ones -- but it buys a comfortable margin over timer + resolution and first-iteration effects for nothing: the report still finishes + in under a minute, and gforth, 3-20x slower than WAFER, is what sets that + clock. Fibonacci went from 25 to 33 rather than into a loop, so it stays pure + recursion; Collatz repeats its 2000-value round 50 times instead of counting + higher, because past ~100000 the sequence peaks near 1.5 billion and `3 * 1+` + overflows WAFER's 32-bit cells while sf64's 64-bit cells carry on -- the two + engines would stop doing the same work. All three engines agree on the results + at the new sizes. + +### Explained + +- **Why `CONSOLIDATE` makes some promoted loops slower** (NestedLoops + 1.7x on x86-64, 1.1x on arm64): not worse code -- the WASM is + byte-identical and the machine code instruction-identical modulo + registers -- but worse placement. A tight loop pays for straddling an + instruction-fetch window (16 bytes on the M1 at ~9%; 32 bytes on + Skylake at up to ~65%, where a fused `cmp+jcc` crossing the boundary + drops the loop out of the uop cache every iteration -- the JCC + erratum). Cranelift never aligns loop headers, and the per-word JIT + module's dead dsp-prologue bytes happen to shift its loops onto + luckier offsets. Verified by a padding sweep that reproduces the full + penalty range on both hosts, including placements where consolidated + code beats the JIT. Details in docs/OPTIMIZATIONS.md; native x86-64 + reference numbers in the README re-taken at the new workload sizes. + ## [0.2.8] - 2026-08-10 ### Added @@ -363,6 +440,7 @@ compliance suite, `CONSOLIDATE` whole-program recompilation, `wafer build` AOT export (WASM / native / JS loader), browser REPL, SHA-1/256/512 words, and cross-engine benchmark lanes against gforth and SwiftForth. +[0.2.9]: https://github.com/ok2/wafer/compare/v0.2.8...v0.2.9 [0.2.8]: https://github.com/ok2/wafer/compare/v0.2.7...v0.2.8 [0.2.7]: https://github.com/ok2/wafer/compare/v0.2.6...v0.2.7 [0.2.1]: https://github.com/ok2/wafer/compare/v0.2.0...v0.2.1 diff --git a/CLAUDE.md b/CLAUDE.md index e4e3ee1..b99a1a3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,7 +2,7 @@ ## What is WAFER? -WAFER (WebAssembly Forth Engine in Rust) is an optimizing Forth 2012 compiler targeting WebAssembly. Currently a working Forth system with 200+ words, JIT compilation, 12 word sets at 100% compliance, and a full optimization pipeline (peephole, constant folding, inlining, strength reduction, DCE, tail calls, per-region stack-to-local promotion with DO/BEGIN loop and IF support, self-recursive direct calls, a typed calling convention for words with a known stack effect, self-guard expansion for recursive words, consolidation). Beats gforth on every benchmark, and SwiftForth `sf64` on four of five (measured native-vs-native on x86-64; the macOS sf64 build is x86-64 under Rosetta and flatters WAFER). Includes a browser-based REPL via wasm-pack. +WAFER (WebAssembly Forth Engine in Rust) is an optimizing Forth 2012 compiler targeting WebAssembly. Currently a working Forth system with 200+ words, JIT compilation, 12 word sets at 100% compliance, and a full optimization pipeline (peephole, constant folding, inlining, strength reduction, DCE, tail calls, per-region stack-to-local promotion with DO/BEGIN loop and IF support, self-recursive direct calls, a typed calling convention for words with a known stack effect, self-guard expansion for recursive words, consolidation). Beats gforth on every benchmark, and SwiftForth `sf64` on five of six (measured native-vs-native on x86-64; the macOS sf64 build is x86-64 under Rosetta and flatters WAFER). Includes a browser-based REPL via wasm-pack. ## Architecture @@ -79,7 +79,7 @@ Handle in `interpret_token_immediate()` or `compile_token()` as a special case. ## Testing -- Run `cargo test --workspace` before committing (currently 608 unit + 1 benchmark + 12 compliance + 9 comparison + 5 crypto) +- Run `cargo test --workspace` before committing (currently 611 unit + 1 benchmark + 12 compliance + 9 comparison + 5 crypto) - Forth 2012 compliance: `cargo test -p wafer-core --test compliance` - Cross-engine comparison (vs gforth): `cargo test -p wafer-core --test comparison` - Performance benchmarks (release mode): `cargo test -p wafer-core --test comparison -- --nocapture --ignored` diff --git a/Cargo.lock b/Cargo.lock index 93a1776..38a1269 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1589,7 +1589,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "wafer" -version = "0.2.8" +version = "0.2.9" dependencies = [ "anyhow", "clap", @@ -1600,7 +1600,7 @@ dependencies = [ [[package]] name = "wafer-core" -version = "0.2.8" +version = "0.2.9" dependencies = [ "anyhow", "insta", @@ -1615,7 +1615,7 @@ dependencies = [ [[package]] name = "wafer-web" -version = "0.2.8" +version = "0.2.9" dependencies = [ "anyhow", "js-sys", diff --git a/Cargo.toml b/Cargo.toml index 7fa95a2..4d9ce48 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ members = ["crates/*"] resolver = "2" [workspace.package] -version = "0.2.8" +version = "0.2.9" edition = "2024" license = "MIT OR Apache-2.0" repository = "https://github.com/ok2/wafer" diff --git a/README.md b/README.md index 8a5510e..abcc643 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ An optimizing Forth 2012 compiler targeting WebAssembly. WAFER JIT-compiles each - **200+ words** across 12 Forth 2012 word sets, all at **100% compliance** - **Optimizing compiler** with 6 IR passes + stack-to-local promotion (per region, so a hot loop keeps its registers even inside a word that does I/O; `DO` and `BEGIN` loops alike) + consolidation -- **Faster than gforth** on every benchmark, and past SwiftForth `sf64` -- a native-code compiler -- on four of five +- **Faster than gforth** on every benchmark, and past SwiftForth `sf64` -- a native-code compiler -- on five of six - **JIT compilation** — each `:` definition compiles to its own WASM module - **Self-recursive direct calls** — RECURSE compiles to native `call` instead of `call_indirect` - **Typed calling convention** — a word with a statically known stack effect passes its stack items as WASM values, so a call keeps them in registers instead of round-tripping through memory @@ -80,54 +80,69 @@ git submodule update --init ## Performance -WAFER beats gforth (the GNU Forth reference implementation) on every benchmark, and SwiftForth -`sf64` -- which compiles to native code -- on four of the five. +WAFER beats gforth (the GNU Forth reference implementation) on every benchmark by 3-20x, and +SwiftForth `sf64` -- which compiles to native code -- on five of the six. Fibonacci is the one it +loses: one call per node, no loop to promote, and `sf64` keeps its stack in registers across a call +the way only a native code generator can. -Measured with all three engines running **native x86-64**, on an idle 16-vCPU Xeon Platinum 8124M -@ 3.0 GHz (median of three runs): - -``` -Benchmark WAFER gforth sf64 WAFER/gf WAFER/sf -Fibonacci(25) 411 3221 355 0.13x 1.16x -Factorial(12)x100K 994 7141 3058 0.14x 0.33x -GCD-bench(20K) 1591 3211 2423 0.50x 0.66x -NestedLoops(50)x1K 889 6824 2342 0.13x 0.38x -Collatz(2K) 391 3981 1659 0.10x 0.24x -``` - -Times in microseconds; WAFER is the better of the JIT and `CONSOLIDATE` runs. Below 1.0 means WAFER -is faster. Fibonacci is the one WAFER loses: it is one call per node with no loop to promote, and -`sf64` keeps its stack in registers across a call the way only a native code generator can. -Fibonacci, GCD and Collatz held to within 2% across the three runs; Factorial and NestedLoops are -softer, since `sf64` varied by half there, but they are wide wins either way. - -`just bench-compare` on the development machine (M1 Ultra, arm64) reports different numbers, and -they flatter WAFER: +Measured on the development machine (M1 Ultra, arm64), median of three reports: ``` Benchmark WAFER CONSOL gforth sf64 WAFER/gf WAFER/sf -Fibonacci(25) 237 242 3340 287 0.07x 0.83x -Factorial(12)x100K 480 479 6109 1594 0.08x 0.30x -GCD-bench(20K) 549 541 1830 797 0.30x 0.68x -NestedLoops(50)x1K 501 509 7092 1898 0.07x 0.26x -Collatz(2K) 196 190 3955 633 0.05x 0.30x +Fibonacci(33) 11307 11407 157001 13053 0.07x 0.87x +Factorial(12)x2M 9639 9599 123950 32091 0.08x 0.30x +GCD-bench(400K) 11662 11580 38580 17001 0.30x 0.68x +NestedLoops(50)x20K 8920 9852 140518 36828 0.06x 0.24x +CrossCalls(3M) 10883 3769 87691 8240 0.04x 0.46x +Collatz(2K)x50 8838 8715 189903 28657 0.05x 0.30x ``` -The only SwiftForth build for macOS is x86-64 under Rosetta 2, while WAFER and gforth are native -arm64 -- so that `sf64` column is native against emulated. The gap is not small, and it lands -exactly where it matters: Fibonacci reads 0.83x there and 1.16x when neither engine is emulated. -Treat the arm64 table as what the regression limits in `comparison.rs` are calibrated against, and -the x86-64 table as what to believe about the engines. +Times in microseconds; the ratios use the better of `WAFER` and `CONSOL`. Below 1.0 means WAFER is +faster. -A caveat applies to both: `sf64` uses 64-bit cells to WAFER's 32-bit, so WAFER does less work per -operation. +**The `sf64` column here flatters WAFER, and by enough to change an answer.** The only SwiftForth +build for macOS is x86-64 running under Rosetta 2, while WAFER and gforth are native arm64 -- so +that column compares native code against emulated code, and the penalty falls hardest on the +call-heavy benchmark. Measured with all three engines native on x86-64 (Xeon Platinum 8124M, +Ubuntu 22.04; two reports agreed within 1%), Fibonacci reads **1.21x** where the table above says +0.87x; the other five keep their wins. That native comparison is what the "five of six" above +rests on: + +``` +Benchmark WAFER CONSOL gforth sf64 WAFER/gf WAFER/sf +Fibonacci(33) 19512 19511 129784 16076 0.15x 1.21x +Factorial(12)x2M 22532 16601 137168 57986 0.12x 0.29x +GCD-bench(400K) 34216 34089 66595 51680 0.51x 0.66x +NestedLoops(50)x20K 10729 17827 126687 40469 0.08x 0.27x +CrossCalls(3M) 20457 7412 81303 29264 0.09x 0.25x +Collatz(2K)x50 18686 17328 188592 80857 0.09x 0.21x +``` + +A second caveat holds on any host: `sf64` uses 64-bit cells to WAFER's 32-bit, so WAFER does less +work per operation. + +`CrossCalls` is the only benchmark with a cross-word call left in its hot loop -- the other five +have their callee inlined away or are self-recursive -- so it is the only one that measures what +`CONSOLIDATE` does, and there it is worth 2.9x. `NestedLoops` goes the other way: `CONSOLIDATE` +makes it 1.1x _slower_ on the M1 and 1.7x on x86-64 -- not worse code but worse luck. Both paths +emit identical WASM for the hot word; the delta is where the machine code lands. A tight loop +pays for straddling an instruction-fetch window (16 bytes on the M1, 32 on Skylake, where a fused +branch crossing the boundary drops the loop out of the uop cache -- the JCC erratum), Cranelift +does not align loop headers, and dead prologue bytes in the per-word JIT module happen to shift +its loops into luckier spots. Details in +[docs/OPTIMIZATIONS.md](docs/OPTIMIZATIONS.md#8-consolidation). + +Every benchmark is sized to run about 10 ms. Not for the usual reason -- the timing wrapper already +excludes start-up and compilation -- but to keep a comfortable margin over timer resolution and +first-iteration effects without pushing the report past a minute. gforth is 3-20x slower than +WAFER, so it sets the wall clock. A word whose stack effect is statically known gets a **typed entry point**: its stack items travel in and out as WASM values instead of through the memory data stack, so cranelift keeps them in registers across a call the way a native Forth keeps TOS in one. The word also keeps a `( -- )` wrapper, which is what the function table, `EXECUTE` and the outer interpreter reach, so nothing about the memory ABI changes from the outside. -Call-heavy code is what this pays for -- Fibonacci went from 4.3x slower than `sf64` to 1.2x. Set -`WAFER_TYPED_CALLS=0` to fall back to the memory-stack convention. +Only a caller inside the same module can use the fast entry -- `RECURSE` in the JIT path, every resolvable +call after `CONSOLIDATE` -- so that is exactly when it is emitted. Set `WAFER_TYPED_CALLS=0` to fall back. Recursive words then get one more thing: their base-case guard is tested at the **call site**, so a leaf of the recursion costs a comparison instead of a call. `: FIB DUP 2 < IF EXIT THEN ... RECURSE` @@ -136,23 +151,35 @@ on entry anyway. Half of fib's nodes are leaves, and that is worth 1.4x. ## Testing +Everything below has a `just` target; the raw command is given where it is worth +knowing what the target does. + ```bash -# All tests (~635 currently passing) -cargo test --workspace - -# Forth 2012 compliance suite -cargo test -p wafer-core --test compliance - -# Cross-engine comparison (WAFER vs gforth, requires gforth) -cargo test -p wafer-core --test comparison -- --nocapture --ignored - -# Optimization benchmark report (WAFER-internal) -cargo test -p wafer-core --test benchmark_report -- --nocapture --ignored - -# Lints -cargo clippy --workspace +just test # all tests (~638 currently passing) +just compliance # Forth 2012 compliance suite +just clippy # lints +just fmt # formatting check (Rust + Markdown) +just ci # everything CI runs ``` +Benchmarks are separate, because they are `#[ignore]`d -- they take minutes, and +a debug build would measure nothing useful: + +```bash +just bench-compare # WAFER vs gforth vs SwiftForth, the table in Performance +just bench-opts # WAFER against its own optimization settings +just bench # criterion micro-benchmarks +just compare-correctness # same three engines, compared on output instead of time +``` + +`bench-compare` needs `gforth` and `sf64` on `PATH` -- a missing engine drops its +column rather than failing. Each number in it is the best of three processes, and +each process reports the mean of its three fastest of seven timed repetitions: +benchmark noise is one-sided, so the fastest runs are the honest ones, and only a +fresh process resamples core placement and code layout. Run it on an idle +machine; a busy one produced 20-79% run-to-run spread where an idle one gives +1-6%. + ## Architecture ``` diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 11d9f1e..41c92b2 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -9,7 +9,7 @@ license.workspace = true workspace = true [dependencies] -wafer-core = { path = "../core", version = "0.2.8" } +wafer-core = { path = "../core", version = "0.2.9" } wasmtime = { workspace = true } anyhow = { workspace = true } clap = { version = "4", features = ["derive"] } diff --git a/crates/web/Cargo.toml b/crates/web/Cargo.toml index 168d4df..52a2152 100644 --- a/crates/web/Cargo.toml +++ b/crates/web/Cargo.toml @@ -12,7 +12,7 @@ workspace = true crate-type = ["cdylib", "rlib"] [dependencies] -wafer-core = { path = "../core", version = "0.2.8", default-features = false, features = ["crypto"] } +wafer-core = { path = "../core", version = "0.2.9", default-features = false, features = ["crypto"] } wasm-bindgen = "0.2" js-sys = "0.3" send_wrapper = { workspace = true } diff --git a/docs/OPTIMIZATIONS.md b/docs/OPTIMIZATIONS.md index e600e4b..dae7bbd 100644 --- a/docs/OPTIMIZATIONS.md +++ b/docs/OPTIMIZATIONS.md @@ -308,6 +308,28 @@ After interactive development, `CONSOLIDATE` recompiles all defined words into a | JIT (current) | Interactive development | Per-word modules, `call_indirect`, fast redefine | | Consolidated | After `CONSOLIDATE` | Single module, direct `call`, no redefine | +### Why the CONSOL column can lose to the JIT column + +`NestedLoops` runs 1.1x slower after `CONSOLIDATE` on the M1 and 1.7x slower on a Skylake Xeon, +with **byte-identical WASM** for the hot word in both modes (verified via `WAFER_DUMP_WASM` + +`wasm-tools print`) and instruction-identical machine code modulo register names (verified via +`Engine::precompile_module` + objdump). The whole delta is code placement: + +- A tight loop pays for straddling an instruction-fetch window: ~9% for a 16-byte window on the + M1, up to ~65% on Skylake when the fused `cmp+jcc` crosses a 32-byte boundary and the loop + falls out of the uop cache every iteration (the JCC erratum, post-microcode). +- Cranelift never aligns loop headers (`align_basic_block` is an identity default, no ISA + overrides it), so where a loop lands is whatever the code before it leaves behind. +- The per-word JIT module keeps a dead dsp load in its prologue (the store-back is DCE'd, the + load survives), which happens to shift its loops onto luckier offsets than the consolidated + module's cleaner function bodies. A padding experiment that moves the same loop across offsets + reproduces the full penalty range on both hosts, including placements where the consolidated + code **beats** the JIT code. + +So the column difference on loop-only benchmarks is an alignment lottery, not an emitter defect; +divider-bound benchmarks (`GCD`) mask it entirely. Fixing it for real means loop-header alignment +upstream in Cranelift. + ## 9. Compound IR Operations **Status: Done.** `TwoDup` and `TwoDrop` IrOp variants with optimized codegen. Peephole converts `Over, Over -> TwoDup` and `Drop, Drop -> TwoDrop`. @@ -469,7 +491,7 @@ The float stack lives in its own memory region (0x2540--0x2D40). Float operation ## 16. Typed Calling Convention -**Status: Done.** A word whose stack effect is statically known compiles to two entry points: a fast one with signature `(i32 x p) -> (i32 x q)`, carrying its stack items as WASM values, and the usual `( -- )` wrapper that moves those items on and off the memory data stack. The wrapper keeps the function-table slot, so `EXECUTE`, the outer interpreter, host words and `CATCH` see exactly the ABI they saw before; only direct calls inside a module take the fast entry. `WAFER_TYPED_CALLS=0` falls back. +**Status: Done.** A word that calls itself and whose stack effect is statically known compiles to two entry points: a fast one with signature `(i32 x p) -> (i32 x q)`, carrying its stack items as WASM values, and the usual `( -- )` wrapper that moves those items on and off the memory data stack. The wrapper keeps the function-table slot, so `EXECUTE`, the outer interpreter, host words and `CATCH` see exactly the ABI they saw before; only direct calls inside a module take the fast entry. `WAFER_TYPED_CALLS=0` falls back. The self-recursion condition matters: the table slot holds the wrapper, so in the JIT path nothing but `RECURSE` can reach the fast entry, and emitting it for any other word just puts a wrapper hop in front of every call through the table -- measured at +47% before that was fixed in 0.2.9. ### The Problem @@ -517,43 +539,39 @@ Fibonacci(25): 356 to 237 microseconds on the arm64 development machine. In fib' All optimizations enabled, release mode, measured with UTIME: -All three engines **native x86-64**, idle 16-vCPU Xeon Platinum 8124M @ 3.0 GHz, -median of three runs: - -``` -Benchmark WAFER gforth sf64 WAFER/gf WAFER/sf -Fibonacci(25) 411 3221 355 0.13x 1.16x -Factorial(12)x100K 994 7141 3058 0.14x 0.33x -GCD-bench(20K) 1591 3211 2423 0.50x 0.66x -NestedLoops(50)x1K 889 6824 2342 0.13x 0.38x -Collatz(2K) 391 3981 1659 0.10x 0.24x -``` - -The same suite on the arm64 development machine (M1 Ultra), which is what the -regression limits in `comparison.rs` are calibrated against: +Development machine (M1 Ultra, arm64), median of three reports, every +benchmark sized to about 10 ms: ``` Benchmark WAFER CONSOL gforth sf64 WAFER/gf WAFER/sf -Fibonacci(25) 237 242 3340 287 0.07x 0.83x -Factorial(12)x100K 480 479 6109 1594 0.08x 0.30x -GCD-bench(20K) 549 541 1830 797 0.30x 0.68x -NestedLoops(50)x1K 501 509 7092 1898 0.07x 0.26x -Collatz(2K) 196 190 3955 633 0.05x 0.30x +Fibonacci(33) 11307 11407 157001 13053 0.07x 0.87x +Factorial(12)x2M 9639 9599 123950 32091 0.08x 0.30x +GCD-bench(400K) 11662 11580 38580 17001 0.30x 0.68x +NestedLoops(50)x20K 8920 9852 140518 36828 0.06x 0.24x +CrossCalls(3M) 10883 3769 87691 8240 0.04x 0.46x +Collatz(2K)x50 8838 8715 189903 28657 0.05x 0.30x ``` -Times in microseconds. WAFER/gf < 1.0 means WAFER is faster. The two tables -disagree because the only SwiftForth build for macOS is x86-64 under Rosetta 2 -while WAFER and gforth are native arm64 -- and the emulation penalty lands -hardest on the call-heavy benchmark, so Fibonacci reads 0.83x on arm64 and 1.16x -when neither engine is emulated. Believe the x86-64 table about the engines. One -caveat holds for both: sf64 uses 64-bit cells to WAFER's 32-bit. +Times in microseconds; ratios take the better of WAFER and CONSOL. The `sf64` +column flatters WAFER: the only SwiftForth build for macOS is x86-64 under +Rosetta 2 while WAFER and gforth are native arm64. Measured with all three +native on x86-64, Fibonacci reads 1.23x rather than 0.87x -- the emulation +penalty lands hardest on the call-heavy benchmark -- while the other five keep +their ratios. One caveat holds on both: sf64 uses 64-bit cells to WAFER's +32-bit. (The native table is being re-taken at these workload sizes.) + +`CrossCalls` is the only benchmark with a cross-word call left in its hot loop, +so it is the only one that measures section 8 at all -- the other five have +their callee inlined away or are self-recursive. Note that `CONSOLIDATE` makes +NestedLoops and Collatz _slower_; see the open item below. ## Remaining Opportunities -| Optimization | Status | Potential Impact | -| -------------------------------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Scoped exit for the inliner | Not started | The inliner still refuses any body containing an `EXIT`, because an inlined one would return from the caller. Compiling it as a branch to the end of a block would unlock inlining for every word with an early return, not just the guard shape section 17 handles | -| BeginDoubleWhileRepeat promotion | Not started | Rare pattern, low priority. Its promoted emitter exists but has no loop fixup and is unverified | -| LEAVE as IR primitive | Not started | Would enable fast-path for loops with LEAVE | -| Float stack-to-local | Not started | Eliminate float stack memory traffic | -| WASM tail calls proposal | Waiting on wasmtime | Would eliminate stack growth for tail-recursive words | +| Optimization | Status | Potential Impact | +| --------------------------------- | ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Explain CONSOLIDATE on pure loops | Open defect | Isolated on an idle box: NestedLoops 540 -> 905 us (1.68x) and Collatz 310 -> 360 (1.16x) on x86-64, against 1.07x and 1.05x for the same probes on arm64 -- so the magnitude is strongly architecture-dependent, which points at code size or branch density rather than a gross codegen error. Not the promotion logic (same code path), not inlining (no call left), not the harness (CONSOLIDATE is outside the timed window). Next step is to diff the emitted wat for NESTED-BENCH between the two paths | +| Scoped exit for the inliner | Not started | The inliner still refuses any body containing an `EXIT`, because an inlined one would return from the caller. Compiling it as a branch to the end of a block would unlock inlining for every word with an early return, not just the guard shape section 17 handles | +| BeginDoubleWhileRepeat promotion | Not started | Rare pattern, low priority. Its promoted emitter exists but has no loop fixup and is unverified | +| LEAVE as IR primitive | Not started | Would enable fast-path for loops with LEAVE | +| Float stack-to-local | Not started | Eliminate float stack memory traffic | +| WASM tail calls proposal | Waiting on wasmtime | Would eliminate stack growth for tail-recursive words |