Git 对于100,000个对象来说真的很慢。有什么补救措施吗?

我有一个“新鲜的”git-svn repo (11.13 GB) ,其中包含超过100,000个对象。

我已经预演过了

git fsck
git gc

在最初的结账后回购。

然后我试着做了一个

git status

执行 git 状态所需的时间从2m25.578 s 到2m53.901 s 不等

我通过发出命令来测试 git 状态

time git status

5次,所有的次数都在上面列出的两次之间。

我在 Mac OS X 上做这件事,本地不通过虚拟机。

不可能花这么长时间。

有办法吗? 帮忙?

谢谢。

剪辑

我的一个同事就坐在我旁边,拿着一个类似的盒子。内存更少,使用 jfs 文件系统运行 Debian。他的 Git 状态在同一个回购中运行.3(它也是一个 git-svn 签出)。

此外,我最近改变了我的文件权限(777)在这个文件夹,它带来了相当大的时间下降(为什么,我不知道)。我现在可以在3到6秒之间完成。这是可以控制的,但还是很痛苦。

69810 次浏览

你可以尝试将 --aggressive开关传递给 git gc,看看是否有帮助:

# this will take a while ...
git gc --aggressive

此外,如果历史记录中有不需要的东西(例如,旧的二进制文件) ,可以使用 git filter-branch删除旧的提交和/或文件。

您也可以尝试 git repack

也许“探照灯”想要索引文件。也许可以为你的代码目录关闭聚光灯。检查活动监视器并查看正在运行的进程。

我将使用不同的文件系统创建一个分区。与在其他文件系统上进行类似的操作相比,HFT + 对我来说总是比较缓慢。

Git 状态必须每次查看存储库中的每个文件。你可以告诉它不要再看那些你没有在研究的树

git update-index --assume-unchanged <trees to skip>

来源

页面上写着:

如果指定了这些标志,则 为路径记录的对象名称 没有更新。相反,这些 设置和取消设置“假设”选项 路径的位。当 “假设不变”开始了,饭桶 停止检查工作树文件 可能的修改,所以你 需要手动取消位告诉 当你改变工作树的时候 这有时候是有帮助的时候 在一个大项目上工作 具有非常慢的 lstat (2)的文件系统 系统调用(例如 cifs)。

