Quirlv0.6.1
ResearchBenchmark archive

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 51

2026-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:

WorkloadQuirl ac09361Quirl candidatedashBashZsh
startup (true)5.75.11.82.13.0
arithmetic while, 10k iterations44.133.921.438.519.1
for with echo, 10k iterations14.07.717.224.422.2
function calls, 5k25.221.013.624.125.6
case, 10k iterations38.631.219.037.819.3
200 external commands1,576.6475.4247.7251.5258.8

Interactive latency, quirl-bench preview, 51 PTY samples:

Measurementac09361 P50 / P95candidate P50 / P95
cold start to editable prompt7.32 / 10.44 ms6.48 / 7.14 ms
keystroke to frame0.42 / 0.53 ms0.42 / 0.53 ms
process startup to --version exit4.79 / 4.88 ms3.25 / 3.40 ms

What changed:

  • Pipelines start their trusted staging and anchor scripts in /bin/dash instead of the macOS /bin/sh shim, 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:

Workloadv0.5.1ac09361candidatedashBash
startup (true)14.05.04.01.92.2
arithmetic while, 10k iterations67.491.371.222.339.8
for with echo, 10k iterations50.814.69.118.526.0
function calls, 5kfails46.238.714.424.9
case, 10k iterations61.877.662.618.838.4
200 external commands303.61,485.0477.3269.0279.1

Single commands, median over 31 runs in the same rotation:

Workloadv0.5.1ac09361candidatedashBash
/usr/bin/true12.512.76.52.73.1
/bin/echo hi | /usr/bin/tr a-z A-Z12.713.06.64.04.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. for with echo is about five times faster than 0.5.1 and faster than Bash. Arithmetic and case loops run about as fast as they did through /bin/sh in 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 --version exit, from quirl-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.

On this page