Sind Gits Packdateien eher Deltas als Schnappschüsse?


70

Einer der Hauptunterschiede zwischen Git und den meisten anderen Versionskontrollsystemen besteht darin, dass die anderen Commits als eine Reihe von Deltas speichern - Änderungssätze zwischen einem Commit und dem nächsten. Dies erscheint logisch, da es sich um die kleinstmögliche Menge an Informationen handelt, die über ein Commit gespeichert werden müssen. Je länger der Commit-Verlauf dauert, desto mehr Berechnungen sind erforderlich, um die Revisionsbereiche zu vergleichen.

Im Gegensatz dazu speichert Git in jeder Revision einen vollständigen Schnappschuss des gesamten Projekts . Der Grund dafür, dass die Repo-Größe mit jedem Commit nicht dramatisch zunimmt, ist, dass jede Datei im Projekt als Datei im Git-Unterverzeichnis gespeichert wird, die nach dem Hash ihres Inhalts benannt ist. Wenn sich der Inhalt nicht geändert hat, hat sich der Hash nicht geändert, und das Festschreiben zeigt nur auf dieselbe Datei. Und es gibt noch andere Optimierungen.

All dies machte für mich Sinn, bis ich auf diese Informationen über Packdateien stieß , in die Git regelmäßig Daten einfügt, um Platz zu sparen:

Um diesen Platz zu sparen, verwendet Git die Packdatei. Dies ist ein Format, in dem Git nur den Teil speichert, der sich in der zweiten Datei geändert hat, mit einem Zeiger auf die Datei, der es ähnlich ist.

Geht das nicht im Grunde auf die Speicherung von Deltas zurück? Wenn nicht, wie ist es anders? Wie kann vermieden werden, dass Git denselben Problemen ausgesetzt wird, die andere Versionskontrollsysteme haben?

Zum Beispiel verwendet Subversion Deltas, und das Zurücksetzen von 50 Versionen bedeutet, dass 50 Unterschiede rückgängig gemacht werden, während Sie mit Git einfach den entsprechenden Schnappschuss erstellen können. Es sei denn, git speichert auch 50 Unterschiede in den Packdateien ... gibt es einen Mechanismus, der besagt, dass "nach einer kleinen Anzahl von Deltas ein ganz neuer Schnappschuss gespeichert wird", damit wir keinen zu großen Änderungssatz stapeln? Wie könnte Git sonst die Nachteile von Deltas vermeiden?


6
Siehe pack-format.txt und pack-heuristics.txt in der Dokumentation.
Josh Lee

@jleedev: Danke für die Links!
Paŭlo Ebermann

Antworten:


72

Zusammenfassung:
Die Pack-Dateien von Git wurden sorgfältig erstellt, um Festplatten-Caches effektiv zu nutzen und "nette" Zugriffsmuster für allgemeine Befehle und zum Lesen kürzlich referenzierter Objekte bereitzustellen.


Das Pack-Dateiformat von Git ist sehr flexibel (siehe Dokumentation / technisch / pack-format.txt oder The Packfile im Git Community Book ). In den Packdateien werden Objekte auf zwei Arten gespeichert: "undeltifiziert" (nehmen Sie die Rohobjektdaten und entleeren Sie sie) oder "deltifiziert" (bilden Sie ein Delta gegen ein anderes Objekt und entleeren Sie dann die resultierenden Delta-Daten). Die in einer Packung gespeicherten Objekte können in beliebiger Reihenfolge vorliegen (sie müssen (nicht unbedingt) nach Objekttyp, Objektname oder einem anderen Attribut sortiert sein), und deltifizierte Objekte können gegen jedes andere geeignete Objekt desselben Typs erstellt werden.

Der Befehl pack-objects von Git verwendet mehrere Heuristiken , um eine hervorragende Referenzlokalität für allgemeine Befehle bereitzustellen . Diese Heuristiken steuern sowohl die Auswahl der Basisobjekte für deltifizierte Objekte als auch die Reihenfolge der Objekte. Jeder Mechanismus ist größtenteils unabhängig, aber sie teilen einige Ziele.

Git bildet zwar lange Ketten von Delta-komprimierten Objekten, aber die Heuristiken versuchen sicherzustellen, dass sich nur „alte“ Objekte am Ende der langen Ketten befinden. Der Delta-Basis-Cache (dessen Größe von der core.deltaBaseCacheLimitKonfigurationsvariablen gesteuert wird ) wird automatisch verwendet und kann die Anzahl der für Befehle, die eine große Anzahl von Objekten lesen müssen (z git log -p. B. ), erforderlichen "Neuerstellungen" erheblich reduzieren .

Delta-Komprimierungsheuristik

Ein typisches Git-Repository speichert eine sehr große Anzahl von Objekten, sodass es nicht alle vernünftigerweise vergleichen kann, um die Paare (und Ketten) zu finden, die die kleinsten Delta-Darstellungen ergeben.

Die Heuristik zur Auswahl der Delta-Basen basiert auf der Idee, dass die guten Delta-Basen unter Objekten mit ähnlichen Dateinamen und Größen gefunden werden. Jeder Objekttyp wird separat verarbeitet (dh ein Objekt eines Typs wird niemals als Delta-Basis für ein Objekt eines anderen Typs verwendet).

Für die Auswahl der Delta-Basis werden die Objekte (hauptsächlich) nach Dateiname und dann nach Größe sortiert. Ein Fenster in diese sortierte Liste wird verwendet, um die Anzahl der Objekte zu begrenzen, die als potenzielle Delta-Basen betrachtet werden. Wenn für ein Objekt unter den Objekten in seinem Fenster keine "gut genug" 1- Delta-Darstellung gefunden wird, wird das Objekt nicht deltakomprimiert.

Die Größe des Fensters wird durch die --window=Option git pack-objectsoder die pack.windowKonfigurationsvariable gesteuert . Die maximale Tiefe einer Delta-Kette wird durch die --depth= Option git pack-objectsoder die pack.depthKonfigurationsvariable gesteuert . Durch die --aggressiveOption, git gcsowohl die Fenstergröße als auch die maximale Tiefe erheblich zu vergrößern, wird versucht, eine kleinere Packdatei zu erstellen.

Die Dateinamensortierung fasst die Objekte für Einträge mit identischen Namen (oder zumindest ähnlichen Endungen (z .c. B. )) zusammen. Die Größensortierung ist vom größten zum kleinsten, sodass Deltas, die Daten entfernen, Deltas vorgezogen werden, die Daten hinzufügen (da Entfernungsdeltas kürzere Darstellungen haben) und die früheren, größeren Objekte (normalerweise neuere) in der Regel mit einfacher Komprimierung dargestellt werden.

1 Was als „gut genug“ eingestuft wird, hängt von der Größe des betreffenden Objekts und seiner potenziellen Delta-Basis sowie von der Tiefe seiner resultierenden Delta-Kette ab.

Heuristik zur Objektbestellung

Objekte werden in den Packdateien in der Reihenfolge "Zuletzt referenziert" gespeichert. Die Objekte, die zur Rekonstruktion des letzten Verlaufs benötigt werden, werden früher im Paket platziert und liegen nahe beieinander. Dies funktioniert normalerweise gut für OS-Festplatten-Caches.

Alle Festschreibungsobjekte werden nach Festschreibungsdatum (zuletzt zuerst) sortiert und zusammen gespeichert. Diese Platzierung und Reihenfolge optimiert die Festplattenzugriffe, die zum Durchlaufen des Verlaufsdiagramms und zum Extrahieren grundlegender Festschreibungsinformationen (z git log. B. ) erforderlich sind .

Die Baum- und Blob-Objekte werden beginnend mit dem Baum ab dem ersten gespeicherten (letzten) Commit gespeichert. Jeder Baum wird in der Tiefe zuerst verarbeitet, wobei alle Objekte gespeichert werden, die noch nicht gespeichert wurden. Dadurch werden alle Bäume und Blobs, die für die Rekonstruktion des letzten Commits erforderlich sind, an einem Ort zusammengefasst. Alle Bäume und Blobs, die noch nicht gespeichert wurden, aber für spätere Festschreibungen erforderlich sind, werden als Nächstes in der sortierten Festschreibungsreihenfolge gespeichert.

Die endgültige Objektreihenfolge wird durch die Delta-Basisauswahl geringfügig beeinflusst. Wenn ein Objekt für die Delta-Darstellung ausgewählt wird und sein Basisobjekt noch nicht gespeichert wurde, wird sein Basisobjekt unmittelbar vor dem Delta-Objekt selbst gespeichert. Dies verhindert wahrscheinliche Festplatten-Cache-Fehler aufgrund des nichtlinearen Zugriffs, der zum Lesen eines Basisobjekts erforderlich ist, das "natürlich" später in der Pack-Datei gespeichert worden wäre.


8

Die Verwendung von Delta-Speicher in der Pack-Datei ist nur ein Implementierungsdetail. Auf dieser Ebene weiß Git nicht, warum oder wie sich etwas von einer Revision zur nächsten geändert hat, sondern weiß nur, dass Blob B mit Ausnahme dieser Änderungen C Blob A ziemlich ähnlich ist. Daher werden nur Blob A und Änderungen C gespeichert (Wenn dies gewünscht wird, kann auch Blob A und Blob B gespeichert werden.)

Beim Abrufen von Objekten aus der Packdatei wird der Delta-Speicher dem Aufrufer nicht zur Verfügung gestellt. Der Anrufer sieht immer noch vollständige Blobs. Git funktioniert also genauso wie immer ohne die Delta-Speicheroptimierung.


4
All dies gilt gleichermaßen für Deltas, die von den meisten anderen Versionskontrollsystemen gespeichert werden ...
Jerry Coffin

12
Die Beziehungen zwischen den Blobs beim Komprimieren haben möglicherweise nichts mit den Beziehungen zu tun, die im Revisionsverlauf vorgeschlagen / impliziert werden.
Ken Bloom

2
Ich verstehe, dass dies nicht für den Aufrufer verfügbar ist, aber um einen bestimmten Hash abzurufen, muss Git möglicherweise einen Blob nehmen und einen Änderungssatz anwenden. Es gibt keinen echten Schnappschuss, oder? Was unterscheidet dies von SVN zu diesem Zeitpunkt - ist es, dass es nie eine große Anzahl von Deltas stapeln muss, um zu einem bestimmten Punkt in der Geschichte zu gelangen, weil es begrenzt, wie viele Deltas auf einem Blob gestapelt sind?
Nathan Long

2
Ich bin mir nicht sicher, wie es sich von SVN unterscheidet, weil ich nicht wirklich sicher bin, wie SVN im Inneren funktioniert. Aber ich kann darauf hinweisen, dass es sich von Darcs (und der Darcs-Theorie der Patches) darin unterscheidet, dass die Pack-Dateien nichts damit zu tun haben, wie Git Zweige zusammenführt. Git rekonstruiert den Snapshot der Revisionen, die zusammengeführt werden. Außerdem rekonstruiert es Snapshots aller älteren Revisionen, die zusammengeführt werden müssen, und findet dann heraus, was zu tun ist. Darcs hingegen speichert Patches und kombiniert sie, wenn Sie ein Arbeitsverzeichnis benötigen. Außerdem können verschiedene Arbeitsverzeichnisse basierend auf verschiedenen Patch-Sätzen erstellt werden.
Ken Bloom

5
Git tut Limit , wie viele Deltas müssen angewandt werden , um ein Objekt zu regenerieren (und btw Delta Darstellung für Bäume als Blobs ebenso nützlich ist) , welche die --depthParameter git pack-objects. Einige, die sich ansehen, was tatsächlich gepackt wird, legen auch nahe, dass Git ein Delta benötigt, das wirklich recht klein ist, damit es sich lohnt, es anstelle des komprimierten Blobs zu verwenden.
Araqnid

4

