The model
One call optimizes a module; everything else is for callers who want control:
optimizeruns the default pipeline on every function with a body (Tier 1).Optimizerchooses the passes and their order, the number of sweeps, the work budget, validation between passes, and compaction (Tier 2).run_passruns onePassKindon anir_lang::Builderwith aBudget, for callers with their own pipeline (Tier 3).Statsreports sizes before and after, sweeps, budget exhaustion, and per-pass counts;OptErrorsays why optimization stopped.
The passes:
| Pass | What it does |
|---|---|
SimplifyCfg |
Folds branches on constants and branches with identical edges, prunes switch cases, removes unreachable blocks, threads jumps through empty blocks, merges straight-line blocks. |
Sccp |
Sparse conditional constant propagation over block parameters and the edges that can be taken, including switch and check edges; folds a check to its normal edge, its error edge (with the OPS code), or a trap; turns a check that cannot fail into the plain operation. |
CopyProp |
Replaces a block parameter that receives the same value on every edge (loops included) and removes it. |
Gvn |
Dominator-based value numbering of pure operations, with commutative operands canonicalized and a few exact integer identities (x + 0, x * 1, not(not(x)), ...). |
Licm |
Hoists pure loop-invariant operations into the loop's preheader, creating one when needed, inner loops first. |
Dce |
Removes dead instructions and dead block parameters (dead loop variables included), keeping every side effect. |
What it guarantees, and how each guarantee is checked:
| Guarantee | How it is held |
|---|---|
Optimized code behaves exactly like the original: same return values, same trap codes, same check error codes, same exceptions, the same import calls with the same arguments in the same order, and the same final memory. |
Differential tests generate random SSA programs (loops, switch, calls, invoke and landing pads, resume, checked arithmetic under every policy, floats including NaN, stack and global memory, volatile accesses, atomics, memcpy/memset) and run them before and after optimization in the ir-lang reference interpreter: the whole pipeline, each pass alone, and random pass sequences. |
| Constant folding is exact, and never folds an operation that would raise into a value. | Every operation, type, and policy is folded and compared with the interpreter's eval_* functions over the OPS edge values and random bits; float rounding and square root are implemented in core and checked bit for bit against std. |
| The IR stays valid, including the GC safepoint rules. | The ir-lang validator runs after every pass in debug builds and in every test. Random programs that carry GC references through safepoints, branches, and loops are optimized, validated, and run. |
| Every pass is idempotent, and the pipeline reaches its fixpoint. | A second run of each pass, and of the pipeline, changes nothing, on every generated program. |
| No input makes a pass recurse or run away. | Every algorithm is iterative; 40,000-deep block chains, 10,000-deep dominator trees, 300-deep loop nests, 20,000-case switches, and 3,000-parameter blocks are optimized in the tests, and any budget (zero included) leaves valid IR with unchanged behaviour. |
| Work is linear in the function. | Each pass is linear by construction (the dominator tree is O(E log V)); the whole pipeline on a function of 100k or of 200k instructions fits in 160 budget steps per unit of size (a scale test). |
Installation
[]
= "0.2"
= "=2.0.0-alpha.1"
Without the standard library (optimization results are identical):
[]
= { = "0.2", = false }
Quick start
use ;
// fn f(x: i64) -> i64 { let a = 2 + 3; (x * a) + (x * a) }
let mut m = new;
let f = m.declare_function?;
let mut b = m.build?;
let x = b.param?;
let = ;
let a = b.add?;
let p = b.mul?;
let q = b.mul?;
let r = b.add?;
b.ret?;
let stats = optimize?;
assert_eq!;
let text = m.display_function.to_string;
assert!;
assert!;
assert!;
# Ok::
Policies decide what may be folded
A check holds an operation whose policy can raise. With constant operands it
folds to exactly what would happen at run time:
use ;
// i64::MAX + 1 with overflow=error: the error edge, with code 1 (E0001).
let mut m = new;
let f = m.declare_function?;
let mut b = m.build?;
let max = b.iconst?;
let one = b.iconst?;
let ok = b.create_block?;
let err = b.create_block?;
b.check?;
b.switch_to?;
let sum = b.block_param?;
b.ret?;
b.switch_to?;
let code = b.block_param?;
let wide = b.convert?;
b.ret?;
optimize?;
let text = m.display_function.to_string;
assert!;
assert!;
# Ok::
The same addition with overflow=trap stays an addition (it traps at run
time); with overflow=wrap it folds to i64::MIN.
Choosing the passes
use ;
use ;
let mut m = new;
let f = m.declare_function?;
let mut b = m.build?;
let x = b.param?;
let y = b.add?;
let z = b.add?;
let w = b.mul?;
b.ret?;
// Tier 2: value numbering and dead code only, at most two sweeps, a fixed
// budget, and the validator after every pass.
let stats = new
.passes
.max_iterations
.budget
.validate
.run?;
assert_eq!;
assert_eq!;
// Tier 3: one pass on a builder.
let mut budget = unlimited;
assert!;
# Ok::
Examples
Runnable programs in examples/:
| Example | What it shows |
|---|---|
optimize |
The one-call path on a function with a constant expression, a recomputed value, a constant branch, and a loop-invariant product; prints the IR before and after. cargo run --example optimize |
pipeline |
A configured Optimizer with per-pass counts, and single passes run by hand from one budget. cargo run --example pipeline |
Performance
Every pass is a constant number of linear walks over the function (plus the
O(E log V) dominator tree), so time grows linearly with size; the cost per
instruction rises once the function no longer fits in cache (about 1.6x for
the pipeline between 100k and 1M instructions). Measured with the benchmarks
in benches/ on functions of the typical shape (see below),
x86_64, Rust 1.95 stable, release profile, Windows 11, on a machine also
running other builds; each figure is one run of one pass, or the whole
pipeline to its fixpoint, on the unoptimized function. Library-only figures:
building the IR is not included.
| Benchmark | 100k instructions (76,674 live) | 1M instructions (766,674 live) |
|---|---|---|
pipeline/optimize (all passes, to the fixpoint: 3 sweeps) |
~69 ms (1.1 M inst/s) | ~1.12 s (0.69 M inst/s) |
pass/simplify-cfg |
~16 ms | ~254 ms |
pass/sccp |
~16 ms | ~215 ms |
pass/copy-prop |
~6.4 ms | ~102 ms |
pass/gvn |
~11 ms | ~169 ms |
pass/licm |
~7.7 ms | ~128 ms |
pass/dce |
~5.6 ms | ~119 ms |
Every figure includes the input validation each optimizer run performs (about 4 ms at 100k and 60 ms at 1M instructions). The pipeline compacts a function between passes once most of its arena is tombstones, which keeps later sweeps on dense tables.
The typical shape is what a front end lowers ordinary code into: arithmetic with a constant subexpression, the same value computed twice, a dead value, a diamond joined by a block parameter, a branch on a constant, and every fourth unit a counted loop with an invariant product. On it the pipeline removes 63.8% of the instructions (76,674 to 27,787 at 100k; 766,674 to 277,787 at 1M) and 63.0% of the blocks (30,004 to 11,114), in three sweeps. Those are the numbers for that shape only; real code has less redundancy.
Criterion writes per-benchmark reports to target/criterion/; the reduction
figures are printed to stderr. Numbers vary by CPU; use the trend across runs,
not a single absolute.
Design notes
- Analysis first, then rewrite. Each pass computes everything it will do
before it changes the function, and the IR is rewritten only through the
ir-lang
Builder, which type-checks every insertion. A pass that cannot be paid for from the budget changes nothing. - Bulk edits in linear time. The builder has no "move instruction" and its
remove_block_paramscans every block, so the passes move an instruction by re-inserting a copy and forwarding its results, and remove many block parameters at once by rebuilding the affected blocks and retargeting their edges in one pass. - GC references are never moved. A
ref, or a value derived from one, is never made a constant, value-numbered, or hoisted: moving it across a safepoint would break the IR's statepoint rules. Everything else is fair game. - Folding without
std.corelacksfloor,round,sqrt, and their kin on the MSRV; the folder implements them from the bit representation (rounding by clearing fraction bits, the square root by an exact integer root), sono_stdbuilds fold exactly whatstdbuilds fold. A float operation whose result is a NaN is not folded: OPS lets a machine produce any quiet NaN, and folding would fix one payload at compile time. - The pipeline is pass-lang. The sweep loop is a
pass_lang::PassManagerrun to a fixpoint. pass-lang's errors carry only a message, so the typedOptErrortravels beside it and is what callers see.
What is not done yet, plainly: no memory optimizations (loads are not
value-numbered or forwarded across stores, dead stores are not removed), no
inlining, no SROA, no loop canonicalization or strength reduction, and fma is
not constant-folded. Calls are never removed, even to functions marked
readnone (the attribute is an unchecked hint). See
dev/ROADMAP.md.
Testing
tests/differential.rs is the core of the suite:
random programs, run in the ir-lang reference interpreter before and after
optimization. tests/gc.rs does the same for programs with
GC references and safepoints, tests/passes.rs pins down
each pass on hand-written IR, tests/hostile.rs feeds
deep, long, and wide functions and tiny budgets, and
tests/scale.rs checks the figures quoted above. Every
rust example in this README and in docs/API.md is compiled
and run as a doctest.
Cross-platform support
- Linux (x86_64, aarch64)
- macOS (x86_64, Apple Silicon)
- Windows (x86_64)
The crate uses no operating-system facilities and no platform-specific code; a function optimizes to the same IR on every platform.
Contributing
See REPS.md for the engineering standards every change is held to, dev/DIRECTIVES.md for the definition of done, and dev/ROADMAP.md for what comes next. Before a PR: cargo fmt --all, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features must be clean.