此选项还可以用作 要忽略的粗文件级机制 跟踪文件中未提交的更改 (类似于. gitignore 所做的 Git 会失败 (优雅地)以防万一 在索引中修改这个文件。 在提交中合并时; 因此,在 假定未跟踪的文件是 改变上游,你将需要 手动处理这种情况。

Git 中的许多操作都取决于 文件系统要有一个高效的 Lstat (2)实现,以便 工作树的 st _ mtime 信息 文件可以很便宜地检查,看看是否 文件内容已从 索引中记录的版本 不幸的是,一些文件系统 有低效的 lstat (2)。如果您的 文件系统是其中之一,您可以设置 “假设不变”位到路径你 没有改变使基特不 做这个检查。注意设置这个 路径上的 bit 并不意味着 git 将 检查文件内容以查看 如果它已经改变ーー它使 git 成为 省略任何检查,并假设它已经 没有更改。当您对 工作树文件,你必须 明确地告诉 git 放弃“假设不变” 在你修改之前或之后 他们。

...

为了设置“假设不变” 位,使用——呈现-不改变的选项 未设置,使用——不假设——未改变。

该命令查看 core.ignrestat 配置变量 True,使用 git 更新路径 更新-索引路径... 和更新的路径 其他更新的 git 命令 索引和工作树(例如,git 应用—— index,git checkout-index-u, 和 git read-tree-u)是 自动标记为“假设” 注意“假设” 如果 git,则不设置“未更改”位 Update-index —— 工作树文件与索引匹配 (使用 git update-index —— really-refresh 如果你想把它们标记为“假设” 保持不变”)。


现在,很明显,这个解决方案只有在回购的某些部分可以方便地忽略的情况下才会有效。我在一个类似大小的项目中工作,那里绝对有大树,我不需要定期检查。Git-status 的语义使它成为一个通常为 O (n)的问题(文件数量为 n)。您需要特定于领域的优化来做得更好。

请注意,如果您使用拼接模式,也就是说,如果您通过合并(merge)而不是重建(rebase)来集成来自上游的更改,那么这个解决方案将变得不那么方便,因为从上游合并的对象变成了一个合并冲突。您可以通过重定基工作流来避免这个问题。

归根结底就是我现在能看到的几样东西。

  1. git gc --aggressive
  2. 777打开文件权限

肯定还有别的原因,但这显然是造成最大影响的原因。

不管怎样,我最近发现主分支和开发分支之间的 git status命令存在很大的差异。

长话短说,我将问题追溯到项目根目录中的一个280MB 的文件。这是一个意外签入数据库转储,所以它是罚款删除它。

这是之前和之后的:

⚡ time git status
# On branch master
nothing to commit (working directory clean)
git status  1.35s user 0.25s system 98% cpu 1.615 total


⚡ rm savedev.sql


⚡ time git status
# On branch master
# Changes not staged for commit:
#   (use "git add/rm <file>..." to update what will be committed)
#   (use "git checkout -- <file>..." to discard changes in working directory)
#
#   deleted:    savedev.sql
#
no changes added to commit (use "git add" and/or "git commit -a")
git status  0.07s user 0.08s system 98% cpu 0.157 total

我存储了105,000个对象,但似乎大文件比许多小文件更具威胁性。

一个长期的解决方案是增加 git 以在内部缓存文件系统状态。

Karsten Blees 为 msysgit 做到了这一点,它极大地提高了 Windows 的性能。在我的实验中,他的改变花费了我在 VM 中运行的 Win7机器上“ git 状态”从25秒到1-2秒的时间。

Karsten 的改变: https://github.com/msysgit/git/pull/94

缓存方法的讨论: https://groups.google.com/forum/#!topic/msysgit/fL_jykUmUNE/discussion

一般来说,我的 mac 对 git 还可以,但是如果有很多松散的对象,那么它就会变得非常慢。看起来 hfs 对于单个目录中的大量文件不是很好。

git repack -ad

接下来

git gc --prune=now

将使一个单一的包文件,并删除任何松散的对象遗留。运行这些需要一些时间。

尝试运行 Prune 命令,它将摆脱,松散的对象

Git 远程梅干起源

在 Git 2.13(2017年第二季度)中,git status应该更快,因为:

关于最后一点,请参阅 提交33fc72(2017年4月14日) by 杰夫 · 霍斯泰特勒(jeffhostetler)
(由 朱尼奥 · C · 哈马诺 gitster于2017年4月24日在 提交 cdfe138合并)

read-cache: force_verify_index_checksum

教 git 在结尾跳过 SHA1-1校验和的验证 除非设置了“ force_verify_index_checksum”全局变量,否则从 read_index()调用 verify_hdr()中的索引文件。

fsck强制执行此验证。

校验和验证用于检测磁盘损坏,对于小型项目,计算 SHA-1所花费的时间并不重要,但对于巨大的存储库,这种计算会为每个命令增加很多时间。


Git 2.14通过更好地考虑“ 未跟踪缓存 ”,再次提高了 Git 的状态性能。“ 未跟踪缓存 ”允许 Git 使用 stat结构的 mtime字段,在 stat数据没有更改的情况下跳过读取未跟踪的目录。

有关未跟踪缓存的更多信息,请参见 Documentation/technical/index-format.txt

提交 edf3b90(2017年5月8日) by 大卫 · 特纳(dturner-tw)
(由 朱尼奥 · C · 哈马诺 gitster于2017年5月30日在 提交 fa0624f合并)

当“ git checkout”、“ git merge”等操作内核索引时,索引扩展中的各种信息会从原始状态中丢弃,因为它们通常不会保持最新并与主索引上的操作同步。

未跟踪的缓存扩展现在被复制到这些操作中,这将加速“ git status”(只要缓存正确地失效)。


更一般地说,使用 Git 2.14. x/2.15写入缓存也会更快

承认吧提交 b50386c提交3921a0b(2017年8月21日) by Kevin Willford (“)
(由 朱尼奥 · C · 哈马诺 gitster于2017年8月27日在 提交030faf2合并)

我们过去常常花费超过必要的周期来分配和释放 在写入每个索引条目时输出一段内存。
这已经被优化过了。

当该指数有超过100万条条目,且小型回购协议没有性能下降时,这将节省3% 至7% 的成本。


2017年12月更新: Git 2.16(2018年第一季度)将提出一个额外的增强,这次是针对 git log,因为在松散对象文件上迭代的代码刚刚得到了优化。

犯罪(2017年12月4日) by 德里克 · 斯托利(derrickstolee)
(由 朱尼奥 · C · 哈马诺 gitster于2017年12月13日在 犯罪心理,第九季,第15集合并)

使用 strbuf_add()代替 strbuf_addf()

枚举时将使用 strbuf_addf()替换为使用 strbuf_add()for_each_file_in_obj_subdir()中的松动物体。因为我们已经 在使用前检查字符串的长度和十六进制值 路径,我们可以防止额外的计算使用较低的- 水平测量法水平测量法。

for_each_file_in_obj_subdir()的一个消费者是缩写 密码。OID (对象标识符)缩写使用缓存的松散对象列表(每个对象子目录)来快速进行重复查询,但是有 当有许多松散对象时,显著的缓存加载时间。

大多数存储库在重新打包之前没有很多松散对象,但是在 < a href = “ https://github.com/Microsoft/GVFS”rel = “ nofollow noReferrer”> GVFS 的情况下(参见“ 发布 GVFS (Git 虚拟文件系统)”) ,回购可以增长到有数百万个松散对象。
在启用了 GVFS 的回购协议中,使用约250万个松散对象分析 Windows 的 Git中的“ git log”性能,结果显示12% 的 CPU 时间花在了 strbuf_addf()中。

p4211-line-log.sh添加新的性能测试 对这个缓存加载敏感。
通过限制1000次提交,我们更接近于在页导航中读取历史记录时的用户等待时间。

对于带有两个 ~ 512MB 包文件和 ~ 572K 松散对象的 Linux repo 副本,运行‘ git log --oneline --parents --raw -1000’具有以下性能:

HEAD~1            HEAD
----------------------------------------
7.70(7.15+0.54)   7.44(7.09+0.29) -3.4%

2018年3月更新: Git 2.17将进一步改进 git status: 见 这个答案


更新: Git 2.20(2018年第四季度)添加了 索引进入偏移量表(IEOT),这允许 git status更快地加载索引。

承认77ff112提交3255089犯下罪行提交 c780b9c第三季,第9集犯了3710d(2018年10月10日) by 本 · 皮尔特(benpeart)
252d079(2018年9月26日) by 易懂的泰语(pclouds)
(由 朱尼奥 · C · 哈马诺 gitster于2018年10月19日在 犯了 e-27bfaa合并)

Read-cache: 工作线程上的加载缓存条目

这个补丁通过使用 索引进入偏移量表(IEOT)的分装和转换 并行地跨多个线程的缓存条目。

我使用 p0002-read-cache.sh生成了一些性能数据:

Test w/100,000 files reduced the time by 32.24%
Test w/1,000,000 files reduced the time by -4.77%

请注意,在1,000,000个文件的情况下,对缓存条目进行多线程解析 不会产生性能胜利。这是因为解析 这个回购中的索引扩展,远远超过加载缓存的成本 参赛作品。

考虑到:

config: 添加新的 index.threads配置设置

添加对新的 index.threads配置设置的支持,该设置将用于 控制 do_read_index()中的线程代码。

  • 值0将告诉索引代码自动确定要使用的正确线程数。
    值1将使代码成为单线程的。
  • 大于1的值将设置要使用的最大线程数。

为了进行测试,可以通过设置 ABc0的环境变量大于0。


Git 2.21(Q12019)引入了一个新的改进,更新了用于优化存在查找的 松散对象缓存,该 松散对象缓存已经更新。

提交8be88db(2019年1月7日) ,以及 承认吧犯罪,第四季,第19集承诺0000d65(2019年1月6日)。
(由 朱尼奥 · C · 哈马诺 gitster于2019年1月18日在 犯下 eb8638a合并)

object-store: 对于松散缓存,每个子目录使用一个 oid_array

松散对象缓存根据需要一次填充一个子目录。
它存储在一个 oid_array中,在每次添加操作之后必须重新使用该 oid_array
因此,当查询范围很广的对象时,部分填充的数组最多需要重新使用255次,这比排序一次所需的时间长100多倍。

对每个子目录使用一个 oid_array
这样可以确保只对条目进行一次排序。
它还为每个缓存查找避免了八个二进制搜索步骤,这是一个小小的额外好处。

缓存用于对日志占位符 %h%t%p进行冲突检查,我们可以在一个存储库中看到更改,每个子目录大约有100个对象:

$ git count-objects
26733 objects, 68808 kilobytes


Test                        HEAD^             HEAD
--------------------------------------------------------------------
4205.1: log with %H         0.51(0.47+0.04)   0.51(0.49+0.02) +0.0%
4205.2: log with %h         0.84(0.82+0.02)   0.60(0.57+0.03) -28.6%
4205.3: log with %T         0.53(0.49+0.04)   0.52(0.48+0.03) -1.9%
4205.4: log with %t         0.84(0.80+0.04)   0.60(0.59+0.01) -28.6%
4205.5: log with %P         0.52(0.48+0.03)   0.51(0.50+0.01) -1.9%
4205.6: log with %p         0.85(0.78+0.06)   0.61(0.56+0.05) -28.2%
4205.7: log with %h-%h-%h   0.96(0.92+0.03)   0.69(0.64+0.04) -28.1%

对于 Git 2.26(2020年第一季度) ,对象可达性位图机制和部分克隆机制并没有准备好很好地协同工作,因为部分克隆使用的一些对象过滤标准本质上依赖于对象遍历,但位图机制是一种绕过对象遍历的优化。

然而,有些情况下,他们可以一起工作,他们被教导。

承诺20a5fd8(2020年2月18日) by 滨野俊男(gitster)
参见 提交3ab3185提交84243da提交4f3bd56提交 cc4aa28承诺2aaeb9a提交6663ae0犯罪承认吧犯下608d9c9提交3ab31850,提交3ab31851,提交3ab31852(2020年2月14日)和 提交3ab31853,提交3ab31854,提交3ab31855(2020年2月13日)。
(由 朱尼奥 · C · 哈马诺 gitster提交0df82d9合并,2020年3月2日)

pack-bitmap : 实现 BLOB_NONE过滤

签名: Jeff King

我们可以很容易地 用位图支持 BLOB_NONE过滤器
因为我们知道所有对象的类型,所以我们只需要清除任何 blobs 的结果位。

请注意实现中的两个微妙之处(我在注释中也提到了) :

  • 我们必须包括任何特定要求的 blobs (并且不是通过图遍历达到的) ,以匹配非位图版本
  • 我们必须分别处理 in-pack 和“ ext _ index”对象。
    可以说,ready _ bitmap _ walk ()可以将这些 ext_index对象添加到类型位图中。
    但是现在没有,所以让我们匹配这里剩下的位图代码(这样做可能不会提高效率,因为扩展这些位图的成本与我们这里的循环大致相同,但它可能会使代码更简单一些)。

以下是 Git Git新测试的完美结果:

Test                                    HEAD^             HEAD
--------------------------------------------------------------------------------
5310.9: rev-list count with blob:none   1.67(1.62+0.05)   0.22(0.21+0.02) -86.8%

要了解更多关于 oid_array的信息,请考虑 Git 2.27(Q22020)

犯下0740d0a提交 c79eddf提交7383b25提交 ed4b804承认吧犯下 ECCCE52做600个蜂4(2020年3月30日) by 杰夫 · 金(peff)
(由 朱尼奥 · C · 哈马诺 gitster提交768f86合并,2020年4月22日)

oid_array : 使用 size_t进行计数和分配

签名: Jeff King

oid_array对象使用“ int”来存储项的数量和分配的大小。

存储库中不太可能有超过2 ^ 31个对象(仅 sha1就有40GB!),但如果他们这样做,我们将溢出我们的 alloc 变量。

你可以重现这个例子,比如:

git init repo
cd repo


# make a pack with 2^24 objects
perl -e '
my $nr = 2**24;


for (my $i = 0; $i < $nr; $i++) {
print "blob\n";
print "data 4\n";
print pack("N", $i);
}
| git fast-import


# now make 256 copies of it; most of these objects will be duplicates,
# but oid_array doesn't de-dup until all values are read and it can
# sort the result.
cd .git/objects/pack/
pack=$(echo *.pack)
idx=$(echo *.idx)
for i in $(seq 0 255); do
# no need to waste disk space
ln "$pack" "pack-extra-$i.pack"
ln "$idx" "pack-extra-$i.idx"
done


# and now force an oid_array to store all of it
git cat-file --batch-all-objects --batch-check

结果是:

fatal: size_t overflow: 32 * 18446744071562067968

所以好消息是 st_mult()发现了问题(大数字是因为 int 包装了负值,然后它被强制转换为 size_t) ,做了它应该做的工作: 在疯狂的情况下跳出,而不是造成缓冲区不足。

但是,我们应该避免打这种情况下,而是限制自己的基础上,malloc()愿意给我们。
我们可以很容易地做到这一点,切换到 size_t

上面的 cat-file进程在整数溢出之前使其达到约120GB 的虚拟集大小(我们的内部哈希存储现在是32字节,准备用于 sha256,所以我们预计总共需要约128GB 的大小,加上从一个 realloc 块复制到另一个块的可能性更大)。
在这个补丁之后(以及大约130GB 的 RAM + 交换机) ,它最终会读取整个集合。因为显而易见的原因没有检查。

注意,这个对象是在 sha1-array.c中定义的,它已经被重命名为 oid-array.c: 考虑到 Git 最终将从 SHA1过渡到 SHA2,这是一个更为中立的名称。


另一个优化:

使用 Git 2.31(Q12021) ,索引中缓存树扩展的代码得到了优化。

参见 犯下第4b6d20条提交4bdde33犯下22ad860犯下845d15d(2021年1月7日)和 第五季,第950集犯罪现场调查,第三季,第187集提交 fa7ca5d提交 c338898犯罪(2021年1月4日)。
提交0b72536(2021年1月7日) by 勒内 · 沙夫(rscharfe)
(由 朱尼奥 · C · 哈马诺 gitster于2021年2月5日在 承认犯罪合并)

cache-tree : 加速连续路径比较

签字人: Derrick Stolee

前面的更改减少了在 strlen()中花费的时间,同时比较了 verify_cache()中的连续路径,但是我们可以做得更好。
该条件检查目录分隔符是否存在于正确的位置,但仅在进行字符串比较之后。
将顺序交换为逻辑上等效但执行较少字符串比较的顺序。

为了测试对性能的影响,我使用了索引中包含超过300万条路径的存储库。
然后在 repeat 上运行以下命令:

git -c index.threads=1 commit --amend --allow-empty --no-edit

以下是5次热身后10次跑步的测量结果:

Benchmark #1: v2.30.0
Time (mean ± σ):     854.5 ms ±  18.2 ms
Range (min … max):   825.0 ms … 892.8 ms


Benchmark #2: Previous change
Time (mean ± σ):     833.2 ms ±  10.3 ms
Range (min … max):   815.8 ms … 849.7 ms


Benchmark #3: This change
Time (mean ± σ):     815.5 ms ±  18.1 ms
Range (min … max):   795.4 ms … 849.5 ms

这个更改比前一个更改快2% ,比 v2.30.0快5% 。