Skip to content

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
  • 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
tiny3017/172.8×
small19817/173.5×
medium2,20817/176.0×
large17,12917/177.4×
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 30 lines)

LanguagedogbatRatio
bash5ms14ms2.7×
c5ms12ms2.3×
cpp5ms14ms2.8×
css5ms17ms3.4×
go5ms11ms2.1×
html5ms11ms2.1×
javascript5ms15ms2.8×
json5ms12ms2.3×
lua5ms12ms2.2×
markdown5ms15ms2.8×
python5ms14ms2.8×
ruby6ms14ms2.6×
rust5ms13ms2.5×
swift5ms12ms2.3×
tsx5ms22ms4.4×
typescript5ms24ms4.7×
yaml5ms11ms2.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)

LanguagedogbatRatio
bash6ms19ms3.3×
c6ms19ms3.1×
cpp7ms27ms3.9×
css5ms21ms3.9×
go6ms20ms3.2×
html9ms26ms3.0×
javascript6ms18ms3.1×
json5ms14ms2.7×
lua7ms18ms2.7×
markdown5ms16ms3.3×
python6ms22ms3.6×
ruby7ms23ms3.6×
rust6ms18ms3.0×
swift6ms18ms2.9×
tsx6ms33ms5.6×
typescript6ms32ms5.7×
yaml5ms13ms2.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)

LanguagedogbatRatio
bash18ms83ms4.7×
c18ms87ms4.9×
cpp23ms150ms6.5×
css10ms55ms5.7×
go15ms75ms5.1×
html32ms80ms2.5×
javascript23ms168ms7.2×
json11ms70ms6.2×
lua21ms82ms3.9×
markdown25ms129ms5.2×
python18ms93ms5.2×
ruby21ms111ms5.4×
rust16ms84ms5.4×
swift18ms97ms5.3×
tsx11ms98ms9.0×
typescript16ms197ms12.0×
yaml15ms99ms6.8×

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

Large (average 17,129 lines)

LanguagedogbatRatio
bash44ms269ms6.1×
c63ms366ms5.8×
cpp159ms1,219ms7.7×
css32ms205ms6.4×
go356ms2,400ms6.7×
html48ms82ms1.7×
javascript62ms626ms10.1×
json38ms350ms9.1×
lua35ms181ms5.2×
markdown69ms536ms7.7×
python116ms702ms6.1×
ruby31ms161ms5.2×
rust35ms235ms6.8×
swift38ms200ms5.3×
tsx48ms565ms11.9×
typescript401ms5,837ms14.6×
yaml17ms163ms9.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.

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%92.7%dog +7
c100%76.6%dog +23
cpp100%69.3%dog +31
css100%89.9%dog +10
go100%68.3%dog +32
html100%83.4%dog +17
javascript100%71.3%dog +29
json100%100%tied
lua100%96.5%dog +4
markdown100%48.8%dog +51
python100%77.1%dog +23
ruby100%62.1%dog +38
rust100%71.0%dog +29
swift100%80.8%dog +19
tsx100%52.7%dog +47
typescript100%71.8%dog +28
yaml100%99.7%tied
dogbat
Average100.0%77.2%
Wins15/170/17
Ties2/172/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=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
  • Stdinecho ... | dog -l swift highlights; stdin without a language passes through as plain text
  • Binary files — produces a clear message, not a crash
  • SIGPIPEdog 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 writeappend(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/docs-bench/results/, summarized as perf/baseline/docs-2026-07-18.csvperf/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.