Where Sley beats Git, and where it doesn't
Sley is
the Git engine I wanted and couldn't find: a Rust library light enough to embed and
fast enough to sit in the hot path. You link it into a process and call it directly
instead of shelling out to the git binary and parsing its stdout. So
the test that matters is the real one. I timed all eight everyday commands against
the system git binary (2.54.0) on three real repos. The wins land where
there's real work to do: log runs at half Git's time on every repo, the
mid-size repo's everyday commands all roughly halve, and on Flutter the heavy ones
(full status, commit) run a third to a half faster. The
cheapest commands, where most of the clock is process startup, come out a wash.
The repos are all public: benhoyt/inih (61 files), we-promise/sure (2,765 files), and flutter/flutter (15,102 files). Each number is the median of fifteen timed
runs (eleven for staged writes, nine for commit) after warmups,
wall-clock from process start to exit, on an Apple-Silicon Mac (APFS, git 2.54.0).
The runner interleaves the tools each round and rotates their order, so run position
can't quietly favor one. And it checks itself: every command's output is compared to
Git's outside the timing window, the same status rows, diff paths and commits, with
zero mismatches across all three matrices. Nothing here is fast by doing less.
Two caveats I'll state plainly. Git's untracked-cache and fsmonitor are off for both
tools, because they cache the working-tree walk across runs and what I'm measuring is
the cold walk: the first run on a fresh checkout, in CI, or any process without a
warmed daemon. With them on, Git's status is competitive again; that's a
real config, just a different benchmark. And every number is a full CLI process,
Sley's against Git's, both paying the same ~12ms of startup. Embedded, you skip that
cost entirely; more on that below.
Small and mid-size: near the floor
On the small repo (benhoyt/inih) most commands finish around the process-start floor
and the two trade them in the noise. The clear exception is log: Git
takes about 20ms, Sley 11, and that gap holds at every size. The mid-size repo
separates cleanly: status -uno, both diffs, add and add -u all fall to ~11ms against Git's ~21, a clean halving. status and commit there are washes, both near 40ms. The
real spread waits for a big repo.
The wins come from the index. Sley keeps Git's semantics in typed core models
(a crate for the index, one for objects, one for refs), and when a command only
needs paths and modes, the index hands it a borrowed view sliced out of the
on-disk file instead of allocating an owned entry per path. status streams those entries; add of a known path stages straight through
the index instead of walking the worktree first. At a few hundred entries that's
the whole game.
Flutter: where it pulls ahead
Flutter splits in two. The heavy commands win big: full status runs in
129ms to Git's 187, commit in 41ms to Git's 76, and log in
11ms to Git's 21, each a third to a half off. The lighter reads
(status -uno, both diffs, add, add -u) are
already down around 40ms for both tools and come in level, a hair either way. The
commit gap is the steadiest cell on the board: 41ms against 76, within a millisecond
run to run for Sley.
Full status was the last to fall. It's mostly one thing, walking the
fifteen-thousand-file tree for files Git doesn't track yet, and for a long time
Sley did that walk about as fast as Git and no faster, a dead heat near 190ms. Then
I changed how the walk works, streaming untracked entries as they're found instead
of collecting the whole tree first. Same command, same checkout: a median of 129
against Git's 187. It's the heaviest cell on the board, and once the run settled both
tools held to within a couple of milliseconds, with the gap sitting near sixty.
commit is roughly half of Git and full status --short a
third faster (129 to 187); log drops to 11 from 21. The lighter reads
are already ~40ms and come in level. status --short dwarfs the rest
because it walks the whole 15,000-file tree for untracked files.| command | small | medium | large |
|---|---|---|---|
status --short | 0.55× 21→11 ms | 1.00× 41→41 ms | 0.69× 187→129 ms |
status -uno | 0.74× 16→12 ms | 0.53× 22→11 ms | 0.93× 43→40 ms |
diff --name-only | 0.96× 11→11 ms | 0.54× 21→11 ms | 0.93× 43→40 ms |
diff --stat | 0.96× 12→12 ms | 0.54× 21→11 ms | 0.99× 42→41 ms |
log --oneline | 0.55× 20→11 ms | 0.55× 21→12 ms | 0.54× 21→12 ms |
add <file> | 0.90× 12→10 ms | 0.54× 21→11 ms | 0.99× 21→21 ms |
add -u | 1.02× 11→11 ms | 0.54× 21→11 ms | 0.98× 41→41 ms |
commit -m | 1.02× 40→40 ms | 0.98× 41→40 ms | 0.54× 76→41 ms |
git 2.54.0, fifteen runs per read (eleven per write, nine per commit) after warmups, interleaved and rotated. The small-repo cells,
and the cheapest commands generally, sit at the ~12ms process-start floor where
run-to-run noise is the whole margin; the repeatable wins are the larger-margin
cells, the reads on bigger repos and everything on Flutter. Every cell's output was
checked against Git's: zero mismatches. Apple-Silicon Mac, APFS; untracked-cache
and fsmonitor off for both; Sley @ 28555c8.Lighter, and faster embedded
Speed isn't the only number that moved. Peak memory (the high-water mark a process touches, measured the same way with five runs a cell) runs well under half of Git's on a small repo: ~3.8 MiB to Git's ~8.4. The gap narrows as the tree grows but never closes: ~0.55× at mid-size, ~0.67× on Flutter, and Sley stays under Git on every command.
wait4 ru_maxrss, five runs a cell. Some of Git's floor
is the binary and its linked libraries resident in memory; Sley is a single static
binary. The two run-heavy Flutter commands, commit and add -u, are where the two nearly converge, both genuinely
allocating then.And it's faster than its own CLI when embedded. Everything above is the CLI paying
to start a process and format text for a human.
Embedded, with the repo opened once and the library called in a warm loop the way
you'd actually use it, you skip both. On the small repo that overhead is most of the
number: status is about 0.7ms as a library call against 11ms at the CLI,
and diff --name-only is 0.1ms against 11. The work is a fraction of a
millisecond; the rest is process startup and output you never render. On Flutter,
where walking the tree is the actual work, the fixed cost barely shows: in-process status is ~101ms against the CLI's 129. The smaller and more frequent
the call, the more embedding wins; the big slow ones are bounded by the work itself.
Parity is the gate
The cheap way to win a Git benchmark is to do less work and call it equivalent:
skip the awkward edge case, approximate the output, declare victory. So correctness
isn't a phase at the end here; it's the constraint the whole way through. Sley runs
against Git's own test suite, the real t-files from the upstream source tree, and
checks output, exit status, and side effects against the actual git binary, cell by cell. It isn't all of Git yet: it passes 19,307 of those assertions,
68% of the in-scope suite (skips excluded), across 844 of Git's test files,
everything but the serving side of transport, foreign-SCM, and email. The
commands you reach for first are furthest along (86% on setup and config, 73% on
plumbing, 74% on branch/merge/rebase), and the per-file floors in CI only ratchet
up, so a number that's green today can't quietly go red tomorrow. A fast path
doesn't merge because it "seems equivalent"; it merges because it gives Git's
answer and the floor held. The whole scoreboard is public, down to the individual
failing cell: the Sley ⇄ Git parity report.
The pattern was the same every time: find the work Sley was doing that Git wasn't, and stop. What's left is a millisecond here and there on the cheapest commands, bounded by the filesystem and the kernel, not by Sley. That's a good place to be.
Sley is open source at github.com/HeddleCo/sley.