Summary
Since v2.55, a branch-changing git checkout on a large full index has become dramatically slower. Almost the entire cost is in cache_tree_fully_valid(). The slowdown is independent of how many paths change: switching between two branches that point at the same commit (zero working-tree updates, nr_unpack_entries=0) takes ~40s. The same operation on v2.54 completes in <1s.
Environment
- Microsoft Git v2.55.0.vfs.0.3 (also reproduces on v2.55.0.vfs.0.6), Windows x64
- Large repository — index has ~2.4M entries (~180 MB full index)
- Reproduced in two independent setups (see below)
Steps to reproduce
- In a repo with a very large index, create two branches at the same commit:
git branch branchA
git branch branchB
- Alternate checking them out:
git checkout branchA
git checkout branchB
Each switch moves HEAD to a different ref but changes no files (identical trees).
Expected
Sub-second — nothing is written to the working tree.
Actual
~40–44s per switch on v2.55. The same steps on v2.54 finish in <1s (verified by direct A/B on the same repository).
Evidence — GIT_TRACE2_PERF, exactly one checkout (wall-clock ~44s)
Confirmed a single git process (one cmd_name = checkout, no child git processes):
| Region |
t_rel (s) |
cache_tree … fully_valid (call 1, inside unpack_trees) |
~20 |
cache_tree … fully_valid (call 2, during index write) |
~19 |
unpack_trees … check_updates |
~0.0001 |
unpack_trees/nr_unpack_entries |
0 |
cache-tree.c:262 region_enter cache_tree ....label:fully_valid
cache-tree.c:264 region_leave 19.919597 cache_tree ....label:fully_valid (inside unpack_trees)
unpack-trees.c:2147 data unpack_trees/nr_unpack_entries:0
cache-tree.c:262 region_enter cache_tree label:fully_valid
cache-tree.c:264 region_leave 18.359747 cache_tree label:fully_valid (during index write)
In our repro cache_tree_fully_valid() is invoked twice per checkout (~20s + ~19s ≈ ~40s of the ~44s total); check_updates — the phase parallel checkout speeds up — is ~0.
Two independent reproductions
-
GVFS/VFS-for-Git enlistment (virtual filesystem, ~183 MB index): ~39–44s per switch.
-
Plain scalar clone of the same repo (no virtual filesystem, ~179 MB index): ~44s per switch, same trace shape (2× fully_valid, nr_unpack_entries=0).
Important: a default scalar clone uses a sparse index, and with the sparse index the checkout is fast — the regression does not appear. It only reproduces after disabling the sparse index and expanding to a full index:
git config index.sparse false
git read-tree HEAD # materialize the full ~2.4M-entry index
This localizes the cost to full-index cache-tree validation, and shows it is not specific to GVFS.
What we ruled out
- Parallel checkout /
core.fscache: checkout.workers=0 (parallel) vs =1 (sequential) are identical (~38–44s each). check_updates is ~0, so parallelizing working-tree writes changes nothing.
- Amount of change:
nr_unpack_entries=0 — the cost is a fixed per-checkout tax, not proportional to changed paths, and is worst for same-commit switches.
- GVFS virtual filesystem: also reproduces on a plain scalar clone (full index).
- Not present in v2.54: verified by direct A/B on the same repository (<1s).
- Sparse index avoids it: only the full index is affected.
Notes
- Also present in v2.55.0.vfs.0.6.
Summary
Since v2.55, a branch-changing
git checkouton a large full index has become dramatically slower. Almost the entire cost is incache_tree_fully_valid(). The slowdown is independent of how many paths change: switching between two branches that point at the same commit (zero working-tree updates,nr_unpack_entries=0) takes ~40s. The same operation on v2.54 completes in <1s.Environment
Steps to reproduce
HEADto a different ref but changes no files (identical trees).Expected
Sub-second — nothing is written to the working tree.
Actual
~40–44s per switch on v2.55. The same steps on v2.54 finish in <1s (verified by direct A/B on the same repository).
Evidence —
GIT_TRACE2_PERF, exactly one checkout (wall-clock ~44s)Confirmed a single git process (one
cmd_name = checkout, no child git processes):cache_tree … fully_valid(call 1, insideunpack_trees)cache_tree … fully_valid(call 2, during index write)unpack_trees … check_updatesunpack_trees/nr_unpack_entriesIn our repro
cache_tree_fully_valid()is invoked twice per checkout (~20s + ~19s ≈ ~40s of the ~44s total);check_updates— the phase parallel checkout speeds up — is ~0.Two independent reproductions
GVFS/VFS-for-Git enlistment (virtual filesystem, ~183 MB index): ~39–44s per switch.
Plain
scalar cloneof the same repo (no virtual filesystem, ~179 MB index): ~44s per switch, same trace shape (2×fully_valid,nr_unpack_entries=0).Important: a default
scalar cloneuses a sparse index, and with the sparse index the checkout is fast — the regression does not appear. It only reproduces after disabling the sparse index and expanding to a full index:This localizes the cost to full-index cache-tree validation, and shows it is not specific to GVFS.
What we ruled out
core.fscache:checkout.workers=0(parallel) vs=1(sequential) are identical (~38–44s each).check_updatesis ~0, so parallelizing working-tree writes changes nothing.nr_unpack_entries=0— the cost is a fixed per-checkout tax, not proportional to changed paths, and is worst for same-commit switches.Notes