解决此问题的另一种方法是并行结帐(从 Git 2.32 开始,2021 年第二季度)。
如this patch (still in progress) 中所述:
本系列将并行工作器添加到结帐机器中。
缓存条目分布在帮助进程中,这些进程负责
读取、过滤 blob 并将其写入工作树。
这应该有利于所有调用unpack_trees() 或check_updates() 的命令,
如:checkout、clone、sparse-checkout、checkout-index等
Local:
Clone Checkout I Checkout II
Sequential 8.180 s ± 0.021 s 6.936 s ± 0.030 s 2.585 s ± 0.005 s
10 workers 3.406 s ± 0.187 s 2.164 s ± 0.033 s 1.050 s ± 0.021 s
Speedup 2.40 ± 0.13 3.21 ± 0.05 2.46 ± 0.05
例如,对于 Git 2.32(2021 年第 2 季度),有用于并行结帐的准备性 API 更改。
请参阅commit ae22751、commit 30419e7、commit 584a0d1、commit 49cfd90、commit d052cc0(2021 年 3 月 23 日)Matheus Tavares (matheustavares)。
请参阅Jeff Hostetler (Jeff-Hostetler) 的commit f59d15b、commit 3e9e82c、commit 55b4ad0、commit 38e9584(2020 年 12 月 16 日)。
(由 Junio C Hamano -- gitster -- 合并到 commit c47679d,2021 年 4 月 2 日)
convert:添加[async_]convert_to_working_tree_ca() 变体
签字人:Jeff Hostetler
签字人:Matheus Tavares
通过添加转换函数的_ca() 变体将属性收集与实际转换分开。
这些变体接收预先计算的“struct conv_attrs”,因此不依赖于索引状态。
它们将在未来的补丁中使用,添加并行结账支持,原因有两个:
- 在转换之前,我们将在
checkout_entry() 中加载转换属性,以确定路径是否符合并行结帐条件。
因此,为了实际转换,稍后再次加载它们会很浪费。
- 并行工作人员将负责读取、转换 Blob 并将其写入工作树。
他们无法访问主进程的索引状态,因此无法加载属性。
相反,它们将接收预加载的并调用转换函数的 _ca() 变体。
此外,属性机制经过优化,可以按顺序处理路径,因此最好将其留给主进程。
还有:
在 Git 2.32(2021 年第 2 季度)中,结帐机器被教导在可能的情况下并行执行文件的实际写出。
参见commit 68e66f2(2021 年 4 月 19 日)和commit 1c4d6f4、commit 7531e4b、commit e9e8adf、commit 04155bd(2021 年 4 月 18 日)Matheus Tavares (matheustavares)。
(由@987654342 合并@in commit a1cac26,2021 年 4 月 30 日)
合着:Jeff Hostetler
署名:Matheus Tavares
通过引入两个新设置使并行结帐可配置:>- checkout.workers 和
-
checkout.thresholdForParallelism.
第一个定义工作人员的数量(其中一个表示顺序结账),第二个定义尝试并行结账的最小条目数。
为了确定 checkout.workers 的默认值,并行版本在 linux repo 中的三个操作中使用冷缓存进行了基准测试:克隆 v5.8、从 v2.6.15 签出 v5.8(签出 I)和检查从 v5.7 中取出 v5.8(结帐 II)。
下面的四个表格显示了 5 次运行的平均运行时间和标准偏差:SSD 上的本地文件系统、HDD 上的本地文件系统、Linux NFS 服务器和 Amazon EFS(都在 Linux 上)。
每个并行结帐测试都是使用在该环境中带来最佳整体结果的工作人员数量执行的。
本地 SSD:
Sequential 10 workers Speedup
Clone 8.805 s ± 0.043 s 3.564 s ± 0.041 s 2.47 ± 0.03
Checkout I 9.678 s ± 0.057 s 4.486 s ± 0.050 s 2.16 ± 0.03
Checkout II 5.034 s ± 0.072 s 3.021 s ± 0.038 s 1.67 ± 0.03
本地硬盘:
Sequential 10 workers Speedup
Clone 32.288 s ± 0.580 s 30.724 s ± 0.522 s 1.05 ± 0.03
Checkout I 54.172 s ± 7.119 s 54.429 s ± 6.738 s 1.00 ± 0.18
Checkout II 40.465 s ± 2.402 s 38.682 s ± 1.365 s 1.05 ± 0.07
Linux NFS 服务器(v4.1,在 EBS 上,单一可用区):
Sequential 32 workers Speedup
Clone 240.368 s ± 6.347 s 57.349 s ± 0.870 s 4.19 ± 0.13
Checkout I 242.862 s ± 2.215 s 58.700 s ± 0.904 s 4.14 ± 0.07
Checkout II 65.751 s ± 1.577 s 23.820 s ± 0.407 s 2.76 ± 0.08
EFS(v4.1,在多个可用区复制):
Sequential 32 workers Speedup
Clone 922.321 s ± 2.274 s 210.453 s ± 3.412 s 4.38 ± 0.07
Checkout I 1011.300 s ± 7.346 s 297.828 s ± 0.964 s 3.40 ± 0.03
Checkout II 294.104 s ± 1.836 s 126.017 s ± 1.190 s 2.33 ± 0.03
上述基准测试表明,并行签出对于位于 SSD 或分布式文件系统上的存储库最为有效。
对于旋转磁盘和/或旧机器上的本地文件系统,并行性并不总能带来良好的性能。
出于这个原因,checkout.workers 的默认值是一,也就是
顺序结帐。
为了确定 checkout.thresholdForParallelism 的默认值,在“本地 SSD”设置中执行了另一个基准测试,其中并行结账显示是有益的。
这一次,我们比较了 git checkout -f(man) 在从 Linux 工作树中随机删除越来越多的文件后,有和没有并行性的运行时间。
下面的“顺序回退”列对应于 checkout.workers 为 10 但checkout.thresholdForParallelism 等于要更新的文件数加一的执行(因此我们最终按顺序写入)。
每个测试用例抽样 15 次,每个样本随机删除一组不同的文件。
结果如下:
sequential fallback 10 workers speedup
10 files 772.3 ms ± 12.6 ms 769.0 ms ± 13.6 ms 1.00 ± 0.02
20 files 780.5 ms ± 15.8 ms 775.2 ms ± 9.2 ms 1.01 ± 0.02
50 files 806.2 ms ± 13.8 ms 767.4 ms ± 8.5 ms 1.05 ± 0.02
100 files 833.7 ms ± 21.4 ms 750.5 ms ± 16.8 ms 1.11 ± 0.04
200 files 897.6 ms ± 30.9 ms 730.5 ms ± 14.7 ms 1.23 ± 0.05
500 files 1035.4 ms ± 48.0 ms 677.1 ms ± 22.3 ms 1.53 ± 0.09
1000 files 1244.6 ms ± 35.6 ms 654.0 ms ± 38.3 ms 1.90 ± 0.12
2000 files 1488.8 ms ± 53.4 ms 658.8 ms ± 23.8 ms 2.26 ± 0.12
从以上数字来看,100 个文件似乎是阈值设置的合理默认值。
注意:最多 1000 个文件,我们观察到并行代码的执行时间随着文件数量的增加而下降。
这是一个相当奇怪的行为,但在多次重复中观察到。
超过 1000 个文件,执行时间会随着文件数量的增加而增加,正如预期的那样。
关于测试环境:本地 SSD 测试是在运行 Manjaro Linux 的 i7-7700HQ(4 核超线程)上执行的。
本地硬盘测试在 Intel(R) Xeon(R) E3-1230(也是具有超线程的 4 个内核)、硬盘 Seagate Barracuda 7200.14 SATA 3.1、运行 Debian 上执行。
NFS 和 EFS 测试在具有 4 个 vCPU 的 Amazon EC2 c5n.xlarge 实例上执行。
Linux NFS 服务器在具有 2 个 vCPUS 和 1 TB EBS GP2 卷的 m6g.large 实例上运行。
在每次计时之前,linux 存储库都被删除(或检出到之前的状态),并执行了sync && sysctl vm.drop_caches=3。
git config 现在包含在其man page 中:
checkout.workers
更新工作树时使用的并行工作器数量。
默认值为 1,即顺序执行。如果设置为小于
大于 1,Git 将使用与逻辑核心数量一样多的工作人员
可用的。此设置和checkout.thresholdForParallelism 影响
所有执行结帐的命令。例如。结帐,克隆,重置,
稀疏结帐等。
注意:并行签出通常为存储库提供更好的性能
位于 SSD 或 NFS 上。对于旋转磁盘和/或机器上的存储库
内核数量较少时,通常会执行默认的顺序签出
更好的。存储库的大小和压缩级别也可能影响如何
并行版本执行良好。
checkout.thresholdForParallelism
当使用少量文件运行并行检出时,成本
子进程产生和进程间通信的重要性可能超过
并行化收益。
此设置允许定义最小值
应尝试并行检出的文件数。
默认为 100。
而且,仍然使用 Git 2.32(2021 年第二季度),“并行结账”的最后一部分:
请参阅commit 87094fc、commit d590422、commit 2fa3cba、commit 6a7bc9d、commit d0e5d35、commit 70b052b、commit 6053950、commit 9616882(2021 年 5 月 4 日)Matheus Tavares (matheustavares)。
(由Junio C Hamano -- gitster -- 合并于commit a737e1f,2021 年 5 月 16 日)
签字人:Matheus Tavares
允许 checkout-index 使用并行结账框架,遵守 checkout.workers 配置。
checkout-index 中有两个代码路径调用checkout_entry(),因此可以使用并行结账:
-
checkout_file(),用于写入在命令行中显式给出的路径;和
-
checkout_all(),当给定--all选项时,用于写入索引中的所有路径。
在这两种操作模式下,checkout-index 不会在 checkout_entry() 失败时立即中止。
相反,它会在使用非零退出代码退出之前尝试检查所有剩余路径。
为了在使用并行结账时保持这种行为,我们必须允许 run_parallel_checkout() 在退出之前尝试写入排队的条目,即使我们已经从之前的 checkout_entry() 调用中得到错误代码。
但是,checkout_all() 不会在错误时返回,它使用代码 128 调用 exit()。我们可以让它在退出之前调用 run_parallel_checkout(),但是如果我们统一退出路径cmd_checkout_index() 的两种结帐索引模式,并让此函数负责与并行结帐 API 的交互。
所以让我们这样做吧。
有了这个改变,我们还必须考虑是否要继续使用 128 作为 git checkout-index --all(man) 的错误代码,而我们使用 1 表示 git checkout-index(man)<path>(即使实际错误相同)。
由于代码 128 仅用于 --all 并没有太大价值,并且文档中也没有提及它(因此更改它不太可能会破坏任何现有脚本),让我们让两种模式在 @ 上使用代码 1 退出987654421@ 错误。
在 Git 2.33(2021 年第三季度)之前,并行结帐代码路径未初始化用于以面向未来的方式与工作进程对话的对象 ID 字段。
参见Matheus Tavares (matheustavares) 的commit 3d20ed2(2021 年 5 月 17 日)。
(由 Junio C Hamano -- gitster -- 合并于 commit bb6a63a,2021 年 6 月 10 日)
签字人:Matheus Tavares
存储 SHA-1 名称的 object_id 在哈希数组的末尾有一些未使用的字节。
由于不使用这些字节,它们通常也不初始化为任何值。
但是,在parallel_checkout.c:send_one_item() 处,缓存条目的object_id 被复制到缓冲区中,该缓冲区稍后通过管道write() 发送给结帐工作人员。
这让 Valgrind 抱怨将未初始化的字节传递给系统调用。
但是,由于cf09832 (hash: 添加一个算法成员到 struct object_id, 2021-04-26, Git v2.32.0-rc0 -- merge 列在batch #15) ("hash: add结构 object_id", 2021-04-26) 的算法成员,在这里使用 hashcpy() 已经不够用了,因为它不会从 object_id 复制新的算法字段。
让我们添加并使用一个新函数,该函数满足复制所有重要object_id 数据的要求,同时仍然避免未初始化的字节,方法是在目标object_id 中填充散列数组的末尾。
通过此更改,我们也不再需要将来自send_one_item() 的目标缓冲区初始化为零,因此让我们从xcalloc() 切换到xmalloc() 以明确这一点。