Optimization
Optimization is opt-in: -O runs rasc’s own passes,
-O2 adds a pass through
Binaryen’s wasm-opt.
The default build stays fast and reproducible.
-O: rasc’s own passes
- Inliner — splices callee bodies at call sites for
functions of at most 32 instructions or marked
@inline; two rounds, recursion excluded. This matters more than it sounds: generic stdlib code produces deep dispatch chains that generic wasm optimizers cannot untangle from outside. - Allocation sinking — a post-inline pass that scalarizes
non-escaping
newinto locals, deleting the allocation, its header writes, and its shadow-stack spills. A sunk object never reaches any GC. Naive allocate-per-frame code measured 12.3× faster on the WAMR interpreter — the tier un-optimized user content actually runs on — and 2.8× on V8.--no-alloc-sinkturns it off for bisecting. - Bounds-check elimination — the canonical
for (let i = 0; i < arr.length; i++)read loop routes element reads through unchecked accessors. Stores keep the checked path.unchecked(expr)is honored under-O; debug builds stay fully checked.
-O2: the binaryen post-pass
-O2 is -O plus a wasm-opt round trip
(-O3 -fimfs=100 -ifwl -g), discovering the binary via
RASC_WASM_OPT or PATH. The post-pass is best-effort:
builds without binaryen keep working. It passes --enable-multivalue
only when a signature actually carries several results, so byte-identity is
preserved for modules that never use it.
Know the shape of the cost: on the Box2D port the full pipeline is 32 ms
debug, 41 ms at -O, and about 1 s at -O2 —
wasm-opt dominates. And binaryen alone recovers little from naive codegen;
rasc’s own inlining is the prerequisite that opens the module up.