Git的打包文件是增量文件,而不是快照?


70

Git与大多数其他版本控制系统之间的主要区别之一是,其他版本控制系统倾向于将提交存储为一系列增量-一个提交与下一个提交之间的变更集。这似乎是合乎逻辑的,因为它是存储有关提交的尽可能少的信息。但是提交历史记录越长,用于比较修订范围的计算就越多。

相比之下,Git在每个修订版中存储整个项目完整快照。每次提交不会使仓库的大小显着增加的原因是项目中的每个文件都作为文件存储在Git子目录中,并以其内容的哈希命名。因此,如果内容未更改,则哈希值也未更改,并且提交仅指向同一文件。还有其他优化。

所有这些对我来说都是有意义的,直到我偶然发现了有关pack文件的信息,Git定期将数据放入其中以节省空间:

为了节省空间,Git利用了packfile。这是一种格式,其中Git仅将更改过的部分保存在第二个文件中,并带有指向该文件的指针。

这基本上不是回到存储增量吗?如果没有,那有什么不同?如何避免Git遭受其他版本控制系统所遇到的相同问题?

例如,Subversion使用增量,而回滚50个版本意味着撤消50个差异,而使用Git,您只需获取适当的快照即可。除非git也将50个差异存储在packfile中...否则是否有某种机制说“在少量增量之后,我们将存储一个新快照”,这样我们才不会堆积太大的变更集?Git还能如何避免三角洲的弊端?


6
请参阅文档中的pack-format.txtpack-heuristics.txt
Josh Lee

@jleedev:感谢您的链接!
圣保罗Ebermann

Answers:


72

简介:
Git的pack文件经过精心构造,可以有效地使用磁盘缓存,并为常用命令和读取最近引用的对象提供“不错”的访问模式。


Git的包文件格式相当灵活(见文档/技术/包,format.txt,或将打包文件Git的社区图书)。打包文件以两种主要方式存储对象:“未删除”(获取原始对象数据并进行压缩压缩)或“删除”(针对某个其他对象形成增量,然后对生成的增量数据进行压缩压缩)。包中存储的对象可以按任何顺序排列(不必(不必)按对象类型,对象名称或任何其他属性来排序),并且可以针对相同类型的任何其他合适的对象制作已删除的对象。

Git的pack-objects命令使用了几种启发式方法,可以为常见命令提供出色的参考位置。这些启发式方法既控制了已删除对象的基础对象的选择,又控制了对象的顺序。每种机制大部分都是独立的,但是它们有一些共同的目标。

Git确实形成了增量压缩对象的长链,但是试探法试图确保只有“旧”对象才在长链的末端。core.deltaBaseCacheLimit自动使用增量基本缓存(其大小由配置变量控制 ),并且可以大大减少需要读取大量对象(例如git log -p)的命令所需要的“重建”次数。

增量压缩启发式

典型的Git存储库存储大量对象,因此无法合理地比较所有对象以找到将产生最小增量表示形式的对(和链)。

增量基数选择启发式算法是基于这样的想法,即可以在文件名和大小相似的对象中找到良好的增量基数。每种类型的对象都是单独处理的(即,一种类型的对象永远不会用作另一种类型的对象的增量基础)。

为了选择增量基数,将对象(主要)按照文件名和大小排序。进入此排序列表的窗口用于限制被视为潜在增量基础的对象数量。如果一个“足够好” 1个没有找到在其窗口中的对象之间的对象增量表示,则该对象将不被增量压缩。

窗口的大小由的--window=选项 git pack-objectspack.window配置变量控制。增量链的最大深度由的--depth= 选项git pack-objectspack.depth配置变量控制。该--aggressive选项可以git gc极大地扩大窗口大小和最大深度,以尝试创建较小的打包文件。

文件名排序会将名称相同(或至少类似结尾(例如.c))的条目的对象聚在一起。大小排序从最大到最小,因此删除数据的增量要优先于添加数据的增量(因为删除增量具有较短的表示形式),因此较早,较大的对象(通常是较新的)倾向于用普通压缩表示。

1 什么才算是“足够好”取决于所讨论对象的大小及其潜在的增量基数以及其产生的增量链的深度。

对象排序启发式

对象以“最近引用”的顺序存储在打包文件中。重建最新历史记录所需的对象放在包装中的较早位置,它们将靠近在一起。这通常适用于OS磁盘缓存。

所有提交对象均按提交日期排序(最新的优先),并存储在一起。此放置和排序优化了遍历历史图并提取基本提交信息(例如git log)所需的磁盘访问。

从第一个存储的(最近的)提交开始,从树开始存储tree和blob对象。每棵树以深度优先的方式进行处理,存储所有尚未存储的对象。这会将重建最新提交所需的所有树木和斑点都放在一个位置。接下来,将按照排序的提交顺序存储尚未保存但以后提交需要的所有树和Blob。

最终对象排序受增量基本选择的影响很小,因为如果为增量表示选择了一个对象并且尚未存储其基本对象,则其基本对象将立即存储在已修改对象本身之前。这样可以防止由于读取基础对象所需的非线性访问而导致磁盘高速缓存未命中,该基础对象后来将“自然”存储在打包文件中。


