|
Lime Parser Generator 0.1.0
Runtime-extensible LALR(1) parser with SIMD tokenization and LLVM JIT
|
A side-by-side measurement of Lime against GNU Bison 3.8.2 on identical grammars, running on the same hardware with the same compiler.
TL;DR: On a fair lex+parse JSON benchmark Lime is 1.69× faster than Bison+Flex (consistent across v0.2.7-v0.4.4 on x86_64 i9-12900H). On a parser-only arithmetic benchmark Lime runs at 0.81× of Bison – Bison's tight-loop arith parser edges Lime by ~20%, a result that has held steady through every release in the v0.2 / v0.3 / v0.4 series (verified by bisecting v0.2.7 / v0.3.3 / v0.4.4 – all measured 0.81-0.85×).
Lime still generates parsers measurably faster (~4× on a 250-rule grammar – last measured on Lime 0.1.0; the gap is unlikely to have shrunk). Generated source is larger (1.47× on the older benchmark) because Lime carries more introspection metadata. Pick Lime when you need runtime grammar extension, the SIMD lexer, the LLVM JIT for hot grammars, or modern error diagnostics. Pick Bison when you need IELR(1) or its long production track record.
This document is a working measurement, not a marketing page. Numbers are reproducible and the methodology is below.
Reproducibility: x86_64 i9-12900H Linux, gcc 15.2.0, glibc 2.42, bison 3.8.2 + flex 2.6.4 from nix store. meson setup --buildtype=debugoptimized -Dllvm=disabled. Run bench/ bench_flex_bison_compare/bench_flex_bison_compare.
This is the headline number. Both sides perform an honest lex-and-parse cycle on the same 515-byte JSON document with nested objects and arrays. The Bison side runs Flex 2.6.4 + json__scan_string + json_parse. The Lime side runs a small hand-rolled tokenizer in a loop, feeding tokens to parse_token as it goes. Same input bytes, same logical work, same harness.
100,000 iterations per trial, 5 trials, median (3-trial sweep):
| Tool | mean (ms) | per-doc | per-token | vs Bison |
|---|---|---|---|---|
| bison + flex | 263.0 | 2.63 µs | ~28 ns | 1.00× |
| lime | 155.6 | 1.56 µs | ~16 ns | 1.69× |
Lime's lexer (hand-rolled per-call inner loop) outpaces Flex's table-driven scanner. This is the user-visible number for shipped consumers (PG team, etc.).
bench/bench_flex_bison_compare/: 12-rule arithmetic grammar, parsing (1 + 2) * (3 + 4) - 5 (13 tokens). 100,000 iterations per trial, 3-trial sweep:
| Tool | min (ms) | mean (ms) | per-parse | per-token | vs Bison |
|---|---|---|---|---|---|
| bison | 23.7 | 24.2 | 242 ns | 19 ns | 1.00× |
| lime | 27.4 | 29.8 | 298 ns | 23 ns | 0.81× |
The arith bench is parser-internals only: pre-tokenized input fed to Lime's parse_token push API, vs Bison's pull-mode bison_parse driving its own static state machine. Bison's generated arith parser fits in L1 cache and beats Lime's runtime push engine on this micro-workload.
When the workload includes lexing (the JSON benchmark above), the order reverses by ~2×. Real applications combine lex+parse, so the JSON number is the practical comparison.
This number has been stable across the v0.2 / v0.3 / v0.4 series (measured at v0.2.7: 0.85×, v0.3.3 pre-GLR: 0.81×, v0.4.4 current: 0.81×). The GLR engine merged in v0.3.4 has zero impact on this result – LALR fast-path files are byte-identical pre/post merge.
The earlier doc claim of 1.15× faster on this bench was from m3pro hardware with different bison + glibc; corrected to the stable x86_64 measurement during the v0.4.4 perf-audit pass.
tests/test_pg_grammar: full PostgreSQL SQL grammar (3,842 LR states, 3,584 rules, 557 terminals, 145,227 action-table entries).
| Codegen path | Compile time | Notes |
|---|---|---|
Original (fully unrolled state × lookahead switch) | did not terminate within 5 minutes | LLVM default<O2> does not scale on ~146 K basic blocks |
| With O2 disabled, still unrolled | ~186 s | LLVM mandatory lowering still slow on that IR size |
Compact (generate_find_shift_action_compact)** | **~19 ms** | tables baked as constant globals, ~30 IR instructions |
The compact path kicks in above IR_SIZE_THRESHOLD = 500_000 (nstate × nterminal). Below the threshold the unrolled path is still used because its constant-folded action returns are faster per call on small grammars.
bench/jit_comparison: synthetic 128-state grammar with 64 terminals, action table of 8,192 entries.
| Path | Per-parse |
|---|---|
| Interpreted (table walk in C) | 98 ns |
| JIT (after warmup) | 43 ns |
JIT is 2.28× faster than the interpreter on this grammar size. JIT compile time on the same grammar is 750 ms; break-even after ~13.6 M parses. This is the workload class where JIT shines: a single grammar parsed many times in a long-running process (a database, a language server, a query engine).
bench/parser_bench: SIMD vs scalar character classification on 4 KB through 256 KB inputs, AVX2 on x86_64 / NEON on aarch64.
| Path | Time per 4 KB classify |
|---|---|
| Scalar | 3171 ns |
| SIMD (best available) | 1784 ns |
SIMD is 1.78× faster on classification. End-to-end tokenizer throughput holds steady at 99-101 MB/s across input sizes.
simdjson is a SIMD-accelerated JSON-only parser; Lime is a generic LALR(1) parser generator that happens to be hosting a JSON grammar. simdjson wins; the question is by how much, and how much of the gap is allocation cost vs parsing cost.
bench/bench_simdjson_compare/ runs Lime in three allocator modes and simdjson in its standard ondemand mode. Median of 5 trials, 100 K iterations each, on a 515-byte JSON document (Apple M1, clang 21.1.8 -O2, simdjson 4.6.4):
| Tool | mean (ms) | MB/s | docs/s | gap to simdjson |
|---|---|---|---|---|
| lime+jit malloc (full free) | 622 | 79 | 161 K | 20.3× |
| lime+jit malloc-leak (no free) | 561 | 88 | 178 K | 18.3× |
| lime+jit arena (zero alloc steady) | 383 | 128 | 261 K | 12.5× |
| simdjson ondemand | 31 | 1603 | 3.3 M | 1.0× |
Cost breakdown:
| Component | ms | % of malloc total |
|---|---|---|
free() walk | 61 | 10 % |
malloc() per node | 178 | 29 % |
| Parse machinery | 383 | 61 % |
| Total | 622 | 100 % |
So:
The original publication of this document reported Bison 1.29× faster than Lime at parse throughput on a 55-token SQL stream (525 ns vs 675 ns median). That measurement used the bench/bench_grammar.{y,lime} 250-rule SQL-like grammar – a different benchmark than the arithmetic one above, so the numbers are not directly comparable to the current table. Several rounds of work since 0.1.0 have closed and reversed the gap on the arithmetic benchmark:
find_shift_action access pattern, improving cache hit rate.int16_t -> int32_t widening of the offset arrays (correctness fix; trivial perf impact on small grammars).A re-run of the original 250-rule SQL benchmark on the current codebase is on the to-do list; an apples-to-apples comparison against the 0.1.0 baseline is the right way to verify the turnaround on a larger grammar.
-O2 -std=c11nix develop -> flake.nix)
The Nix flake provides every tool used; no host install required.
The arithmetic-grammar benchmark is wall-clock time across 5 inner trials; on a quiet machine the variance run-to-run is ~10 %. Take the median of several outer runs for a stable number.
parse_token); Bison is pull (parser calls yylex()). The shape of the integration affects the measured numbers as much as the parser internals do.
Performance numbers in isolation are misleading. Here is what each tool actually gives you:
| Feature | Bison 3.8.2 | Lime 0.2.4 |
|---|---|---|
| Parser generator in one file | ✗ | ✓ (single lime.c) |
| Public domain license | ✗ (GPL exception) | ✓ |
| Push-parser API | ✗ (pull via yylex) | ✓ |
| Reentrant by default | ✗ (opt-in) | ✓ |
| Runtime grammar extension | ✗ | ✓ |
| SIMD-accelerated tokenizer | ✗ | ✓ (AVX2/NEON) |
| LLVM JIT | ✗ | ✓ (auto-tuned per grammar size) |
symbol_prefix for namespace isolation | ✗ | ✓ |
Built-in lexer compiler (-X flag) | (separate Flex) | ✓ |
| Format and lint flags | ✗ | ✓ (-F, -L) |
| Structured error diagnostics | ✗ | ✓ (RFC 0059) |
ParseTokenName, ParseExpectedTokens | ✗ | ✓ |
| Copy-on-write snapshots for thread safety | ✗ | ✓ |
Error recovery with error nonterminal | ✓ | ✓ (inherited from Lemon) |
| Generalized-LR with merge functions | ✓ | ✓ (since v0.3.4; opt-in via lime_parse_glr(), ~5–8× slower than LALR fast path on unambiguous input – see GLR.md) |
| IELR(1) / LALR(1) algorithm choice | ✓ | LALR(1) only |
| Mature, large user base | ✓ | ✗ (newer project) |
Choose Bison if:
Choose Lime if: