Являются ли файлы пакета Git дельтами, а не снимками?

Одно из ключевых отличий между Git и большинством других систем управления версиями заключается в том, что другие, как правило, хранят коммиты как серию дельт - наборов изменений между одним коммитом и следующим. Это кажется логичным, поскольку это минимально возможное количество информации о коммите. Но чем дольше длится история фиксации, тем больше требуется вычислений для сравнения диапазонов ревизий.

Напротив, Git хранит полный снимок всего проекта в каждой ревизии. Причина, по которой это не приводит к резкому увеличению размера репо с каждым коммитом, заключается в том, что каждый файл в проекте хранится как файл в подкаталоге Git, названный по имени хэша его содержимого. Так что, если содержимое не изменилось, хеш не изменился, и фиксация просто указывает на тот же файл. И есть и другие оптимизации.

Все это имело смысл для меня, пока я не наткнулся на эту информацию о пакетных файлах, в которые Git периодически помещает данные для экономии места:

Чтобы сэкономить это место, Git использует файл пакета. Это формат, в котором Git сохраняет только часть, которая изменилась во втором файле, с указателем на файл, с которым он похож.

Разве это в основном не возвращает к хранению дельт? Если нет, то чем он отличается? Как это избежать подверженности Git тем же проблемам, что и другие системы контроля версий?

Например, Subversion использует дельты, а откат 50 версий означает отмена 50 различий, тогда как с Git вы можете просто получить соответствующий снимок. Если в git-файле git также не хранит 50 различий... есть ли какой-нибудь механизм, который говорит, что "после некоторого небольшого количества дельт мы сохраним совершенно новый снимок", чтобы мы не накапливали слишком большой набор изменений? Как еще Git может избежать недостатков дельт?

3 ответа

Решение

Резюме:
Пакетные файлы Git тщательно сконструированы для эффективного использования дисковых кешей и обеспечения "хороших" шаблонов доступа для общих команд и для чтения объектов, на которые недавно ссылались.


Формат файла пакета Git довольно гибкий (см. Documentation / technical / pack-format.txt или The Packfile в Книге сообщества Git). Файлы пакета хранят объекты двумя основными способами: "не определено" (взять необработанные данные объекта и сжать-сжать их), или "разделить" (сформировать дельту по отношению к некоторому другому объекту, а затем сжать результирующие дельта-данные). Объекты, хранящиеся в пакете, могут быть в любом порядке (они не должны (обязательно) сортироваться по типу объекта, имени объекта или любому другому атрибуту), а объекты с разделением могут быть созданы для любого другого подходящего объекта того же типа.

Команда Git pack-objects использует несколько эвристик, чтобы обеспечить отличную локализацию ссылок для общих команд. Эта эвристика контролирует как выбор базовых объектов для делитированных объектов, так и порядок объектов. Каждый механизм в основном независим, но у них есть общие цели.

Git формирует длинные цепочки дельта-сжатых объектов, но эвристика пытается убедиться, что только "старые" объекты находятся на концах длинных цепочек. Кэш дельта-базы (чей размер контролируетсяcore.deltaBaseCacheLimit переменная конфигурации) используется автоматически и может значительно уменьшить количество "перестроений", необходимых для команд, которые должны прочитать большое количество объектов (например, git log -p).

Дельта Сжатие Эвристический

Типичный репозиторий Git хранит очень большое количество объектов, поэтому он не может разумно сравнить их все, чтобы найти пары (и цепочки), которые дадут наименьшие дельта-представления.

Эвристика выбора дельта-баз основана на идее, что хорошие дельта-базы будут найдены среди объектов с похожими именами файлов и размерами. Каждый тип объекта обрабатывается отдельно (т. Е. Объект одного типа никогда не будет использоваться в качестве дельта-базы для объекта другого типа).

В целях выбора дельта-базы объекты сортируются (в основном) по имени файла, а затем по размеру. Окно в этот отсортированный список используется для ограничения количества объектов, которые рассматриваются как потенциальные дельта-базы. Если "достаточно хорошее"1 дельта-представление не найдено для объекта среди объектов в его окне, то объект не будет дельта-сжатым.

