Performance

Loft runs a program four ways: the interpreter, native (compiled to Rust via --native), WebAssembly (--native-wasm), and — for comparison — a hand-written Rust version of the same benchmark. The table below shows the time per operation of the ten engine benchmarks in bench/, loft native against its Rust twin.

loft native (rustc -C opt-level=3) Rust reference (rustc -O)
Benchmark loft native Rust native ÷ Rust
01 fibonacci (recursive fib(30) / fib(31))
8.29 ms
2.64 ms
3.14×
02 sum loop (5 M steps of a loop-carried integer mix)
5.39 ms
2.98 ms
1.81×
03 prime sieve (trial division below 300 000)
10.85 ms
10.65 ms
1.02×
04 Collatz lengths (starts below 200 000)
51.03 ms
25.69 ms
1.99×
05 Mandelbrot (200×200, 256 iter)
6.18 ms
6.13 ms
1.01×
06 Newton sqrt (100 000 calls of 50 steps)
28.71 ms
13.00 ms
2.21×
07 string build (200 000 formatted appends)
9.38 ms
4.41 ms
2.12×
08 word frequency (hash map, 300 000 operations)
18.69 ms
5.27 ms
3.55×
09 dot product (2 M floats)
2.01 ms
1.89 ms
1.06×
10 insertion sort (3 000 integers)
3.43 ms
0.90 ms
3.81×

Time per operation: the median of seven interleaved samples, pinned to the fastest core of an x86-64 Linux machine, and both lanes printed the same result hash. The source is the performance portal (make perf-portal), which also covers the standard library and the published libraries. Run bench/run_bench.sh from the project root for a quick look on your own hardware, with the Python, loft interpreter and loft wasm lanes beside these two.

Key takeaways

The four loft execution modes have very different performance profiles depending on what the program does.

loft interpreter vs Python

The interpreter runs your program without a separate compilation step. Loft is statically typed, so the interpreter never checks the type of a value at run time; its cost is dispatching each bytecode operation and reaching collection data through the store layer. It is slower than CPython on these benchmarks: close to it on an integer loop and on string building, several times slower on hash lookups and float kernels. For hot code, compile with --native.

loft native vs Rust

The native pipeline compiles loft source to Rust via --native or --native-release, then invokes rustc. For floating-point kernels the generated Rust is as fast as hand-written Rust (Mandelbrot 1.01×, dot product 1.06×), and so is trial division (sieve 1.02×). Integer loops run about 2× (sum loop 1.81×, Collatz 1.99×), because loft integer arithmetic is checked: an overflow answers null instead of wrapping. Newton sqrt (2.21×) pays the same for its float division, whose result may be null. Recursion (Fibonacci 3.14×) and collection-heavy code (word count 3.55×, insertion sort 3.81×) are the widest gaps.

loft wasm vs loft native

The wasm build compiles the same generated Rust for wasm32-wasip2, so it runs everywhere wasmtime or a browser is installed. Arithmetic runs close to native speed; text building trails native most, because every text allocation goes through the allocator inside wasm linear memory. Wasm is a good target when native compilation is unavailable.

Bottlenecks

Interpreter — bytecode dispatch The interpreter executes bytecode. The most frequent operator shapes — an integer operator over locals, a compare-and-jump, an element read of an integer vector, the step of a text walk — run as one fused instruction, and every other operation is one dispatch. There is no just-in-time compiler (JIT). Each iteration of a tight loop pays this per-instruction cost.
Native — collection access through the store layer Hash lookup (word count: 3.55×) and indexed vector reads and writes (insertion sort: 3.81×) go through the helpers in src/codegen_runtime.rs. They perform bounds checks, null-sentinel tests and store-pointer indirections that hand-written Rust avoids.
Native — function call overhead for recursive code The recursive Fibonacci benchmark runs at 3.14× hand-written Rust. The generated code passes the stores and other runtime context through every call frame, also for a function that does not touch the heap.
Native — checked integer arithmetic Integer overflow answers null rather than wrapping, so every integer operator checks its result. Counted loops run their index arithmetic unchecked behind one guard; straight-line arithmetic stays checked.
wasm — text allocation Text in the wasm build is a Rust String allocated inside wasm linear memory, which grows with memory.grow; each append that reallocates costs more than it does natively.

Open performance work