bigywater.tech
Git · 35

index 的 stat 缓存:git status 为什么快

git status 在大仓库里为什么能这么快?答案是 index 里的 stat 缓存:只在文件的 stat 变了的时候才去读内容。这篇解析 index 条目、验证 touch 与原地改写时 status 的判断,再测 10,000 个文件时缓存有效与失效的差别,以及 cp -a 之后为什么第一次 status 特别慢。

29 vs 204 ms
10,000 个文件的仓库,stat 缓存有效与全部失效时 git status 的耗时
8 个字段
index 条目里缓存的 stat 信息:ctime、mtime、dev、ino、mode、uid、gid、size
734 ms
cp -a 把仓库搬走后第一次 git status 的耗时(inode 与 ctime 全变),第二次只要 28 ms

index 里的 stat

实测 · 本机执行输出
  index 版本 2,3 个条目,扩展块: [('TREE', 82)]
  路径               size            mtime(秒.纳秒)  ino      mode    与 stat 一致?
  a.txt               6 1791728834.904695532  44030464 100644  True
  src/b.txt           7 1791728834.904872532  44030466 100644  True
  src/deep/c.txt      2 1791728834.904997991  44030468 100644  True

每个 index 条目除了路径、blob hash、flags,还存着该文件当时的 stat:ctime、mtime(秒与纳秒)、dev、ino、权限、uid、gid、大小。解析 .git/index 后逐字段与 os.stat 对比完全一致。

怎么判断没改

实测 · 本机执行输出
  只 touch(内容不变,mtime 变了):status 无输出,但要重新读文件算 hash,再把新的 stat 写回 index
  $ git status --short; echo "(status 为空:内容没变)"
  (status 为空:内容没变)
  a.txt 在 index 里的 mtime: 1791728834.904695532 → 1791728836.059909000(git status 顺手刷新了 index)

git status 对每个文件先比较 stat:全部一致就认定没改,不读内容;只要有字段不同,才去读内容算 hash。如果内容其实没变(比如只是 touch),git 会把新的 stat 写回 index,下次又能走快速路径。

盲点

实测 · 本机执行输出
  $ cat a.txt; git status --short; echo "(默认配置:原地改写会更新 ctime,git 发现 stat 不同,重新比较内容,报告 M)"
  HELLO
   M a.txt
  (默认配置:原地改写会更新 ctime,git 发现 stat 不同,重新比较内容,报告 M)
  $ git config core.trustctime false; git config core.checkStat minimal; cat a.txt; git status --short; echo "(trustctime=false + checkStat=minimal:只比较 mtime 和 size,没有任何输出,看不出改动)"; git diff --stat | wc -l
  HELLO
  (trustctime=false + checkStat=minimal:只比较 mtime 和 size,没有任何输出,看不出改动)
         0
  $ echo "工作区文件的 blob hash : $(git hash-object a.txt | cut -c1-12)"; echo "index 里记的 blob hash : $(git rev-parse :a.txt | cut -c1-12)"; echo "(两者不同:内容确实变了,只是 git status 的 stat 比较没发现)"
  工作区文件的 blob hash : e427984d4a2c
  index 里记的 blob hash : ce013625030b
  (两者不同:内容确实变了,只是 git status 的 stat 比较没发现)
  $ git config --unset core.trustctime; git config --unset core.checkStat; git status --short; echo "(恢复默认比较后,ctime 暴露了改动)"
   M a.txt
  (恢复默认比较后,ctime 暴露了改动)

stat 比较是启发式的:内容改了但大小不变、mtime 又被恢复,只要 ctime 也不比较(core.trustctime=false 加 core.checkStat=minimal),git 就看不出来。默认配置下原地写文件会更新 ctime,把这个改动暴露出来。

值多少钱

实测 · 本机执行输出
  stat 缓存有效:git status 29 ms
  把 10,000 个文件的 mtime 全部改掉(内容不变)后的第一次 git status(要重读全部文件算 hash)204 ms
  等过 1 秒、再跑一次让 index 刷新之后的 git status(原因见下一篇 racy git:刚被改过的文件会被认为“可能 racy”)29 ms

10,000 个小文件,缓存有效 29 ms;缓存全失效要重读所有文件,204 ms,大约 7 倍。文件越多、越大,差距越大。

搬仓库

实测 · 本机执行输出
  cp -a 之后第一次 git status 734 ms,第二次 28 ms
  同样 cp -a,设置 core.checkStat=minimal(只看 mtime 和 size)和 core.trustctime=false 之后第一次 git status 29 ms

cp -a 保留 mtime 但 inode 与 ctime 变了,index 里的缓存整体失效,第一次 git status 慢(734 ms),第二次恢复。把 stat 检查放宽到 mtime 和 size 可以避开,但放弃了 ctime 带来的保护。

分步演示

index 的 stat 缓存
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / git 2.54.0(Apple Git-157);每个实验在临时目录里新建仓库,HOME 和全局配置指向实验专用的空目录(不读用户自己的 git 配置),提交的作者与时间固定以便复现;需要网络协议的实验用本机的 file:// 或 git daemon 回环端口(9800 以上),不碰任何远程仓库;计时用 time.perf_counter,数字每次运行会有小幅抖动。文件系统是 macOS APFS(纳秒级时间戳);index 解析器是我写的 Python(ll.py 的 parse_index,只处理 v2/v3);耗时 3 次中位数,含进程启动;第四节在每次计时前等过 1 秒以避开 racy 窗口(原因见下一篇)
    • 没有验证:index v4(路径前缀压缩)、split index、sparse index 的 stat 保存;Linux / Windows 上 stat 字段的差异(dev、ino 的稳定性);core.checkStat 的 default 与 minimal 之间每个字段的具体取舍;fsmonitor 与 untracked cache(见后面一篇)
    京ICP备2026067502号-1京公网安备11010502063487号