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

Git 和大多数其他版本控制系统的一个关键区别是,其他版本控制系统倾向于将提交存储为一系列的 delta-change 集,在一个提交和下一个提交之间。这似乎是合乎逻辑的,因为它是存储关于提交的信息的最小可能数量。但是提交历史越长,比较修订范围所需的计算就越多。

相比之下,Git 存储 在每次修订中完成整个项目的快照。这没有使回购规模随着每次提交而急剧增长的原因是,项目中的每个文件都作为一个文件存储在 Git 子目录中,以其内容的散列命名。因此,如果内容没有更改,散列也没有更改,提交只是指向同一个文件。还有其他的优化。

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

为了节省空间,Git 利用包文件。这是一个 Git 只保存 第二部分改变了 文件,并且有一个指向它的文件的指针 类似于。

这不就是回到存储 delta 的老路上了吗?如果没有,有什么不同?这如何避免 Git 遇到其他版本控制系统所遇到的相同问题呢?

例如,Subversion 使用 deltas,回滚50个版本意味着撤消50个差异,而使用 Git 只需抓取适当的快照。除非 git 在包文件中也存储50个差异... ... 是否有某种机制说“在少量差异之后,我们将存储一个全新的快照”,这样我们就不会堆积太大的变更集?否则 Git 如何避免 delta 的缺点?

14034 次浏览

在包文件中使用 delta 存储只是一个实现细节。在这个层次上,Git 不知道某些东西为什么或者如何从一个版本改变到下一个版本,它只知道 blob B 与 blob A 非常相似,除了这些改变 C。所以它只会存储 blob A 并更改 C (如果它选择这样做——它也可以选择存储 blob A 和 blob B)。

当从包文件中检索对象时,delta 存储不会公开给调用方。调用者仍然看到完整的斑点。因此,Git 的工作方式与以往一样,没有增量存储优化。

摘要:
Git 的包文件是精心构造的,以有效地使用磁盘缓存和 为常用命令和最近引用的读取提供“漂亮”的访问模式 物品。


Git 的文件夹 格式相当灵活(见 文档/technology/pack-format. txt, 或 Git 社区手册中的 包裹文件)。 包文件以两种主要方式存储对象 方法: “未定义”(采取原始对象数据和放气-压缩 它) ,或“分隔”(形成一个三角形对一些其他对象然后 压缩生成的 delta 数据) 包可以按任何顺序排列(不一定) 按对象类型、对象名称或任何其他属性排序)和 分隔对象可以针对任何其他相同类型的合适对象制作。

Git 的 Pack-Objects 命令使用几个 启发式来 为普通人提供优良的 访问局部性 命令。这些启发式控制基地的选择 分隔对象的对象和对象的顺序 机制大多是独立的,但它们有一些共同的目标。

Git 确实形成了 delta 压缩对象的长链,但是 启发式试图确保只有“旧的”对象位于 增量基础缓存(其大小由 core.deltaBaseCacheLimit配置变量)是自动的 使用,并可以大大减少“重建”的数量需要 需要读取大量对象的命令(例如 < code > git log) - p ) .

Delta 压缩启发式

典型的 Git 存储库存储大量对象,因此 它不能合理地将它们全部进行比较以找到对(和 链) ,它将产生最小的 delta 表示。

增量基选择启发式基于 在具有相似文件名的对象中可以找到良好的 delta 基 每种类型的对象都是分开处理的(即 类型的对象将永远不会用作 另一种类型的对象)。

为了进行 delta 基选择,对象的排序(主要)依据 文件名,然后调整大小。进入这个排序列表的窗口用于限制 被认为是潜在的 δ 基的对象的数量。 如果没有为对象找到“足够好”的 1 delta 表示形式 在其窗口中的对象之间,那么该对象将不是 delta 压缩。

窗口的大小由 或者 pack.window配置变量 三角洲链的最大深度由 --depth=控制 选项的 git pack-objects,或 pack.depth配置 git gc--aggressive选项大大扩大 要尝试创建的窗口大小和最大深度 更小的包文件。

文件名排序将要使用 相同的名称(或至少相似的结尾(例如 .c)) Sort 是从最大到最小,因此删除数据的 delta 是 喜欢增加数据的增量(因为删除增量有更短的 表示) ,因此,较早的,较大的对象(通常 较新的)倾向于用普通压缩来表示。

1 什么是“足够好”取决于问题对象的大小和它潜在的 delta 基础,以及它的结果 delta 链有多深。

对象排序启发式

对象存储在包文件中的“最近引用”中 重建最近历史所需要的物体是 放置在包装的早期,他们将在一起。这 操作系统磁盘缓存通常运行良好。

