Parser performance
usage's compiled Rust parser is designed so ordinary argv parsing reads static tables and writes directly into the result. Cold metadata for help, specs, and completions is not constructed on a successful parse.
Mise-scale result
The gate uses generated shadows of the same checked-in mise spec: 211 commands, 722 flags, 129 positional arguments, and four command levels. A third binary does the same startup work without parsing, so instruction and wall-time measurements subtract startup from two runs of the same binary.
| Framework | Instructions | vs usage | Wall time | Allocations, no values bound |
|---|---|---|---|---|
| usage | 7,377 | — | 0.7 µs | 0 |
| clap | 6.31M | 855x | 544 µs | 6,560 |
| bpaf | 21.9M | 2,957x | 1,610 µs | — |
clap and bpaf construct and validate a parser at runtime before reading a single word, so their floor is hundreds of microseconds. usage reads tables the compiler already laid out, so its floor is hundreds of nanoseconds.
Why it is fast
The derive does the expensive work while the application is compiled. It emits the command tree as static Command, Flag, and Arg tables, so starting a parse does not construct, validate, or allocate a parser. The hot path then does only the work the argv asks for:
- It reads argv once, borrowing each value from its
OsStrinstead of copying it. - A flag lookup scans the current command's flags and inherited globals, not all 722 flags in the CLI. Once found, a generated integer key selects the destination field without another string lookup.
- The parser's command stack is a fixed-size array. A bare parse never reaches the heap; an owned value such as
Stringaccounts for one allocation only when that value is actually bound. - Help text, spec and completion metadata, and human-readable diagnostics live outside the parse tables. A successful parse neither constructs nor reads them.
- Generated binding writes into one accumulator in place. Selecting a subcommand creates only that variant's accumulator, not storage for every command the user did not select.
The result scales with the command path and values that were typed, rather than with the whole CLI.
Compile time
Compiling the mise-scale shadow is slower with usage-rs: a debug rebuild took 10.2 seconds, compared with 3.6 seconds for clap and 1.6 seconds for bpaf. Each framework's dependencies were already built; measurements used rustc 1.97.1 on x86_64 Linux.
What clap's number includes
For this generated command tree, clap's approximately 544 µs consists of about 343 µs constructing the tree, 178 µs validating it, and 23 µs parsing. usage's tables are compiled, so its 0.7 µs path has no equivalent construction or validation phase. Even compared only with clap's already-built parse phase, usage is about 34x faster in this measurement.
Binary size
The gate's parse-only binaries — each linking its framework's mise-scale shadow, built by the same workspace release build, stripped:
| Framework | Bytes | Decimal MB |
|---|---|---|
| usage | 1,319,424 | 1.3 MB |
| bpaf | 2,493,936 | 2.5 MB |
| clap | 3,102,696 | 3.1 MB |
Where the size lives, and what removes it
The runtime crate itself compiles to about 16 KB; nearly all of the parser's footprint is the per-command binding code the derive generates, plus the static tables it reads (about 150 KB at mise scale). Size therefore tracks how many commands and fields a CLI declares, not which usage features it turns on.
Two profile settings any CLI can apply cut further, independent of usage:
strip = trueremoves symbols and debug info — the tables above already assume it.panic = "abort"removes unwinding landing pads and the backtrace machinery std otherwise links. On the mise-scale binary this is another 84 KB (−6%).
Method and limits
- Instruction counts come from Cachegrind, whose run-to-run variation on the benchmark host is much lower than wall-clock timing.
takruns the release binaries repeatedly and reports the difference between the no-parse and parse paths.- Every shadow is generated from the same spec, and each comparison framework intentionally drops what it cannot express.
- Binary sizes are the gate's
parse-n*binaries from a full workspace release build, stripped, against rustc 1.97.1 on x86_64-unknown-linux-gnu. The workspace build matters: cargo unifies features, so clap gets the features (color, suggestions, help, derive, env) a real CLI of this size enables. - This measures routing and parsing, not process startup, configuration loading, command execution, help rendering, or completion generation.
- Refreshing or growing the mise fixture moves the absolute counts. Compare parsers in the same commit, not one commit's count against another's.
The benchmark sources live in benches/gate, with generated shadows under benches/shadows. Gate maintenance — including the markdown benchmark on this repository's own spec — is documented in Contributing.