8

打包文件中增量存储的使用只是实现细节。在那个级别上,Git不知道为什么或某事从一个修订版本更改到下一个修订版本,而是只知道blob B与blob A非常相似,除了这些更改C。所以它只会存储blob A并更改C (如果选择这样做-它也可以选择存储Blob A和Blob)。

从打包文件中检索对象时,增量存储不会暴露给调用方。呼叫者仍然看到完整的斑点。因此,在不优化增量存储的情况下,Git的工作方式与以往一样。


4
尽管所有其他版本控制系统都存储了增量,但所有这些都同样适用……
Jerry Coffin

12
压缩时,斑点之间的关系可能与修订历史记录建议/暗示的关系无关
肯·布鲁姆

2
我知道调用者没有看到它,但是,要检索给定的哈希值,Git可能必须取一个blob并应用一个变更集。它没有真实的快照,对不对?那么,在这一点上与SVN的区别是什么-它是否不需要堆叠大量的增量来到达历史上的特定点,因为它限制了一个斑点上堆叠了多少增量?
内森·朗

2
我不确定它与SVN有什么不同,因为我不确定SVN在内部如何工作。但是我可以指出,它与Darcs(以及Darcs补丁理论)的不同之处在于pack文件与git合并分支的方式无关。Git会重建要合并的修订版本的快照,也要重建需要合并的任何较旧版本的快照,然后找出要做什么。另一方面,Darcs存储修补程序,并在需要工作目录时将它们组合在一起,并且它可以基于不同的修补程序集创建不同的工作目录。
肯·布鲁姆

5
Git确实限制了可能需要应用多少个增量来重新生成对象(btw增量表示对树和blob一样有用),这是--depthto的参数git pack-objects。有人查看实际打包后的内容还表明,Git需要一个很小的增量,以使其值得使用,而不是压缩的blob。
araqnid

4

正如我在“ git的瘦包是什么? ”中提到的那样

Git仅在packfiles中进行删除

我在“ git二进制差异算法(增量存储)是否标准化? ”中详细介绍了用于打包文件的增量编码。
另请参阅“ git何时以及如何使用增量存储? ”。

请注意core.deltaBaseCacheLimit,对于Git 2.0.x / 2.1(2014年第三季度),控制打包文件默认大小的配置很快就会从16MB增加到96MB。

参见David Kastrup的commit 4874f54(2014年5月):

将core.deltaBaseCacheLimit碰撞到96m

默认值16m导致大型增量链和大型文件的严重抖动。

以下是一些基准测试(的pu变体git blame):

time git blame -C src/xdisp.c >/dev/null

用于git gc --aggressive在SSD驱动器上重新打包的Emacs存储库(v1.9,导致窗口大小为250)。
有问题的文件大约有30000行,大小为1Mb,历史记录约为2500次提交。

16m (previous default):
  real  3m33.936s
  user  2m15.396s
  sys   1m17.352s

96m:
  real  2m5.668s
  user  1m50.784s
  sys   0m14.288s

这在Git 2.29(2020年第四季度)中得到了进一步的优化,其中“ git index-packman学会了以更高的并行度解析带斑点的对象。

请参阅Jonathan Tan(提交commit f08cbf6(2020年9月8日),以及commit ee6f058commit b4718cacommit a7f7e84commit 46e6fb1commit fc968e2commit 009be0d(2020年8月24日(由Junio C Hamano合并--commit b7e65b5中,2020年9月22日)jhowtan
gitster

index-pack:缩小工作量

签字人:Jonathan Tan

当前,当index-pack解析增量时,它不会将增量树拆分为多个线程:每个增量基础根(不是REF_DELTAOFS_DELTA)可以进入其自己线程的对象,但是该根上的所有增量(直接或间接)都是在同一线程中处理。

当存储库包含经过多次修改的大文本文件(因此,可增量)时,这是一个问题-提取期间的增量解析时间主要由处理与该文本文件相对应的增量来决定。

该补丁包含一个解决方案。
克隆时使用

git -c core.deltabasecachelimit=1g clone \
  https://fuchsia.googlesource.com/third_party/vulkan-cts  

在我的笔记本电脑上,克隆时间从3m2s改进为2m5s(使用3个线程,这是默认设置)。

解决方案是拥有全局工作堆栈。该堆栈包含需要处理的增量基数(对象,无论是直接出现在packfile中还是由增量解析生成,它们本身都有增量子代)。每当线程需要工作时,它就会窥视堆栈的顶部并处理其下一个未处理的子级。如果一个线程发现堆栈为空,它将寻找更多的增量基根来压入堆栈。

具有全局工作堆栈的主要缺点是互斥锁上花费了更多时间,但分析显示大部分时间都花在了三角洲本身的解决上,因此在实践中这不应该成为问题。无论如何,实验(如上面的克隆命令中所述)表明,此修补程序是一项净改进。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.