所有提交对象都按提交日期排序(最近的第一个) 并存储在一起。这种布局和排序优化了磁盘 访问需要遍历历史图并提取基本提交 资料(例如 git log)。

类中的树开始存储树和 blob 对象 第一次存储(最近一次)提交。每个树在深度中处理 第一种方式,存储任何尚未被 这使得重建所需的所有树和斑点 最近的在一个地方。任何树木和斑点 尚未保存但以后提交所需的 然后以排序的提交顺序存储。

最终的对象顺序稍微受 delta 基选择的影响 如果选择一个对象作为 delta 表示及其基对象 尚未存储,则将其基对象存储在紧接 分隔对象本身。这可以防止磁盘缓存由于 读取基对象所需的非线性访问 存储在包文件中。

正如我在《 Git 的薄包装是什么?》中提到的

Git 只在包文件中进行划分

我在“ Git 二进制 diff 算法(delta 存储)是否标准化?”中详细描述了用于包文件的 delta 编码。
另见“ Git 何时以及如何使用 delta 进行存储?”。

注意,对于 Git 2.0,控制包文件默认大小的 core.deltaBaseCacheLimit配置将很快从16 MB 升级到96 MB。X/2.1(2014年第3季)。

见大卫 · 卡斯特鲁普 提交4874f54(2014年5月) :

Bump core.deltaBaseCache 限制为96米

默认值为1600万会导致大型 delta 链与大型文件结合在一起遭受严重冲击。

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

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

git gc --aggressive(v1.9,导致窗口大小为250)重新打包的 Emacs 存储库位于一个 SSD 驱动器上。
该文件大约有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(Q42020)中得到了进一步的优化,其中“ git index-pack(< a href = “ https://git-scm.com/docs/git-index-pack”rel = “ nofollow noReferrer”> man )学会了以更大的并行性来解析分隔对象。

参见 提交 f08cbf6(2020年9月8日)和 谭(jhowtan)提交 e6f058犯下 b4718ca犯罪,第七季,第84集第四十六季,第一集提交 fc968e2犯罪(2020年8月24日)。
(由 朱尼奥 · C · 哈马诺 gitster犯下 b7e65b5合并,2020年9月22日)

使工作量更小

签名: Jonathan Tan

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

如果存储库包含一个大型文本文件(因此,delta-able) ,并且该文件被修改了很多次,那么就会出现这个问题——在获取期间,delta 解析时间主要是处理与该文本文件相对应的 delta。

这个补丁包含一个解决方案。
当克隆使用

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

在我的笔记本电脑上,克隆时间从3m2提高到2m5(使用3个线程,这是默认的)。

解决方案是拥有一个全局工作堆栈。这个堆栈包含需要处理的 delta 基(对象,无论是直接出现在包文件中还是通过 delta 解析生成的,它们本身都有 delta 子元素) ; 每当一个线程需要工作时,它就会查看堆栈的顶部并处理下一个未处理的子元素。如果一个线程发现堆栈为空,它将寻找更多的 delta 基底根来推送堆栈。

拥有全局工作堆栈的主要缺点是在互斥锁中花费了更多的时间,但是分析表明大部分时间都花在了增量本身的解析上,所以在实践中这不应该是一个问题。在任何情况下,实验(如上面的克隆命令所述)表明,这个补丁是一个净改进。


使用 Git 2.31(Q12021) ,您可以获得关于格式的更多细节。

犯下7b77f5a(2020年12月29日) by Martin Ågren (none)
(由 朱尼奥 · C · 哈马诺 gitster提交16a8055合并,2021年1月15日)

pack-format.txt : delta 数据开始时的文档大小

作者: Ross Light
签名: Martin Ågren

我们将 delta 数据记录为一组指令,但是忘记记录这些指令之前的两个大小: 基对象的大小和要重构的对象的大小。
弥补这个遗漏。

与其将有关编码的所有细节填充到正在运行的文本中,不如引入一个单独的部分,详细说明我们的“大小编码”并引用它。

technical/pack-format现在在其 手册中包括:

大小编码

本文档使用以下非负数的“大小编码” 整数: 从每个字节,七个最低有效位是 用于形成结果整数。
只要是最重要的 位为1,此过程继续; 带有 MSB0的字节提供 最后七位。

七位块是连接的。
回见 价值观更为重要。

此大小编码不应与“偏移编码”混淆, 本文档中也使用了。

technical/pack-format现在在其 手册中包括:

Delta 数据以基对象的大小和 要重建的对象的大小。这些大小是 使用上面的大小编码进行编码。

余下的 Delta 数据是重建对象的一系列指令