Performance and execution targets¶
This page gives the cost of each operation and the measurements behind it. The library makes no general zero-overhead claim: every performance statement on this site names the operation, the compiler, the optimization level and how it was measured. The numbers here were taken on Mojo 1.1.0 (8189361e), Linux x86-64, unless stated otherwise.
Cost model¶
Costs below exclude work done inside user callbacks and source iterators.
| Operation | Time and callback work | Auxiliary storage |
|---|---|---|
pipe with k stages |
One call per reached stage | Live intermediates only; no intermediate collection |
flow / compose construction |
Proportional to the moved component state | The components, stored natively; no callable box |
partial construction |
One copy of each bound argument | The bound copies and the function |
partial call |
One copy of each bound argument, then one call | The per-call copies, destroyed after the call |
| Sequential fold | Linear in consumed elements | Constant bookkeeping plus accumulator, callback and source state |
scan_left |
Linear, plus one accumulator copy per snapshot | The accumulator and the next snapshot |
map, filter, filter_map |
Linear in inspected elements | Constant bookkeeping plus source and callback state |
flatten, flat_map |
Linear in outer advances and yielded inner elements | The current inner source and outer state |
fp.match |
One dispatch and the selected clause per distinct value evaluated, plus the clauses tried before it | A 32-byte frame per pending value and one result per evaluated child on a value stack; a result per shared value when the result is Copyable |
fp.rewrite |
As fp.match |
Plus one new node for each rebuilt value |
Two caveats: a fold whose accumulator is a growing collection does not use
constant storage, and a filter over an infinite source whose predicate never
succeeds does not terminate.
Allocation policy¶
- The minimal non-capturing, trivial-payload cases of composition, folds and constructor dispatch perform no heap allocation for the abstraction itself.
- Allocation that the semantics require is allowed and documented: captured
state, bound-argument copies in
partial, snapshot copies inscan_left,collect_list,collect_results,fp.control.scanoutputs,Nodevalues (one allocation each) and the matching loop's frames and slots. - A match keeps its frames and pending results in standard
Liststorage instead of the native stack. That is the price of its depth guarantee; the matching contract gives the measured cost. - Hot paths in the algebra core avoid
Optional/Variantstorage for non-trivial types, which Mojo initializes and takes out of line, and use untaggedMaybeUninitslots instead. A checkedOptional.take()is not inlined.
Run-time evidence¶
At O3 the following compile to the same instructions as the equivalent
hand-written code, checked by comparing --emit asm output:
- a call through
as_unary; partial(scale, 3, 7)(x)and the same partial passed throughmap[IdentityFamily], againstscale(3, 7, x);- Reader, State, OptionalT, ResultT and Writer pipelines built from
map,flat_map,map2,ask,getandmodify.
The suite's runtime benchmarks
time Result mapping and list traversal against hand-written loops on every run;
they currently take about 1.4× and 1.3× the native loop.
Benchmarks compare against straightforward native code with the same order, ownership, copying and error behavior. A parallel reduction is not a fair baseline for a sequential fold, nor a borrowing binder for a copying one.
Compile-time cost¶
Compile time is a cost users pay on every cold build, so it is measured like run time, from an empty cache (measuring compile time). Small algebra and effects programs build in 3–6 seconds cold on Linux x86-64, and each additional transformer layer adds about a tenth of a second.
What makes Mojo 1.1 compile slowly is the type-level program, not code generation:
- associated types that reach through chains of other structs' aliases,
conditional types, or
TypeListtabulate/filter pipelines can take minutes to verify, especially when a precompiled package is imported, because the import verifies conformances symbolically with unknown parameters; - a warm compile cache hides this completely, so every measurement uses an empty
MODULAR_CACHE_DIR.
The library therefore keeps associated types to a struct's own parameters and
their immediate members, puts behavior in method bodies, and uses one concrete
struct per operation (see algebra).
A client using partial builds cold in about 3 seconds from source or from the
precompiled package.
SIMD¶
One algorithm serves scalar, SIMD and non-numeric operations: fold_left over
SIMD vectors with vector addition returns a vector accumulator. Lanes are never
reduced horizontally unless the callback does it explicitly, and a SIMD mask is
never collapsed into a branch decision. A combinator adds no host-only
allocation, logging, reflection, exceptions or runtime dispatch to a
specialization that passes through it.