tv-labs/lua · full-mode benchee · 2026-07-28
Lua on the BEAM: 0.4.0 → 1.0.0 → 1.0.2
3 releases of the lua
Elixir library, measured on the same machine in the same sitting, with Luerl run inside
every benchmark as a same-run control. 0.4.0 was
a thin wrapper over Luerl; 1.0.0 introduced the native VM; 1.0.2 is the
current release.
The headline:
1.0.2 is the first release faster than Luerl on most workloads — and
Lua.new()
is now effectively free.
mix run. ~100× credible in an
:embedded
release; ~5× when passing custom sandbox options.
Runtime vs Luerl, per workload
Each dot is a release's compiled-chunk median divided by the Luerl median from the same run — left of the parity line is faster than Luerl. Hover a dot for the underlying medians. Log scale.
Medians across releases
Compiled-chunk path (the production embedding path: compile once, run many). Ratio column is 1.0.2 ÷ same-run Luerl; green means faster than Luerl.
| Workload | 0.4.0 | 1.0.0 | 1.0.2 | 1.0.2 vs Luerl |
|---|---|---|---|---|
| Lua.new() — steady state | 21.33 μs | 36.67 μs | 0.58 μs | 0.04× |
| Lua.new() — allocation | 73.78 KB | 91.88 KB | 0.88 KB | 0.02× |
| fibonacci fib(30) | 702.84 ms | 792.42 ms | 434.12 ms | 0.59× |
| fibonacci — allocation | 2.45 GB | 2.90 GB | 1016.63 MB | 0.40× |
| string.format (literal-heavy) | 3.91 ms | 1.03 ms | 716.04 μs | 0.18× |
| pcall success path (n=500) | 290.50 μs | 282.13 μs | 222.88 μs | 0.76× |
| pairs over hash part (n=1000) | 1.32 ms | 995.70 μs | 1001.12 μs | 0.74× |
| __index 3-level chain | 216.42 μs | 237.13 μs | 190.08 μs | 0.88× |
| closures | 368.92 μs | 473.54 μs | 373.71 μs | 0.96× |
| table.sort (n=1000) | 174.29 μs | 227.63 μs | 199.34 μs | 1.11× |
| varargs + multi-return (n=500) | 2.40 ms | 4.01 ms | 3.28 ms | 1.36× |
| pcall raise + catch (n=500) | 469.29 μs | 877.33 μs | 727.13 μs | 1.55× |
† 1.0.2 builds PUC-Lua-conformant position-prefixed error messages; Luerl returns the bare error value, doing less work per raise.
Where 1.0.2 is still behind Luerl
pcall raise + catch (1.55× slower).
Partly apples-to-oranges: 1.0.2 builds position-prefixed error messages
("<eval>:1: negative", the PUC-Lua-conformant behavior) where Luerl
returns the bare value — it does strictly more work per raise. Still, it regressed at 1.0.0 (1.81×) and has only partially recovered.
varargs + multiple returns (1.36× slower, ~1.9× the allocation). A genuine gap with no conformance excuse — the clearest optimization target for 1.1.x.
Host-boundary decode of large lists. Not in the chart (its own harness): decoding a 4,096-element integer list is ~13× slower than the 0.4.0/Luerl era; long-string lists ~25×. String-keyed maps moved the other way (1.24× faster). Worth a look before 1.1.
How to read these numbers
-
0.4.0 is not an independent series.
It wraps
luerl 1.5.1; its own no-sandbox instantiation lands within ~1% of raw:luerl.init(). Its dots hugging the parity line is by construction, and it's why the Luerl control column is meaningful across all 3 runs. -
Discipline:
Apple M4 · Elixir 1.20.0 / OTP 29 ·
LUA_BENCH_MODE=full(10 s measure, 2 s warmup, 1 s memory) · onemix runat a time on a quiet machine · medians quoted, not averages. C Lua (luaport) was unavailable; Luerl is the reference. -
The suite got wider for this report.
patterns, metamethods, pcall/varargs, and
Lua.newworkloads are new — chosen to make the suite representative of real Lua (pattern engine,:method dispatch, protected calls), not just of what was optimized. - Coroutines are not benchmarked — they are an intentional 1.0 capability exclusion in the library, not an omission from the suite.
-
Sub-microsecond rows
(
Lua.new()on 1.0.2) show large Benchee deviation percentages due to batching at that timescale; medians are stable across runs.
Full report with per-workload analysis and reproduction instructions: bench_results/versions-2026-07-28.md in tv-labs/lua. Raw Benchee outputs and parsed JSON are committed alongside, one directory per released version.