Benchmarks
The repository has a benchmark harness under bin/ and benchmarks/. It measures every search mode against real corpora (WordPress, Laravel) and generated synthetic datasets, captures structured reports, and lets you diff two report series against each other.
The harness is part of the repository, not of the Composer package: since 0.2.3, benchmarks/ and the bin/bench* scripts are left out of the package, so run them from a clone (git clone https://github.com/inline0/greph && cd greph && composer install).
The published numbers in the README are sourced from GitHub Actions runs, never from local machines. Local runs are useful for iteration; CI runs are the source of truth.
Corpora
The synthetic corpora (synthetic-1k, synthetic-10k, synthetic-100k-single) are generated under benchmarks/corpora/synthetic/ on the first run. The real corpora are not included: put a checkout at benchmarks/corpora/wordpress or benchmarks/corpora/laravel (the CI workflow clones WordPress/WordPress with --depth=1). A corpus whose directory is missing is skipped.
Running benchmarks
# Full benchmark suite, every corpus that is present
./bin/bench
# Specific category (the category names used in benchmarks/suites/)
./bin/bench --category text
./bin/bench --category ast
# Compare against external tools when available
./bin/bench --compare rg,grep
./bin/bench --compare sg
# Choose a corpus
./bin/bench --corpus wordpress
./bin/bench --corpus laravel
./bin/bench --corpus synthetic-1k
# Write the run as JSON
./bin/bench --corpus wordpress --output build/run-1.json
# Measured series: 5 runs after 1 warmup run
./bin/bench-series --repeat 5 --warmup 1 --corpus wordpress --output build/head-series.json
# Aggregate separate runs into one series
./bin/bench-aggregate --output build/head-series.json build/run-*.json
bin/bench-compare base-series.json head-series.json diffs two series files (the output of bench-series or bench-aggregate) and prints a Markdown summary, useful when iterating on a hot path. bin/bench-ci triggers the Benchmark workflow on GitHub Actions through the gh CLI.
Categories
The README publishes eight groups of results, each measuring a specific layer of the engine. Each group collects one or more of the harness categories defined in benchmarks/suites/.
Scan Mode: Text
Native text search against literal, regex, and combined patterns. The published baseline runs on the WordPress corpus and includes:
- Literal
function - Literal case insensitive
- Literal whole word
- Regex new instance
- Regex array call
- Regex prefix literal
- Regex suffix literal
- Regex exact line literal
- Regex literal collapse
Scan Mode: Traversal
File walking only, no search. Measures the cost of the gitignore-aware walker on its own.
Scan Mode: Parallel Text
A fixed-string search for function with 1, 2, and 4 jobs. For that pattern the worker pool only starts above jobs * 4_000 files (see Parallel Workers), more than the WordPress corpus has, so all three published rows run in a single process and do not show pool scaling (#8).
Scan Mode: AST
Native AST search against representative patterns:
new $CLASS()array($$$ITEMS)
Indexed Text Mode
Warmed indexed text search against the same patterns as the scan mode. Includes literal, case-insensitive, whole-word, short-token, and regex queries.
Indexed Summary Queries
Warmed indexed text search using count, files-with-matches, and files-without-matches outputs. These benefit most from the postings store because the per-file work is the cheapest.
Indexed / Cached AST
Warmed AST fact search and cached AST search against the same patterns as the AST scan mode.
Build Costs
Cold-build costs for the trigram index, the AST fact index, and the AST cache. Useful when deciding which mode to maintain for a given workload.
Reading the published numbers
The README publishes a snapshot of the latest CI run for each baseline. Each section is sourced from a specific GitHub Actions run and labeled with the corpus, runner, PHP version, and number of measured runs and warmups. If you need to reproduce a number, use the same combination locally.
The comparison columns (rg, grep, sg) are gathered in the same job, on the same runner, against the same corpus. They are not extrapolated from external benchmarks.
How to add a benchmark
- Add a benchmark definition under
benchmarks/. - Add a corresponding scenario to the regression suite if there is no equivalent yet (so the behavior under measurement is also verified).
- Run
./bin/bench --category <yours>to validate the new benchmark. - Compare against
mainwith the Benchmark workflow, which runs on pull requests that touchsrc/,benchmarks/orbin/and posts a head-versus-base comparison in the run summary. - Update the README table once the CI run that includes the new benchmark has landed on
main.
Performance philosophy
Greph is not trying to outrun ripgrep or grep on cold scans. Ripgrep is implemented in Rust with hand-tuned SIMD literal scanning, and in the published numbers both rg and GNU grep are faster than Greph on every cold text scan. The goals are different:
- Be fast enough that interactive use and agent loops feel native.
- Answer repeated queries from warmed indexes: indexed regex and indexed count and file-list queries are faster than both
rgandgrepscanning the same tree. - Beat
ast-grepon indexed and cached AST workloads, where Greph's warm caches avoid reparsing the tree. Cold AST search is mixed:new $CLASS()is faster thansg,array($$$ITEMS)is slower. - Make every performance improvement measurable and reproducible.
Performance claims that are not backed by a benchmark report are not landed.