Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ jobs:
- name: "🔨 Build & test"
run: >-
nix develop --accept-flake-config --allow-import-from-derivation --command cabal test all --test-show-details=direct
# Deterministic LuaJIT counters over freshly linked bench artifacts
# (FNEW census, trace abort/blacklist state). Wall-clock benchmarks
# stay local — shared runners are too noisy; see bench/README.md.
- name: "📊 Bench counters"
run: >-
nix develop --accept-flake-config --allow-import-from-derivation --command ./bench/ci
format:
runs-on: ubuntu-latest
steps:
Expand Down
1 change: 1 addition & 0 deletions bench/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/_build/
78 changes: 78 additions & 0 deletions bench/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Benchmark harness

Measures the performance of generated Lua and meters how LuaJIT's tracing
JIT treats it. Two kinds of output with two different purposes:

- **Wall-clock timings** (`./bench/run`) — local numbers for defending an
optimization win. Too noisy for CI.
- **Deterministic LuaJIT counters** (`./bench/ci`) — byte-stable reports
that CI diffs against the committed oracles in `goldens/`, so a codegen
change that adds closure allocations or blacklists a loop shows up as a
reviewable diff.

## Layout

- `micro/` — hand-written Lua pairs: the shape the backend currently
generates for a pattern (`current`) next to the idiomatic Lua it stands
for (`ideal`). These are fixed reference points; they do not change when
codegen changes.
- `macro/` — specs driving real linked modules. Each spec names a `Bench.*`
PureScript module (sources in `test/ps/src/Bench/`, linked by
`./bench/link` into `_build/`), how to drive its export hot, and an
`ideal` hand-written equivalent.
- `tools/` — the runners and meters. `fnew_census.lua` and
`trace_report.lua` require LuaJIT (`jit.util`, `jit.attach`); the timing
runners work under both PUC Lua and LuaJIT.
- `goldens/` — committed counter reports; the CI oracle.

## Usage

```bash
./bench/run # all wall-clock benchmarks, all runtimes
lua bench/tools/run_micro.lua bench/micro/curried_apply.lua # one bench
luajit bench/tools/run_macro.lua bench/macro/array_foldl.lua 5e6 # custom n
./bench/ci # regenerate counters, verify against goldens/
./bench/ci --accept # rewrite goldens/ after a deliberate change
```

Timings use `os.clock()` — CPU time, not wall time. That is deliberate:
the benchmarks are pure computation, and CPU time ignores scheduler noise.
It would under-report I/O, so do not reuse the timing helpers for anything
that waits. For quieter numbers, pin the process to a core (`taskset -c N
./bench/run`) and use the `performance` CPU governor; leave ASLR alone —
disabling it trades a real security property for nearly nothing.

## What the counters mean

`fnew_census` statically counts `FNEW` bytecodes (closure creation) in a
linked artifact without running it, split by where the instruction lives:
the **main chunk** runs once at load time, so its FNEWs are init cost; a
**function body** runs per call, so its FNEWs are steady-state allocation —
and each one aborts LuaJIT trace recording (`NYI: bytecode FNEW`), which is
what keeps curried hot code interpreted. The census is a pure function of
the artifact, hence byte-stable.

`trace_report` runs a macro spec hot and reports (a) the *set* of distinct
trace-abort sites with reasons and (b) the end state of loop and
function-entry bytecodes: LuaJIT rewrites an opcode to its `J*` form when
it installs a trace there and to its `I*` form when it blacklists the spot.
Raw abort *counts* are not reported: the retry-penalty step that leads to a
blacklist draws on an entropy-seeded PRNG, so counts jitter across runs
while the final opcode state and the abort-site set do not. Blacklisting is
never logged by `-jv`/`-jdump`; the post-hoc opcode read is the only stable
way to observe it.

Both reports record the LuaJIT version (`runtime:` header line): the abort
reasons, the NYI set, and the opcode families are properties of a specific
LuaJIT snapshot, so a toolchain bump that moves the counters shows up in
the golden diff as an attributable header change, not a mystery regression.

Two caveats about golden stability. The trace reports pin source *lines* of
both the linked artifact and the macro spec file itself, so any edit to
`bench/macro/*.lua` — comments included — legitimately moves the goldens;
rerun `./bench/ci --accept` and review the diff. And one residual
nondeterminism channel exists: LuaJIT's hot-counters live in a small hashed
table keyed by bytecode address, so a rare cross-process aliasing change
can alter trace formation order. `./bench/ci` generates every report twice
and compares, precisely so that this manifests as a distinct "reports
differ between runs" failure rather than a confusing golden mismatch.
44 changes: 44 additions & 0 deletions bench/ci
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#!/usr/bin/env bash
# Deterministic LuaJIT counters: a static FNEW census per linked artifact
# and a trace abort/blacklist report per macro spec. Every report is
# generated twice and compared, proving byte-stability, then diffed against
# the committed oracles in bench/goldens/. Pass --accept after a deliberate
# codegen change to rewrite the oracles from the current output.
set -euo pipefail
cd "$(dirname "$0")/.."

