Benchmarks
Measured performance and coverage for dog vs bat. Last updated: 2026-07-18.
Conditions
All benchmarks and stats on this page were achieved under these conditions:
- Apple M2 Max (8 performance + 4 efficiency cores), 32 GB RAM, macOS 26.2
bat0.26.1hyperfine --warmup 0 --runs 20--paging=neveron both toolsauto-detectlanguage (no-lflag)--color=alwayson both tools (required — see below)Catppuccin Mochaon both tools for thedog-themevsBAT_THEMEcomparison; identical numbers to defaultUtilityDarkdogbuilt with the release command (swift build -c release -Xcc -flto=thin)
WARNING
bat MUST use --color=always in benchmarks. Without it, bat detects the pipe (/dev/null redirect) and skips highlighting entirely, appearing ~30× faster than real. dog skips color on pipes the same way, so both tools run with --color=always — every number above measures both actually highlighting.
Speed
Summary across all five file-size tiers:
| Size | Avg lines | dog wins | Avg ratio |
|---|---|---|---|
| tiny | 30 | 17/17 | 2.8× |
| small | 198 | 17/17 | 3.5× |
| medium | 2,208 | 17/17 | 6.0× |
| large | 17,129 | 17/17 | 7.4× |
| extreme | 157,740 | 2/2 | 8.0× |
There is no crossover: dog wins every language at every size. Highlight queries are compiled at build time and embedded in the binary, so a run starts doing useful work within ~1ms of launch — the per-language startup cost that used to dominate tiny files is gone. The gap then widens as files grow.
Tiny (average 30 lines)
| Language | dog | bat | Ratio |
|---|---|---|---|
| bash | 5ms | 14ms | 2.7× |
| c | 5ms | 12ms | 2.3× |
| cpp | 5ms | 14ms | 2.8× |
| css | 5ms | 17ms | 3.4× |
| go | 5ms | 11ms | 2.1× |
| html | 5ms | 11ms | 2.1× |
| javascript | 5ms | 15ms | 2.8× |
| json | 5ms | 12ms | 2.3× |
| lua | 5ms | 12ms | 2.2× |
| markdown | 5ms | 15ms | 2.8× |
| python | 5ms | 14ms | 2.8× |
| ruby | 6ms | 14ms | 2.6× |
| rust | 5ms | 13ms | 2.5× |
| swift | 5ms | 12ms | 2.3× |
| tsx | 5ms | 22ms | 4.4× |
| typescript | 5ms | 24ms | 4.7× |
| yaml | 5ms | 11ms | 2.2× |
dog wins: 17/17. Every language starts up in ~5ms — precompiled queries removed the per-language startup cost that used to sink heavy grammars here (cpp was 0.1× before; it is 2.8× now). In a previewer like fzf or yazi, that's the latency on every keystroke.
Small (average 198 lines)
| Language | dog | bat | Ratio |
|---|---|---|---|
| bash | 6ms | 19ms | 3.3× |
| c | 6ms | 19ms | 3.1× |
| cpp | 7ms | 27ms | 3.9× |
| css | 5ms | 21ms | 3.9× |
| go | 6ms | 20ms | 3.2× |
| html | 9ms | 26ms | 3.0× |
| javascript | 6ms | 18ms | 3.1× |
| json | 5ms | 14ms | 2.7× |
| lua | 7ms | 18ms | 2.7× |
| markdown | 5ms | 16ms | 3.3× |
| python | 6ms | 22ms | 3.6× |
| ruby | 7ms | 23ms | 3.6× |
| rust | 6ms | 18ms | 3.0× |
| swift | 6ms | 18ms | 2.9× |
| tsx | 6ms | 33ms | 5.6× |
| typescript | 6ms | 32ms | 5.7× |
| yaml | 5ms | 13ms | 2.4× |
dog wins: 17/17. Average 3.5×. Still mostly startup-bound for both tools at this size; dog's floor is simply lower.
Medium (average 2,208 lines)
| Language | dog | bat | Ratio |
|---|---|---|---|
| bash | 18ms | 83ms | 4.7× |
| c | 18ms | 87ms | 4.9× |
| cpp | 23ms | 150ms | 6.5× |
| css | 10ms | 55ms | 5.7× |
| go | 15ms | 75ms | 5.1× |
| html | 32ms | 80ms | 2.5× |
| javascript | 23ms | 168ms | 7.2× |
| json | 11ms | 70ms | 6.2× |
| lua | 21ms | 82ms | 3.9× |
| markdown | 25ms | 129ms | 5.2× |
| python | 18ms | 93ms | 5.2× |
| ruby | 21ms | 111ms | 5.4× |
| rust | 16ms | 84ms | 5.4× |
| swift | 18ms | 97ms | 5.3× |
| tsx | 11ms | 98ms | 9.0× |
| typescript | 16ms | 197ms | 12.0× |
| yaml | 15ms | 99ms | 6.8× |
dog wins: 17/17. Average 6.0×. html is the tightest (2.5×); typescript peaks at 12.0×.
Large (average 17,129 lines)
| Language | dog | bat | Ratio |
|---|---|---|---|
| bash | 44ms | 269ms | 6.1× |
| c | 63ms | 366ms | 5.8× |
| cpp | 159ms | 1,219ms | 7.7× |
| css | 32ms | 205ms | 6.4× |
| go | 356ms | 2,400ms | 6.7× |
| html | 48ms | 82ms | 1.7× |
| javascript | 62ms | 626ms | 10.1× |
| json | 38ms | 350ms | 9.1× |
| lua | 35ms | 181ms | 5.2× |
| markdown | 69ms | 536ms | 7.7× |
| python | 116ms | 702ms | 6.1× |
| ruby | 31ms | 161ms | 5.2× |
| rust | 35ms | 235ms | 6.8× |
| swift | 38ms | 200ms | 5.3× |
| tsx | 48ms | 565ms | 11.9× |
| typescript | 401ms | 5,837ms | 14.6× |
| yaml | 17ms | 163ms | 9.5× |
dog wins: 17/17. Average 7.4×. Range 1.7× (html) to 14.6× (typescript).
INFO
Fixture sizes at this tier vary a lot. html large is 2,381 lines (effectively medium-sized), which is why its ratio looks tight. ruby large is 2,689 lines for the same reason. go large is 79,703 lines and typescript large is 54,434 lines — those are the genuine stress tests.
Extreme
Only c and javascript have xlarge fixtures; the other 15 languages are skipped.
| Language | Lines | dog | bat | Ratio |
|---|---|---|---|---|
| c | 260,493 | 1,147ms | 6,940ms | 6.1× |
| javascript | 54,987 | 245ms | 2,431ms | 9.9× |
c xlarge is the single biggest fixture in the suite — dog finishes it in 1.1s while bat takes 6.9s, dog's biggest absolute save at 5.8 seconds.
🐕 Dog Fact
The Norwegian Lundehund is the only breed with six toes on each foot. Surprisingly, doesn't seem to improve their typing speed.
Coverage
Non-whitespace source bytes that receive a syntax color. Coverage is measured by parsing dog's and bat's ANSI output, mapping colored bytes back to source positions, and computing colored / total.
| Language | dog | bat | Delta |
|---|---|---|---|
| bash | 100% | 92.7% | dog +7 |
| c | 100% | 76.6% | dog +23 |
| cpp | 100% | 69.3% | dog +31 |
| css | 100% | 89.9% | dog +10 |
| go | 100% | 68.3% | dog +32 |
| html | 100% | 83.4% | dog +17 |
| javascript | 100% | 71.3% | dog +29 |
| json | 100% | 100% | tied |
| lua | 100% | 96.5% | dog +4 |
| markdown | 100% | 48.8% | dog +51 |
| python | 100% | 77.1% | dog +23 |
| ruby | 100% | 62.1% | dog +38 |
| rust | 100% | 71.0% | dog +29 |
| swift | 100% | 80.8% | dog +19 |
| tsx | 100% | 52.7% | dog +47 |
| typescript | 100% | 71.8% | dog +28 |
| yaml | 100% | 99.7% | tied |
| dog | bat | |
|---|---|---|
| Average | 100.0% | 77.2% |
| Wins | 15/17 | 0/17 |
| Ties | 2/17 | 2/17 |
Biggest gaps: markdown (+51), tsx (+47), ruby (+38), go (+32), cpp (+31).
Compatibility
Everyday bat behaviors that dog matches in 0.1 — not full feature parity, but the basics existing pipelines rely on. Run against the current binary with bash scripts/test/compat.sh.
13 scenarios covering:
- Color behavior — pipe strips color;
--color=alwaysforces it;NO_COLORstrips it;FORCE_COLORoverridesNO_COLOR - Exit codes — missing file exits
1; bad language flag exits non-zero; empty file exits0 - Stdin —
echo ... | dog -l swifthighlights; stdin without a language passes through as plain text - Binary files — produces a clear message, not a crash
- SIGPIPE —
dog file | headexits cleanly, no broken-pipe error --plain— runs without error on a real fixture--list-languages— prints the supported-language list
Script source: scripts/test/compat.sh. Runtime ~1s.
🐕🦺 Dog Fact
Dogs reduce muscular work by 70% through energy exchange (non-spiritual) with each step; cats only reduce it by 37%.
Performance tuning finds
dog's internal hot paths were benchmarked independently as implementation decisions were made. A few non-obvious results:
Language detection cascade
Every file path goes through a four-stage detector. Total cost: ~193 nanoseconds.
| Operation | Winner | ns/op |
|---|---|---|
| Extract filename from path | UTF8View backward scan | 35 |
| Extract extension | UTF8View backward scan | 22 |
| Strip backup suffix | Precomputed [UInt8] + memcmp | 42 |
| Parse shebang | UTF8View forward scan | 94 |
Notable: parsing a shebang with Swift's split(separator:) costs 1,311 ns/op — 14× slower than the UTF8View scan that won. String allocations add up fast when you're doing multi-step parsing. Full write-up: scripts/benchmarks/bench-detection-README.md.
Render loop
The render loop takes parsed tokens and emits ANSI bytes. Five independent decisions were benchmarked; one of them (pre-resolving token types during parse) saved more time than all the others combined.
| Decision | Winner | Savings |
|---|---|---|
| Theme lookup | Switch (jump table) vs Dictionary | 1.6ms |
| Token type resolution | Pre-resolved at parse time vs per-token string match | 3.86ms |
| ANSI reset strategy | No reset, overwrite color vs reset-every-token | 1.04ms |
| Gap filling | Track last color, emit on change | ≈0ms |
| Buffer write | 2× append(contentsOf:) vs concat vs Unsafe | ≈0ms |
Baseline render loop: 13.56ms. Optimized: 8.05ms. 1.7× faster render loop, 41% improvement, 11% smaller output.
Buffer type
[UInt8] vs Data vs String vs UnsafeMutableBufferPointer for the output buffer on a 20,000-line file:
| Buffer type | Time |
|---|---|
UnsafeMutableBufferPointer | 5.0ms |
[UInt8] | 7.0ms |
String | 11.3ms |
Data | 19.7ms |
Unsafe is 2ms faster but requires manual memory management and has no bounds checking. [UInt8] was chosen — the 2ms isn't worth silent memory corruption. Data is shockingly bad (3.9× slower than Unsafe) due to Foundation bridging overhead.
Theme loading
Zero-copy JSON scanner benchmarks against a 21-theme directory (all measured in a release build on Apple Silicon):
| Operation | Time |
|---|---|
| Alpha composite, 96 colors mixed | ~1ns/color |
End-to-end --theme "name" resolve | 18–87µs |
Saved default load (--set-default-theme) | 17µs |
The worst cases stay sub-millisecond: a name whose file shares none of its words (~0.65ms) and a name that matches nothing (~0.8ms — every file has to be read to prove a miss).
Theme load cost is sub-millisecond, which is why pinning --theme 'Catppuccin Mocha' vs the built-in UtilityDark doesn't change benchmark times on medium or larger files. A saved default is resolved once at set time and loaded precomputed, so benchmark numbers read the same with or without one set.
Reproduce
The harness scripts live in the dog repo under perf/scripts/, scripts/benchmarks/, and scripts/test/ — the raw data and baselines behind the numbers above ship under perf/ — and the commands below run from its root:
git clone https://github.com/edden27/dog
cd dogSpeed
The tables above come from perf/scripts/bench-matrix.sh — its defaults are the exact conditions listed at the top of this page (--warmup 0 --runs 20, auto-detect, pager off) and it keeps every raw hyperfine JSON. Full matrix, all 17 languages, every size:
bash perf/scripts/bench-matrix.sh perf/my-run/results tiny small medium large xlargeThen print this page's tables straight from your results — same format, wins and averages included (perf/scripts/docs-tables.py):
python3 perf/scripts/docs-tables.py perf/my-run/resultsThe published run's raw JSONs live in perf/docs-bench/results/, summarized as perf/baseline/docs-2026-07-18.csv — perf/scripts/summarize-baseline.py compares any new run against it.
The ~5ms startup number has its own harness, perf/scripts/startup-decomp.sh — it splits startup cost into pure process launch, per-language init, and actual parsing. perf/scripts/summarize-startup.py then prints the per-language table with those costs broken out:
bash perf/scripts/startup-decomp.sh perf/my-run/startup
python3 perf/scripts/summarize-startup.py perf/my-run/startup perf/my-run/resultsFor a quick one-off comparison there's also scripts/benchmarks/bench.sh — note its defaults (--warmup 2 --runs 5) are not the published conditions. Its env knobs (warmup, runs, theme pinning, plain mode, etc.) are documented in the script's top comment.
Coverage
python3 scripts/benchmarks/bat-coverage.pyRuntime ~56s. Prints the full coverage table with pass/fail per language.
Methodology, output format, and exit-code conventions: scripts/benchmarks/bat-coverage.py.
Compatibility
bash scripts/test/compat.sh13 drop-in scenarios. Runtime ~1s. Full scenario list with plain-English descriptions: scripts/test/COMPAT.md.
Detection soundness
Independent of the benchmark harness, dog's language detection is verified against GitHub Linguist's real-world sample files:
bash scripts/test/test-linguist-samples.shSource: scripts/test/test-linguist-samples.sh.
Fixtures
Benchmark fixtures ship in the repo at scripts/fixtures/performance/<language>/<size>.<ext> — real-world files pulled from public GitHub repos, so every number above can be reproduced against the exact same inputs.
Cross-platform build and test
Linux builds and cross-platform test runs go through a Docker-based script.
bash scripts/generate/linux.sh allSource: scripts/generate/linux.sh. Read the header comment for the full subcommand list and Docker requirements.