Skip to content

Benchmarks ​

Measured performance and coverage for dog vs bat. Last updated: 2026-10-01.

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
  • bat 0.26.1
  • hyperfine --warmup 0 --runs 20
  • --paging=never on both tools
  • auto-detect language (no -l flag)
  • --color=always on both tools (required — see below)
  • Catppuccin Mocha on both tools for the dog-theme vs BAT_THEME comparison; identical numbers to default UtilityDark
  • dog built 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:

SizeAvg linesdog winsAvg ratio
tiny3717/172.9×
small21917/173.6×
medium2,20617/176.1×
large17,71517/177.6×
extreme157,7402/28.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 37 lines) ​

LanguagedogbatRatio
bash5ms14ms2.7×
c5ms13ms2.4×
cpp6ms19ms3.5×
css5ms18ms3.5×
go5ms12ms2.3×
html5ms12ms2.2×
javascript6ms16ms2.9×
json6ms13ms2.3×
lua6ms13ms2.4×
markdown6ms16ms2.8×
python5ms14ms2.6×
ruby5ms14ms2.6×
rust6ms15ms2.5×
swift5ms13ms2.3×
tsx5ms27ms4.9×
typescript6ms25ms4.6×
yaml5ms12ms2.3×

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 3.5× now). In a previewer like fzf or yazi, that's the latency on every keystroke.

Small (average 219 lines) ​

LanguagedogbatRatio
bash6ms20ms3.3×
c6ms20ms3.1×
cpp7ms29ms3.9×
css6ms22ms3.8×
go7ms26ms3.6×
html9ms28ms3.1×
javascript6ms17ms2.9×
json6ms20ms3.2×
lua7ms19ms2.8×
markdown7ms29ms4.2×
python7ms24ms3.5×
ruby7ms25ms3.6×
rust6ms21ms3.3×
swift6ms19ms3.0×
tsx6ms34ms5.6×
typescript6ms36ms6.1×
yaml6ms15ms2.5×

dog wins: 17/17. Average 3.6×. Still mostly startup-bound for both tools at this size; dog's floor is simply lower.

Medium (average 2,206 lines) ​

LanguagedogbatRatio
bash11ms53ms4.7×
c16ms79ms4.8×
cpp23ms155ms6.6×
css11ms67ms6.0×
go14ms77ms5.3×
html33ms83ms2.5×
javascript17ms144ms8.3×
json11ms73ms6.4×
lua21ms84ms3.9×
markdown25ms132ms5.2×
python19ms96ms5.2×
ruby21ms115ms5.4×
rust17ms94ms5.6×
swift19ms100ms5.3×
tsx12ms102ms8.9×
typescript16ms209ms13.0×
yaml15ms109ms7.0×

dog wins: 17/17. Average 6.1×. html is the tightest (2.5×); typescript peaks at 13.0×.

Large (average 17,715 lines) ​

LanguagedogbatRatio
bash52ms319ms6.2×
c65ms373ms5.8×
cpp162ms1,249ms7.7×
css33ms242ms7.3×
go360ms2,436ms6.8×
html50ms85ms1.7×
javascript70ms797ms11.5×
json34ms305ms8.9×
lua35ms186ms5.3×
markdown71ms549ms7.7×
python118ms715ms6.1×
ruby32ms165ms5.2×
rust35ms240ms6.8×
swift39ms203ms5.3×
tsx59ms697ms11.9×
typescript404ms5,989ms14.8×
yaml18ms167ms9.5×

dog wins: 17/17. Average 7.6×. Range 1.7× (html) to 14.8× (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.

LanguageLinesdogbatRatio
c260,4931,147ms6,940ms6.1×
javascript54,987245ms2,431ms9.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.

LanguagedogbatDelta
bash100%74.9%dog +25
c100%76.6%dog +23
cpp100%69.3%dog +31
css100%86.4%dog +14
go100%68.2%dog +32
html100%83.4%dog +17
javascript100%73.6%dog +26
json100%100%tied
lua100%96.5%dog +4
markdown100%49.0%dog +51
python100%77.1%dog +23
ruby100%62.1%dog +38
rust100%75.1%dog +25
swift100%80.8%dog +19
tsx100%60.4%dog +40
typescript100%71.9%dog +28
yaml100%99.7%tied
dogbat
Average100.0%76.8%
Wins15/170/17
Ties2/172/17

Biggest gaps: markdown (+51), tsx (+40), 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=always forces it; NO_COLOR strips it; FORCE_COLOR overrides NO_COLOR
  • Exit codes — missing file exits 1; bad language flag exits non-zero; empty file exits 0
  • Stdin — echo ... | dog -l swift highlights; stdin without a language passes through as plain text
  • Binary files — produces a clear message, not a crash
  • SIGPIPE — dog file | head exits 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.

OperationWinnerns/op
Extract filename from pathUTF8View backward scan35
Extract extensionUTF8View backward scan22
Strip backup suffixPrecomputed [UInt8] + memcmp42
Parse shebangUTF8View forward scan94

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.

DecisionWinnerSavings
Theme lookupSwitch (jump table) vs Dictionary1.6ms
Token type resolutionPre-resolved at parse time vs per-token string match3.86ms
ANSI reset strategyNo reset, overwrite color vs reset-every-token1.04ms
Gap fillingTrack last color, emit on change≈0ms
Buffer write2× 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 typeTime
UnsafeMutableBufferPointer5.0ms
[UInt8]7.0ms
String11.3ms
Data19.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):

OperationTime
Alpha composite, 96 colors mixed~1ns/color
End-to-end --theme "name" resolve18–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:

sh
git clone https://github.com/edden27/dog
cd dog

Speed ​

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:

sh
bash perf/scripts/bench-matrix.sh perf/my-run/results tiny small medium large xlarge

Then print this page's tables straight from your results — same format, wins and averages included (perf/scripts/docs-tables.py):

sh
python3 perf/scripts/docs-tables.py perf/my-run/results

The published run's raw JSONs live in perf/blank-line-fix-bench/lean-results-3/, summarized as perf/baseline/blank-line-fix-2026-09-27.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:

sh
bash perf/scripts/startup-decomp.sh perf/my-run/startup
python3 perf/scripts/summarize-startup.py perf/my-run/startup perf/my-run/results

For 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 ​

sh
python3 scripts/benchmarks/bat-coverage.py

Runtime ~56s. Prints the full coverage table with pass/fail per language.

Methodology, output format, and exit-code conventions: scripts/benchmarks/bat-coverage.py.

Compatibility ​

sh
bash scripts/test/compat.sh

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

sh
bash scripts/test/test-linguist-samples.sh

Source: 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.

sh
bash scripts/generate/linux.sh all

Source: scripts/generate/linux.sh. Read the header comment for the full subcommand list and Docker requirements.