Wie ich in " Was sind die dünnen Packs von Git? " Erwähnt habe.

Git macht Deltifizierung nur in Packdateien

Ich habe die Delta-Codierung, die für Pack-Dateien verwendet wird, in " Ist der Git-Binärdiff-Algorithmus (Delta-Speicher) standardisiert? " Ausführlich beschrieben .
Siehe auch " Wann und wie verwendet Git Deltas zur Lagerung? ".

Beachten Sie, dass die core.deltaBaseCacheLimitKonfiguration, die die Standardgröße für die Packdatei steuert, für Git 2.0.x / 2.1 (Q3 2014) bald von 16 MB auf 96 MB erhöht wird.

Siehe Commit 4874f54 von David Kastrup (Mai 2014):

Bump core.deltaBaseCacheLimit auf 96m

Die Standardeinstellung von 16 m führt bei großen Delta-Ketten in Kombination mit großen Dateien zu ernsthaftem Thrashing.

Hier sind einige Benchmarks (pu-Variante von git blame):

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

für ein Repository von Emacs, das mit git gc --aggressive(v1.9, was zu einer Fenstergröße von 250 führt) neu gepackt wurde und sich auf einem SSD-Laufwerk befindet.
Die betreffende Datei hat ungefähr 30000 Zeilen, eine Größe von 1 MB und einen Verlauf mit ungefähr 2500 Commits.

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

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

Dies wird mit Git 2.29 (Q4 2020) weiter optimiert, wo " git index-pack" ( man ) gelernt hat, deltifizierte Objekte mit größerer Parallelität aufzulösen.

Siehe Commit f08cbf6 ( 08.09.2020 ) und Commit ee6f058 , Commit b4718ca , Commit a7f7e84 , Commit 46e6fb1 , Commit fc968e2 , Commit 009be0d (24. August 2020) von Jonathan Tan ( jhowtan) .
(Zusammengeführt von Junio ​​C Hamano - gitster- in Commit b7e65b5 , 22. September 2020)

index-pack: Quantität der Arbeit verkleinern

Unterzeichnet von: Jonathan Tan

Wenn das Index-Pack Deltas auflöst, werden Delta-Bäume derzeit nicht in Threads aufgeteilt: Jede Delta-Basiswurzel (ein Objekt, das kein REF_DELTAoder kein OFS_DELTA)eigener Thread ist, aber alle Deltas auf dieser Wurzel (direkt oder indirekt) sind im selben Thread verarbeitet.

Dies ist ein Problem, wenn ein Repository eine große Textdatei enthält (also Delta-fähig), die viele Male geändert wird. Die Delta-Auflösungszeit während des Abrufs wird durch die Verarbeitung der dieser Textdatei entsprechenden Deltas dominiert.

Dieser Patch enthält eine Lösung dafür.
Beim Klonen mit

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

Auf meinem Laptop verbesserte sich die Klonzeit von 3 m2 auf 2 m5 (standardmäßig mit 3 Threads).

Die Lösung besteht in einem globalen Workstack. Dieser Stapel enthält Delta-Basen (Objekte, die direkt in der Packdatei erscheinen oder durch Delta-Auflösung generiert werden und selbst Delta-Kinder haben), die verarbeitet werden müssen. Wenn ein Thread Arbeit benötigt, schaut er oben auf den Stapel und verarbeitet sein nächstes unverarbeitetes untergeordnetes Element. Wenn ein Thread den Stapel leer findet, sucht er nach mehr Delta-Basiswurzeln, um stattdessen auf den Stapel zu drücken.

Die Hauptschwäche eines globalen Workstacks besteht darin, dass mehr Zeit im Mutex verbracht wird. Die Profilerstellung hat jedoch gezeigt, dass die meiste Zeit für die Auflösung der Deltas selbst aufgewendet wird. Daher sollte dies in der Praxis kein Problem sein. In jedem Fall zeigt das Experimentieren (wie im obigen Klonbefehl beschrieben), dass dieser Patch eine Nettoverbesserung darstellt.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.