Являются ли файлы пакета 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