Размер окна контролируется --window= вариантgit pack-objects, или pack.window переменная конфигурации. Максимальная глубина дельта-цепи контролируется --depth=вариант git pack-objects, или pack.depth переменная конфигурации. --aggressive вариант git gc значительно увеличивает как размер окна, так и максимальную глубину, чтобы попытаться создать файл меньшего размера.

Сортировка имен файлов объединяет объекты для записей с одинаковыми именами (или, по крайней мере, с похожими окончаниями (например, .c)). Сортировка по размеру - от наибольшего к наименьшему, поэтому дельты, удаляющие данные, предпочтительнее дельт, добавляющих данные (поскольку дельты удаления имеют более короткие представления), и поэтому более ранние, более крупные объекты (обычно более новые) имеют тенденцию быть представлены с простым сжатием.

1 То, что считается "достаточно хорошим", зависит от размера рассматриваемого объекта и его потенциальной дельта-базы, а также от того, насколько глубокой должна быть полученная в результате дельта-цепь.

Эвристический порядок объектов

Объекты хранятся в файлах пакета в порядке "самой последней ссылки". Объекты, необходимые для восстановления самой последней истории, размещены ранее в пакете, и они будут близко друг к другу. Обычно это хорошо работает для кэшей диска ОС.

Все объекты фиксации сортируются по дате фиксации (сначала самые последние) и сохраняются вместе. Такое размещение и упорядочение оптимизирует доступ к диску, необходимый для просмотра графа истории и извлечения базовой информации о коммите (например, git log).

Объекты дерева и BLOB-объектов сохраняются, начиная с дерева с первой сохраненной (самой последней) фиксации. Каждое дерево обрабатывается в глубине, сохраняя все объекты, которые еще не были сохранены. Это объединяет все деревья и объекты, необходимые для восстановления самого последнего коммита, в одном месте. Все деревья и BLOB-объекты, которые еще не были сохранены, но требуются для последующих коммитов, сохраняются в следующем порядке, в отсортированном порядке.

Окончательный порядок объектов незначительно зависит от выбора дельта-базы в том смысле, что если объект выбран для дельта-представления, а его базовый объект еще не сохранен, то его базовый объект сохраняется непосредственно перед самим делитифицированным объектом. Это предотвращает возможные ошибки дискового кэша из-за нелинейного доступа, необходимого для чтения базового объекта, который "естественно" был бы сохранен позднее в файле пакета.

Использование дельта-хранилища в файле пакета - это просто деталь реализации. На этом уровне Git не знает, почему или как что-то изменилось от одной ревизии к другой, скорее, он просто знает, что BLOB-объект очень похож на BLOB-объект A, за исключением этих изменений C. Таким образом, он будет хранить только BLOB-объект A и изменения C (если он решит это сделать - он также может хранить блоб A и блоб B).

При извлечении объектов из файла пакета дельта-хранилище не отображается вызывающей стороне. Звонящий все еще видит полные сгустки. Итак, Git работает так же, как и всегда, без оптимизации дельта-хранилища.

Как я уже упоминал в " Что такое Git's Slim Pack? "

Git делает делитификацию только в packfiles

Я подробно описал дельта-кодирование, используемое для файлов пакета, в " Стандартизирован ли алгоритм git bin diff (delta storage)? ".
Смотрите также " Когда и как Git использует дельты для хранения? ".

Обратите внимание, что core.deltaBaseCacheLimit config, который управляет размером по умолчанию для файла пакета, скоро будет увеличен с 16 МБ до 96 МБ для Git 2.0.x/2.1 (3 квартал 2014 года).

Смотрите коммит 4874f54 Дэвида Каструпа (май 2014):

Увеличьте core.deltaBaseCacheLimit до 96 м

Значение по умолчанию 16m приводит к серьезным изменениям для больших дельта-цепочек в сочетании с большими файлами.

Вот несколько тестов (вариант пу git blame):

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

для хранилища Emacs, перепакованного с git gc --aggressive (v1.9, в результате размер окна 250), расположенный на диске SSD.
Файл, о котором идет речь, имеет около 30000 строк, размер 1 МБ и историю с около 2500 коммитами.

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

96m:
  real  2m5.668s
  user  1m50.784s
  sys   0m14.288s
Другие вопросы по тегам