pack: use LRU eviction for delta bases by msuozzo · Pull Request #7410 · libgit2/libgit2 · GitHub
Skip to content

pack: use LRU eviction for delta bases - #7410

Open
msuozzo wants to merge 1 commit into
libgit2:mainfrom
msuozzo:push-yzwltlzzowyv
Open

msuozzo wants to merge 1 commit into
libgit2:mainfrom
msuozzo:push-yzwltlzzowyv

Conversation

@msuozzo

@msuozzo msuozzo commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

When a new base doesn't fit in a pack's delta base cache, libgit2 frees every cached base that isn't in use. Before 9c62aaa it freed the eight least recently used, but finding those meant scanning the whole cache on every eviction, and dropping everything turned out faster, since "the most used entries are quickly back into the cache".

They come back by being rebuilt, though. Each flush also discards the bases that later reads keep returning to, and those reads inflate them and apply their deltas again. That costs most when the bases in use are just over the limit, as when reading every file at a commit of a large repository.

This keeps the entries on a doubly linked list in order of use, so the cache knows what to evict without scanning. A hit moves an entry to the front in constant time, and adding a base frees unused entries from the back only until it fits. If the entries in use leave no room, the base isn't cached; the old loop never ended in that case.

Performance

Measured on macOS (Apple silicon), comparing main with this change at the default 16 MiB.

With benchmarks/cli (git2 CLI, hyperfine, wall time):

benchmark main this change change
blame git.c in git (v2.45.0) 4.10 s ± 0.22 3.99 s ± 0.27 −2.6%
blame crates/wasmtime/src/runtime/store.rs in wasmtime 2.35 s ± 0.17 2.29 s ± 0.04 −2.5%
index-pack, wasmtime (163 MB pack) 8.22 s ± 0.25 7.93 s ± 0.52 −3.5%
index-pack, git (322 MB pack) 22.85 s ± 0.61 22.17 s ± 0.26 −3.0%
clone wasmtime over file:// 58.7 s ± 3.2 60.2 s ± 1.5 +2.6%

hyperfine runs one build's runs and then the other's, so a drifting machine shows up as a difference. To control for that, I also timed the library directly. Each round ran both builds back to back, and the table gives the median over five rounds of each round's CPU-time ratio:

workload git wasmtime google-cloud-python
read every file at HEAD runs under 0.3 s runs under 0.3 s −12.4%
4 or 8 threads reading every file at HEAD −1.6% −3.1% −16.7%
generate patches for recent history (log -p) −1.5% −0.9% −5.8%
index a pack, or read every object in pack order −3.9% −3.3%
blame one frequently changed file −2.2% −1.0% +0.5%
build a pack of all history (most of a file:// clone) +0.3%

Measured this way, building a pack shows no difference. That suggests the clone's +2.6% above is spurious.

In every workload, reads found a base in the cache more often, and 11–41% fewer bytes of bases were evicted. Peak memory is 0–7% higher, because the cache stays near its limit instead of emptying and refilling.

Built for wasm32, where rebuilding a base costs more, reading every file at HEAD of google-cloud-python takes 23% less CPU.

When a new base doesn't fit in a pack's delta base cache, libgit2 frees
every cached base that isn't in use. Before 9c62aaa it freed the
eight least recently used, but finding those meant scanning the whole
cache on every eviction, and dropping everything turned out faster:
"the most used entries are quickly back into the cache".

They come back by being rebuilt, though. Each flush also discards the
bases that later reads keep returning to, and those reads inflate them
and apply their deltas again. That costs most when the bases in use
are just over the limit, as when reading every file at a commit of a
large repository.

Keep the entries on a list, most recently used first, so the cache
knows what to evict without scanning: a hit moves an entry to the
front in constant time, and adding a base frees unused entries from
the back only until it fits. If the entries in use leave no room,
don't cache the base, where the old loop never ended. An entry goes on
the list only once it is in the map, so a failure to add it there no
longer leaves the cache counting memory it can't free. The unused
last_usage and use_ctr go away.

Reading a 60k file checkout from a 1G repo takes 12% less CPU at the
default 16 MiB, generating patches for its last 300 commits 6% less, and
indexing the packs of smaller repos 3-4% less.
@msuozzo msuozzo changed the title pack: evict delta bases least recently used first pack: use LRU eviction for delta bases Oct 12, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant