Shell workload benchmarks
Canonical Quirl project documentation synced from docs/benchmarks/shell-workloads.md.
Scoped engineering measurements of everyday shell work: loops over builtins,
arithmetic, function calls, case, and starting external programs. They
compare Quirl's native executor with dash, Bash, and Zsh on one machine. They
are not release evidence and say nothing about other hardware.
Method
scripts/bench-shell-workloads.py
runs each workload as one program, through quirl exec for Quirl and -c for
the reference shells, and reports the median wall time over fresh processes,
including startup. Interactive latency comes from quirl-bench preview, which
drives a real PTY.
cargo build --release -p quirl-cli -p quirl-bench
scripts/bench-shell-workloads.py target/release/quirl 9
target/release/quirl-bench preview --quirl target/release/quirl --pty-samples 512026-11-08: faster process spawning
Measured on an Apple M4 running macOS 27.0, with both builds made back to
back with the same settings (release, opt-level 3, no LTO, 16 codegen units).
The baseline is main at ac09361, which already runs loops, functions, and
aliases natively. The candidate is the perf/fast-process-spawn branch. The
reference shells are the system /bin/dash, /bin/bash 3.2, and /bin/zsh.
Median milliseconds over 9 runs:
| Workload | Quirl ac09361 | Quirl candidate | dash | Bash | Zsh |
|---|---|---|---|---|---|
startup (true) | 5.7 | 5.1 | 1.8 | 2.1 | 3.0 |
arithmetic while, 10k iterations | 44.1 | 33.9 | 21.4 | 38.5 | 19.1 |
for with echo, 10k iterations | 14.0 | 7.7 | 17.2 | 24.4 | 22.2 |
| function calls, 5k | 25.2 | 21.0 | 13.6 | 24.1 | 25.6 |
case, 10k iterations | 38.6 | 31.2 | 19.0 | 37.8 | 19.3 |
| 200 external commands | 1,576.6 | 475.4 | 247.7 | 251.5 | 258.8 |
Interactive latency, quirl-bench preview, 51 PTY samples:
| Measurement | ac09361 P50 / P95 | candidate P50 / P95 |
|---|---|---|
| cold start to editable prompt | 7.32 / 10.44 ms | 6.48 / 7.14 ms |
| keystroke to frame | 0.42 / 0.53 ms | 0.42 / 0.53 ms |
process startup to --version exit | 4.79 / 4.88 ms | 3.25 / 3.40 ms |
What changed:
- Pipelines start their trusted staging and anchor scripts in
/bin/dashinstead of the macOS/bin/shshim, and start the anchor in parallel with the staged leader. See the decision. - Child and staging polls back off from 25 µs instead of sleeping 1 ms.
- Reedline no longer links CoreFoundation, which cost about 0.7 ms per launch.
- Arithmetic operator lookup and pipeline expansion allocate and compare less.
Each external command still costs about 2.4 ms against 1.3 ms in the reference shells, because every pipeline starts two small trusted shells to own its process group. Removing the staging shell would need a separate decision about which process leads the group.
2026-11-09: shipped release profile against 0.5.1
The table above uses opt-level 3 without LTO. Released binaries use the
workspace release profile, opt-level z with fat LTO and one codegen unit,
which runs the native executor more slowly. This run rebuilt main at
ac09361 and the candidate at 81bff90 with that profile, and compares
them with the published v0.5.1 macOS ARM64 archive (SHA-256
c10017d5…7d85). Same Apple M4 and reference shells, with other work
running on the machine (load average about 10 on 10 cores). To spread that
load evenly, a variant of the script ran every shell once per round, in
rotating order, for 15 rounds.
Median milliseconds over 15 runs:
| Workload | v0.5.1 | ac09361 | candidate | dash | Bash |
|---|---|---|---|---|---|
startup (true) | 14.0 | 5.0 | 4.0 | 1.9 | 2.2 |
arithmetic while, 10k iterations | 67.4 | 91.3 | 71.2 | 22.3 | 39.8 |
for with echo, 10k iterations | 50.8 | 14.6 | 9.1 | 18.5 | 26.0 |
| function calls, 5k | fails | 46.2 | 38.7 | 14.4 | 24.9 |
case, 10k iterations | 61.8 | 77.6 | 62.6 | 18.8 | 38.4 |
| 200 external commands | 303.6 | 1,485.0 | 477.3 | 269.0 | 279.1 |
Single commands, median over 31 runs in the same rotation:
| Workload | v0.5.1 | ac09361 | candidate | dash | Bash |
|---|---|---|---|---|---|
/usr/bin/true | 12.5 | 12.7 | 6.5 | 2.7 | 3.1 |
/bin/echo hi | /usr/bin/tr a-z A-Z | 12.7 | 13.0 | 6.6 | 4.0 | 4.2 |
What this shows for users of the release:
- A single external command or pipeline starts about twice as fast as in 0.5.1.
- Loops over builtins run natively.
forwithechois about five times faster than 0.5.1 and faster than Bash. Arithmetic andcaseloops run about as fast as they did through/bin/shin 0.5.1, and slower than Bash with this profile. Function calls fail in 0.5.1 because functions did not persist. - 0.5.1 handed a whole loop to
/bin/sh, so a loop that starts 200 external programs took 0.30 s. Native loops start each program through Quirl's process-group protocol: about 1.5 s before this candidate, 0.48 s with it. - Process start to
--versionexit, fromquirl-bench preview, fell from a P50 of 5.36 ms to 4.03 ms. Interactive prompt percentiles were too noisy under this load to compare; the release workflow's gate measures them on idle runners.