Git 的包文件是增量而不是快照?
Posted
技术标签:
【中文标题】Git 的包文件是增量而不是快照?【英文标题】:Are Git's pack files deltas rather than snapshots? 【发布时间】:2011-07-07 18:25:55 【问题描述】:Git 与大多数其他版本控制系统之间的一个主要区别在于,其他系统倾向于将提交存储为一系列增量 - 一个提交和下一个提交之间的变更集。这似乎是合乎逻辑的,因为它是存储有关提交的尽可能少的信息量。但提交历史越长,比较修订范围所需的计算就越多。
相比之下,Git 会在每个修订版中存储整个项目的完整快照。这不会使 repo 大小随着每次提交而急剧增长的原因是项目中的每个文件都作为文件存储在 Git 子目录中,以其内容的哈希命名。所以如果内容没有改变,hash 没有改变,提交只是指向同一个文件。还有其他优化。
在我偶然发现this information about pack files 之前,这一切对我来说都是有意义的,Git 会定期将数据放入其中以节省空间:
为了节省空间,Git 利用包文件。这是一个 Git 只会保存的格式 第二部分发生了变化 文件,带有指向该文件的指针 类似。
这基本上不是回到存储增量吗?如果不是,它有什么不同?这如何避免 Git 遇到与其他版本控制系统相同的问题?
例如,Subversion 使用增量,回滚 50 个版本意味着撤消 50 个差异,而使用 Git,您只需获取适当的快照即可。除非 git 还在包文件中存储 50 个差异......是否有某种机制说“在少量增量之后,我们将存储一个全新的快照”,这样我们就不会堆积太大的变更集? Git 还能如何避免增量的缺点?
【问题讨论】:
参见文档中的pack-format.txt 和pack-heuristics.txt。 @jleedev:感谢您的链接! 【参考方案1】:在包文件中使用增量存储只是一个实现细节。在那个级别,Git 不知道为什么或如何从一个修订版更改为下一个修订版,而它只知道 blob B 与 blob A 非常相似,除了这些更改 C。所以它只会存储 blob A 和更改 C (如果它选择这样做 - 它也可以选择存储 blob A 和 blob B)。
从包文件中检索对象时,增量存储不会暴露给调用者。调用者仍然看到完整的 blob。因此,在没有 delta 存储优化的情况下,Git 的工作方式与以往相同。
【讨论】:
所有这一切都同样适用于大多数其他版本控制系统存储的增量...... 压缩时 blob 之间的关系可能与修订历史建议/暗示的关系无关。 我知道这不会暴露给调用者,但是,为了检索给定的哈希,Git 可能需要获取一个 blob 并应用一个变更集;它没有真正的快照,对吗?那么,在这一点上,它与 SVN 的区别是什么——它永远不需要堆叠大量的 delta 来到达历史上的特定点,因为它限制了一个 blob 上堆叠的 delta 的数量? 我不确定它与 SVN 有什么不同,因为我不确定 SVN 内部是如何工作的。但我可以指出,它与 Darcs(以及补丁的 Darcs 理论)的不同之处在于,pack 文件与 git 如何合并分支无关。 Git 重建它正在合并的修订的快照,它还重建它需要合并的任何旧修订的快照,然后确定要做什么。而 Darcs 则存储补丁,并在您需要工作目录时将它们组合起来,它可以根据不同的补丁集创建不同的工作目录。 Git 确实 限制了可能需要应用多少增量来重新生成一个对象(顺便说一句,增量表示对于树和 blob 一样有用),即--depth
git pack-objects
的参数。一些查看实际打包内容的人还表明,Git 需要一个非常小的增量,以便值得使用它而不是压缩的 blob。【参考方案2】:
总结: Git 的包文件经过精心构造,可以有效地使用磁盘缓存和 为常用命令和最近引用的阅读提供“不错”的访问模式 对象。
Git 的打包文件 格式相当灵活(见Documentation/technical/pack-format.txt, 或The Packfile 在The Git Community Book)。 包文件将对象存储在两个主要 方式:“未删除”(获取原始对象数据并放气压缩 它)或“deltified”(然后与其他对象形成增量 放气压缩生成的增量数据)。存储的对象 一个包可以按任何顺序排列(它们(不一定)必须是 按对象类型、对象名称或任何其他属性排序)和 可以针对任何其他合适的相同类型的对象制作 deltified 对象。
Git 的pack-objects 命令使用了几个heuristics 来 为共同提供优秀的locality of reference 命令。这些启发式方法控制了基础的选择 对象的分类对象和对象的顺序。每个 机制大多是独立的,但它们有一些共同的目标。
Git 确实形成了 delta 压缩对象的长链,但是
启发式方法试图确保只有“旧”对象位于
长链。增量基本缓存(其大小由
core.deltaBaseCacheLimit
配置变量)是自动
可以大大减少所需的“重建”次数
需要读取大量对象的命令(例如git log
-p
)。
Delta 压缩启发式
一个典型的 Git 存储库存储了大量的对象,所以 它无法合理地比较它们以找到对(和 链)将产生最小的增量表示。
增量基选择启发式基于以下想法: 将在具有相似文件名的对象中找到良好的增量基础 和尺寸。每种类型的对象都是单独处理的(即 一种类型的对象永远不会用作 另一种类型的对象)。
为了选择增量基础,对象(主要)按 文件名,然后是大小。此排序列表的窗口用于限制 被视为潜在增量基础的对象数量。 如果找不到对象的“足够好”1 delta 表示 在其窗口中的对象中,则该对象不会是 delta 压缩。
窗口的大小由--window=
选项控制
git pack-objects
或 pack.window
配置变量。这
delta 链的最大深度由--depth=
控制
git pack-objects
的选项或pack.depth
配置
多变的。 git gc
的--aggressive
选项大大放大
尝试创建的窗口大小和最大深度
一个较小的包文件。
文件名排序将带有 with 的条目的对象聚集在一起
相同的名称(或至少相似的结尾(例如.c
))。规模
排序是从最大到最小,以便删除数据的增量是
优于添加数据的增量(因为删除增量具有更短的
表示)等较早的、较大的对象(通常
较新)倾向于用普通压缩来表示。
1 什么是“足够好”取决于所讨论对象的大小及其潜在的 delta 基数以及由此产生的 delta 链的深度。
对象排序启发式
对象存储在“最近引用”的包文件中 命令。重建最近历史所需的对象是 在包装中较早放置,它们将靠得很近。这 通常适用于 OS 磁盘缓存。
所有提交对象都按提交日期排序(最近的在前)
并存储在一起。这种放置和排序优化了磁盘
遍历历史图和提取基本提交所需的访问
信息(例如git log
)。
树和 blob 对象从树开始存储 第一次存储的(最近的)提交。每棵树都经过深度处理 第一种方式,存储任何尚未存储的对象 存储。这会将重建所需的所有树和 blob 最近的提交一起放在一个地方。任何树木和斑点 尚未保存,但以后提交所需的是 接下来存储,按照排序的提交顺序。
最终的对象排序受增量基础选择的轻微影响 因为如果一个对象被选择用于增量表示及其基础对象 还没有被存储,那么它的基础对象就在 对象本身。这可以防止由于 读取“自然地”被 稍后存储在包文件中。
【讨论】:
【参考方案3】:正如我在“What are git's thin packs?”中提到的
Git 只在包文件中进行 deltification
我在“Is the git binary diff algorithm (delta storage) standardized?”中详细介绍了用于打包文件的增量编码。 另见“When and how does git use deltas for storage?”。
请注意,对于 Git 2.0.x/2.1(2014 年第三季度),控制包文件默认大小的 core.deltaBaseCacheLimit
配置很快将从 16MB 增加到 96MB。
参见 David Kastrup 的 commit 4874f54(2014 年 5 月):
将 core.deltaBaseCacheLimit 提高到 96m
16m 的默认值会导致大型 delta 链与大型文件相结合时出现严重抖动。
以下是一些基准测试(
git blame
的 pu 变体):
time git blame -C src/xdisp.c >/dev/null
用于在 SSD 驱动器上使用
git gc --aggressive
(v1.9,导致窗口大小为 250)重新打包的 Emacs 存储库。 有问题的文件大约有 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-pack
”(man) 学会了以更高的并行度解析 deltified 对象。
参见commit f08cbf6(2020 年 9 月 8 日)和commit ee6f058、commit b4718ca、commit a7f7e84、commit 46e6fb1、commit fc968e2、commit 009be0d(2020 年 8 月 24 日)Jonathan Tan (jhowtan
)。(由 Junio C Hamano -- gitster
-- 合并于 commit b7e65b5,2020 年 9 月 22 日)
index-pack
: 减少工作量签字人:Jonathan Tan
目前,当 index-pack 解析增量时,它不会将增量树拆分为线程:每个增量基根(不是
REF_DELTA
或OFS_DELTA)
的对象可以进入自己的线程,但所有增量在该根上(直接或间接)在同一个线程中处理。当存储库包含多次修改的大型文本文件(因此,可增量)时,这是一个问题 - 提取期间的增量解析时间主要是处理与该文本文件对应的增量。
此补丁包含一个解决方案。 克隆时使用
git -c core.deltabasecachelimit=1g clone \ https://fuchsia.googlesource.com/third_party/vulkan-cts
在我的笔记本电脑上,克隆时间从 3m2s 缩短到 2m5s(使用 3 个线程,这是默认设置)。
解决方案是拥有一个全局工作堆栈。这个堆栈包含需要处理的增量基础(对象,无论是直接出现在包文件中还是由增量解析生成,它们本身都有增量子项);每当一个线程需要工作时,它都会查看堆栈顶部并处理下一个未处理的子进程。如果一个线程发现栈是空的,它会寻找更多的增量基根来压栈。
拥有全局工作堆栈的主要弱点是在互斥体上花费了更多时间,但分析表明大部分时间都花在了 delta 本身的解析上,因此这在实践中不应该成为问题。无论如何,实验(如上面的克隆命令中所述)表明这个补丁是一个净改进。
使用 Git 2.31(2021 年第一季度),您可以了解有关格式的更多详细信息。
参见Martin Ågren (none
)commit 7b77f5a(2020 年 12 月 29 日)。(由 Junio C Hamano -- gitster
-- 合并于 commit 16a8055,2021 年 1 月 15 日)
pack-format.txt
:增量数据开始时的文档大小报告人:Ross Light签字人:Martin Ågren
我们将增量数据记录为一组指令,但忘记记录这些指令之前的两个大小:基础对象的大小和要重构的对象的大小。 修正这个遗漏。
与其将有关编码的所有细节都塞进运行的文本中,不如引入一个单独的部分来详细说明我们的“尺寸编码”并参考它。
technical/pack-format
现在包含在其man page 中:
尺寸编码
本文档使用以下非负数的“尺寸编码” 整数:从每个字节开始,七个最低有效位是 用于形成结果整数。 只要是最重要的 bit 为 1,此过程继续; MSB 为 0 的字节提供 最后七位。
7 位块是连接在一起的。 之后 值更重要。
这种尺寸编码不应与“偏移编码”混淆, 本文档中也使用了它。
technical/pack-format
现在包含在其man page 中:
增量数据以基础对象的大小和 重建对象的大小。这些尺寸是 使用上面的大小编码进行编码。
剩下的 增量数据是重建对象的指令序列
【讨论】:
以上是关于Git 的包文件是增量而不是快照?的主要内容,如果未能解决你的问题,请参考以下文章