accept=0
if [ "${1:-}" = "--accept" ]; then
accept=1
fi

./bench/link

out=bench/_build/counters
rm -rf "$out"
mkdir -p "$out"

for artifact in bench/_build/Bench.*.lua; do
name=$(basename "$artifact" .lua)
luajit bench/tools/fnew_census.lua "$artifact" >"$out/fnew_$name.txt"
luajit bench/tools/fnew_census.lua "$artifact" >"$out/second-run.txt"
cmp "$out/fnew_$name.txt" "$out/second-run.txt"
done

for spec in bench/macro/*.lua; do
name=$(basename "$spec" .lua)
luajit bench/tools/trace_report.lua "$spec" >"$out/trace_$name.txt"
luajit bench/tools/trace_report.lua "$spec" >"$out/second-run.txt"
cmp "$out/trace_$name.txt" "$out/second-run.txt"
done
rm "$out/second-run.txt"

if [ "$accept" = 1 ]; then
mkdir -p bench/goldens
rm -f bench/goldens/*.txt
cp "$out"/*.txt bench/goldens/
echo "accepted $(find "$out" -name '*.txt' | wc -l) counter files into bench/goldens/"
else
diff -ru bench/goldens "$out"
echo "bench counters match goldens"
fi
19 changes: 19 additions & 0 deletions bench/goldens/fnew_Bench.ArrayFoldl.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
chunk: Bench.ArrayFoldl.lua
runtime: LuaJIT 2.1.1741730670
main-chunk FNEW: 7
function-body FNEW: 12
total FNEW: 19
prototypes: 20
function-body FNEW sites:
Bench.ArrayFoldl.lua:3
Bench.ArrayFoldl.lua:14
Bench.ArrayFoldl.lua:13
Bench.ArrayFoldl.lua:24
Bench.ArrayFoldl.lua:23
Bench.ArrayFoldl.lua:28
Bench.ArrayFoldl.lua:28
Bench.ArrayFoldl.lua:54
Bench.ArrayFoldl.lua:53
Bench.ArrayFoldl.lua:52
Bench.ArrayFoldl.lua:63
Bench.ArrayFoldl.lua:62
11 changes: 11 additions & 0 deletions bench/goldens/fnew_Bench.BindChain.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
chunk: Bench.BindChain.lua
runtime: LuaJIT 2.1.1741730670
main-chunk FNEW: 4
function-body FNEW: 4
total FNEW: 8
prototypes: 9
function-body FNEW sites:
Bench.BindChain.lua:3
Bench.BindChain.lua:19
Bench.BindChain.lua:27
Bench.BindChain.lua:26
9 changes: 9 additions & 0 deletions bench/goldens/fnew_Bench.Fib.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
chunk: Bench.Fib.lua
runtime: LuaJIT 2.1.1741730670
main-chunk FNEW: 3
function-body FNEW: 2
total FNEW: 5
prototypes: 6
function-body FNEW sites:
Bench.Fib.lua:3
Bench.Fib.lua:6
18 changes: 18 additions & 0 deletions bench/goldens/trace_array_foldl.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
spec: array_foldl
runtime: LuaJIT 2.1.1741730670
workload: n=5000000 reps=2 result=12500002500000
aborts (distinct site -- reason):
Bench.ArrayFoldl.lua:3 -- NYI: bytecode FNEW
Bench.ArrayFoldl.lua:62 -- NYI: bytecode FNEW
bytecode end state (J*=compiled, I*=blacklisted):
Bench.ArrayFoldl.lua:21 IFORL
Bench.ArrayFoldl.lua:3 IFUNCF
Bench.ArrayFoldl.lua:3 JFUNCF
Bench.ArrayFoldl.lua:35 JLOOP
Bench.ArrayFoldl.lua:59 IFUNCF
Bench.ArrayFoldl.lua:60 IFUNCF
array_foldl.lua:13 JFORI
array_foldl.lua:13 JFORL
array_foldl.lua:17 JFORI
array_foldl.lua:17 JFORL
counts: aborts=2 compiled=6 blacklisted=4
19 changes: 19 additions & 0 deletions bench/goldens/trace_bind_chain.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
spec: bind_chain
runtime: LuaJIT 2.1.1741730670
workload: n=1000000 reps=2 result=3000000
aborts (distinct site -- reason):
Bench.BindChain.lua:19 -- NYI: bytecode FNEW
Bench.BindChain.lua:3 -- NYI: bytecode FNEW
bytecode end state (J*=compiled, I*=blacklisted):
Bench.BindChain.lua:22 IFUNCF
Bench.BindChain.lua:23 IFUNCF
Bench.BindChain.lua:24 IFUNCF
Bench.BindChain.lua:3 IFUNCF
Bench.BindChain.lua:3 JFUNCF
Bench.BindChain.lua:5 JFUNCF
Bench.BindChain.lua:8 IFUNCF
Bench.BindChain.lua:9 IFUNCF
bind_chain.lua:10 IFORL
bind_chain.lua:17 JFORI
bind_chain.lua:17 JFORL
counts: aborts=2 compiled=4 blacklisted=7
15 changes: 15 additions & 0 deletions bench/goldens/trace_fibonacci.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
spec: fibonacci
runtime: LuaJIT 2.1.1741730670
workload: n=30 reps=2 result=832040
aborts (distinct site -- reason):
Bench.Fib.lua:3 -- NYI: bytecode FNEW
Bench.Fib.lua:6 -- NYI: bytecode FNEW
fibonacci.lua:11 -- call unroll limit reached
bytecode end state (J*=compiled, I*=blacklisted):
Bench.Fib.lua:3 IFUNCF
Bench.Fib.lua:3 JFUNCF
Bench.Fib.lua:6 IFUNCF
Bench.Fib.lua:6 JFUNCF
Bench.Fib.lua:8 JFUNCF
fibonacci.lua:11 JFUNCF
counts: aborts=3 compiled=4 blacklisted=2
29 changes: 29 additions & 0 deletions bench/link
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#!/usr/bin/env bash
# Links the Bench.* PureScript modules into standalone Lua artifacts under
# bench/_build/, building pslua and the CoreFn output first. The module list
# is derived from test/ps/src/Bench/*.purs, and stale artifacts are removed,
# so bench/_build/Bench.*.lua always mirrors the current sources.
set -euo pipefail
cd "$(dirname "$0")/.."
root=$PWD

(cd test/ps && spago build)

cabal build -v0 exe:pslua
pslua=$(cabal list-bin pslua)

mkdir -p bench/_build
rm -f bench/_build/Bench.*.lua
# CoreFn module paths are relative to test/ps (where spago runs), and pslua
# resolves foreign files against them, so it has to run from there too.
for src in test/ps/src/Bench/*.purs; do
module="Bench.$(basename "$src" .purs)"
(
cd test/ps
"$pslua" \
--ps-output output \
--foreign-path foreign \
--entry "$module" \
--lua-output-file "$root/bench/_build/$module.lua"
)
done
22 changes: 22 additions & 0 deletions bench/macro/array_foldl.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
-- Foldable `foldl` over an Array with a curried step: the FFI fold loop is
-- a plain Lua `for`, but compiling it means inlining the curried callee, so
-- the tracer aborts on FNEW and blacklists the loop. The ideal variant does
-- the same work — build the array, fold it — with an uncurried step.
return {
artifact = "Bench.ArrayFoldl",
n = 5e6,
drive = function(mod, n)
return mod.run(n)
end,
ideal = function(n)
local t = {}
for i = 1, n do
t[i] = i
end
local acc = 0
for i = 1, #t do
acc = acc + t[i]
end
return acc
end,
}
22 changes: 22 additions & 0 deletions bench/macro/bind_chain.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
-- A three-step Maybe bind chain per iteration: each step goes through the
-- Bind dictionary and allocates a Just constructor. The driver loop is the
-- hot loop here, so this also exercises what happens to a caller loop whose
-- callee cannot be traced.
return {
artifact = "Bench.BindChain",
n = 1e6,
drive = function(mod, n)
local acc = 0
for _ = 1, n do
acc = mod.run(acc)
end
return acc
end,
ideal = function(n)
local acc = 0
for _ = 1, n do
acc = acc + 3
end
return acc
end,
}
22 changes: 22 additions & 0 deletions bench/macro/fibonacci.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
-- Recursion with typeclass arithmetic: the same `fib` as the
-- Golden.Fibonacci.Test golden, linked as a module so the driver can call
-- it without going through stdout.
return {
artifact = "Bench.Fib",
n = 30,
drive = function(mod, n)
return mod.fib(n)
end,
ideal = function(n)
local function fib(v)
if v == 0 then
return 0
end
if v == 1 then
return 1
end
return fib(v - 1) + fib(v - 2)
end
return fib(n)
end,
}
34 changes: 34 additions & 0 deletions bench/micro/ctor_match.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
-- Constructor allocation plus tag match: the generated encoding keeps the
-- tag and the payload in the table's hash part, keyed by long strings,
-- versus an array-part encoding with a small-integer tag.
return {
n = 2e6,
variants = {
{
name = "current",
fn = function(n)
local acc = 0
for i = 1, n do
local m = { ["$ctor"] = "Data.Maybe∷Maybe.Just", value0 = i }
if "Data.Maybe∷Maybe.Just" == m["$ctor"] then
acc = acc + m.value0
end
end
return acc
end,
},
{
name = "ideal",
fn = function(n)
local acc = 0
for i = 1, n do
local m = { 1, i }
if 1 == m[1] then
acc = acc + m[2]
end
end
return acc
end,
},
},
}
Loading
Loading