bigywater.tech
Git · 22

reachability bitmap

服务端怎么在几十毫秒里算出该给 clone 的人发哪些对象?答案是 reachability bitmap。这篇生成 .bitmap,再用 Python 解码 EWAH 压缩位图:四张类型位图、164 张提交位图,并与 git rev-list --objects 逐个对账。

2.6 倍
数出 HEAD 可达的 30,000 个对象,用 bitmap 比不用快的倍数
164 / 6000
被位图化的提交数,不是每个提交都有
集合一致
抽查的 17 张提交位图,与 git rev-list --objects 数出的对象集合完全相同

实验仓库

实测 · 本机执行输出
  $ ls .git/objects/pack; git count-objects -v | grep -E "^(in-pack|packs)"; git rev-list --objects --all | wc -l
  pack-bb7adb7a19a6dc6a176a76fcf3bfee194af1f212.idx
  pack-bb7adb7a19a6dc6a176a76fcf3bfee194af1f212.pack
  pack-bb7adb7a19a6dc6a176a76fcf3bfee194af1f212.rev
  in-pack: 30000
  packs: 1
     30000

6000 个提交的线性历史,每次改一个文件,共 30,000 个对象(6000 commit、18,000 tree、6000 blob),git gc 之后是一个 pack,没有 bitmap。

问题

实测 · 本机执行输出
  $ git rev-list --objects --count HEAD
  30000
  耗时 32.8 ms

“从某个提交出发,能到达哪些对象”需要遍历 commit 和 tree,仓库大时很慢。服务端每次 fetch / clone 都要回答这个问题。不用 bitmap 时,本例数 30,000 个可达对象要 32.8 ms。

预先存下可达集合

实测 · 本机执行输出
  $ git repack -adbq; ls -l .git/objects/pack/*.bitmap | awk '{print $5, $9}' | sed "s#.git/objects/pack/##"
  149820 pack-bb7adb7a19a6dc6a176a76fcf3bfee194af1f212.bitmap
  $ git rev-list --use-bitmap-index --objects --count HEAD
  30000
  耗时 12.5 ms(没有 bitmap 时 32.8 ms,快 2.6 倍)

reachability bitmap 为一部分提交预先存下“可达对象集合”:pack 里每个对象一位,1 表示可达。之后集合运算(并、差)变成位运算,不再需要遍历。本例同样的统计降到 12.5 ms,快 2.6 倍。

文件头与选了多少个

实测 · 本机执行输出
  签名 b'BITM'  版本 1  选项 0x0005  位图化的提交数 164
  0000  42 49 54 4d 00 01 00 05 00 00 00 a4 bb 7a db 7a  BITM.........z.z
  0010  19 a6 dc 6a 17 6a 76 fc f3 bf ee 19 4a f1 f2 12  ...j.jv.....J...
  …(共 149820 字节)
  头部里记的 pack 校验和 bb7adb7a19a6dc6a176a76fcf3bfee194af1f212
  pack 文件最后 20 字节   bb7adb7a19a6dc6a176a76fcf3bfee194af1f212  一致: True
  选项位: FULL_DAG, HASH_CACHE

.bitmap 以签名 BITM、版本、选项、位图化的提交数、配对 pack 的校验和开头。6000 个提交里只给 164 个存了位图,约 2.7%;其余的提交从最近的位图出发再走几步。

类型位图与 EWAH

实测 · 本机执行输出
  commit 位图:共 6,000 位,其中 6,000 位为 1
  tree   位图:共 30,000 位,其中 18,000 位为 1
  blob   位图:共 29,640 位,其中 6,000 位为 1
  tag    位图:共 0 位,其中 0 位为 1
  四类相加 = 30,000;pack 里的对象数 = 30,000

头部后面是 commit / tree / blob / tag 四张类型位图,每个对象一位;位图用 EWAH 压缩(连续的全 0 或全 1 的 64 位字合并成一个游程)。四类 1 的个数相加正好等于 pack 里的对象数。

提交位图与两种下标

实测 · 本机执行输出
  共解出 164 张;xor_offset 非 0 的有 110 张
  entry 里的 object_pos 是提交在 .idx(按 hash 排序)里的下标;位图里的第 n 位对应 pack 里按偏移排序的第 n 个对象
  第   0 张:提交 a41517eeba… xor_offset=0  位图里 1 的个数 30,000  git rev-list --objects 数出 30,000  集合完全一致: True
  第  82 张:提交 1af1790bea… xor_offset=1  位图里 1 的个数 29,590  git rev-list --objects 数出 29,590  集合完全一致: True
  第 163 张:提交 d997d58abf… xor_offset=0  位图里 1 的个数     85  git rev-list --objects 数出     85  集合完全一致: True
  每 10 张抽查一张(共 17 张),集合不一致的: 0
  HEAD 对应第 0 张

每个被选中的提交有一张可达对象位图,相邻的提交位图可以用 XOR 差分(xor_offset)存储。entry 里的 object_pos 是提交在 .idx(hash 排序)里的下标,而位图的第 n 位对应 pack 里按偏移排序的第 n 个对象——实验里用两种顺序分别映射才得到与 git 完全一致的结果。

服务端的用途

实测 · 本机执行输出
  git pack-objects --revs --stdout(相当于整库 clone,打包 HEAD 可达的全部对象):不用 bitmap 63.3 ms,用 bitmap 21.4 ms

git 服务端为 clone 打包时需要知道“该发哪些对象”,位图让它不必遍历。本例整库打包 63.3 → 21.4 ms;仓库越大,遍历越慢,差别越明显。

分步演示

可达性位图
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / git 2.54.0(Apple Git-157);每个实验在临时目录里新建仓库,HOME 和全局配置指向实验专用的空目录(不读用户自己的 git 配置),提交的作者与时间固定以便复现;需要网络协议的实验用本机的 file:// 或 git daemon 回环端口(9800 以上),不碰任何远程仓库;计时用 time.perf_counter,数字每次运行会有小幅抖动。历史由 git fast-import 造出(6000 个提交,每次改 src/d*/f*.txt 中的一个文件);EWAH 位图由我用 Python 按字节解码(ll.py 的 ewah),对账对象是 git rev-list --objects 和 git verify-pack;耗时是 5 次(pack-objects 是 3 次)中位数,含进程启动
    • 没有验证:git 如何选择哪些提交位图化的完整规则(只记录了本例选了 164 个);多 pack / midx 位图(bitmap 配合 multi-pack-index);lookup table 选项;pseudo-merge 位图;位图对 git rev-list --count 之外命令(如 git log --graph)的影响;pack.useBitmaps 在真实 fetch 服务上的效果
    京ICP备2026067502号-1京公网安备11010502063487号