Het artikel analyseert de manier waarop Git data opslaat via zlib-compressie en het gebruik van packfiles om redundantie te verminderen.
Uit experimenten met 'losse objecten' komt naar voren dat:
- Kleine commits een relatief hoge overhead hebben (enkele kilobytes), omdat Git voor elke wijziging nieuwe objecten aanmaakt (het commit-object, tree-bestanden voor de mappenstructuur en de blob voor de inhoud).
- Grote bestanden zeer efficiënt worden verwerkt; binaire bestanden worden gecomprimeerd en grote tekstbestanden kunnen drastisch in omvang worden gereduceerd.
De conclusie is dat de exacte grootte van een commit een functie is van de samendrukbaarheid van de bestanden en de complexiteit van de betrokken directorystructuur.
Hoe groot is een Git-commit?
Referentie: git-scm.com/docs/gitformat-loose
Een andere factor is dat Git objectbestanden uiteindelijk omzet in nog efficiëntere packfiles, waardoor extra ruimte wordt bespaard door redundantie tussen objecten te verwijderen.
Referentie: git-scm.com/book/en/v2/Git-Internals-Packfiles
Experimentele resultaten
Ter illustratie zijn er experimenten uitgevoerd met uitsluitend "losse objecten" (loose objects), waarbij geen packfiles zijn aangemaakt. Dit zijn de resultaten:
- 64.828 bytes:
git init
- 47.640 bytes: commiten van 250 kleine bestanden
- 17.145 bytes: commiten van een wijziging van 3 bytes in een bestand binnen een directory van 50 bestanden
- 8.709 bytes: commiten van een wijziging van 3 bytes in een repository met slechts één bestand
- 297.873 bytes: commiten van een binair bestand van 583.840 bytes
- 1.622.188 bytes: commiten van een enorm bronbestand van 16.415.223 bytes
Hieruit blijkt dat er kilobytes aan overhead zijn bij zeer kleine commits. De exacte hoeveelheid hangt af van de grootte van de bestands- of directorystructuur die bij de wijzigingen betrokken is.
Bij grotere commits met een klein aantal bestanden is de opgeslagen data in .git ongeveer 10% tot 50% van de ongecomprimeerde gecommitteerde bestanden. Met andere woorden: het is extreem efficiënt.
---
Uitgebreid logboek van de experimenten
Initialisatie en 250 bestanden
Hoe groot is een gloednieuwe, lege Git-directory (.git)?
$ git init
Initialized empty Git repository in /home/dave/tmp/foo/.git/
$ du -sb .git
64828 .git
Dit is ongeveer 64 Kb. (De du-opties zijn: s voor samenvatting en b voor het weergeven van bytes.)
Vervolgens worden er met een set geneste shell-loops veel kleine bestanden aangemaakt:
$ for d in {1..5};do
mkdir $d;
for f in {1..50};do
echo foo > $d/$f;
done;
done
Er zijn nu 250 bestanden in 5 directories die allemaal de string "foo" bevatten:
$ ls
1 2 3 4 5
$ cat 2/45
foo
Na het commiten van deze bestanden:
$ git add .
$ git commit -m 'initial commit'
$ du -sb .git
112468 .git
De commit heeft ongeveer 47 Kb toegevoegd aan de initiële lege repo. (Berekening via dc, de reverse-polish notation calculator: 112468 64828 - p = 47.640).
Wanneer er één kleine wijziging in één bestand wordt aangebracht en gecommit:
$ echo bar > 1/1
$ git commit -am 'changed 1/1'
$ du -sb .git
129613
$ dc -e '129613 112468 - p'
17145
Er is dus 17 Kb nodig om ongeveer 3 bytes aan wijzigingen in de repo op te slaan.
Waar komt deze omvang vandaan?
Om te analyseren waar de ruimte naartoe gaat, kijken we naar de grootte van de bestanden in .git vóór en na een wijziging.
Situatie ervoor:
4096 .git/objects/info
4096 .git/objects/pack
4115 .git/objects/25
4155 .git/objects/7b
4225 .git/objects/af
4115 .git/objects/57
4271 .git/objects/07
4177 .git/objects/61
4255 .git/objects/bb
4299 .git/objects/a8
45900 .git/objects
4336 .git/info
4096 .git/refs/tags
4137 .git/refs/heads
12329 .git/refs
4096 .git/branches
4411 .git/logs/refs/heads
8507 .git/logs/refs
12918 .git/logs
27538 .git/hooks
129613 .git
Na het wijzigen van bestand 1/1 naar de string 'baz' en het commiten daarvan:
Situatie erna:
4096 .git/objects/info
4096 .git/objects/pack
4115 .git/objects/25
4253 .git/objects/81 <-- nieuw 4 Kb
4155 .git/objects/7b
4225 .git/objects/af
4115 .git/objects/57
4271 .git/objects/07
4177 .git/objects/61
4302 .git/objects/fc <-- nieuw 4 Kb
4255 .git/objects/bb
4115 .git/objects/76 <-- nieuw 4 Kb
4299 .git/objects/a8
4177 .git/objects/92 <-- nieuw 4 Kb
62747 .git/objects
4336 .git/info
4096 .git/refs/tags
4137 .git/refs/heads
12329 .git/refs
4096 .git/branches
4561 .git/logs/refs/heads <-- toegevoegd 0.1 Kb
8657 .git/logs/refs
13218 .git/logs
27538 .git/hooks
146759 .git
Er zijn 4 nieuwe objecten aangemaakt voor een commit van één enkel bestand. Met behulp van de documentatie over het Git-datamodel (jvns.ca) en het commando git show kunnen deze objecten worden geïdentificeerd:
- 81…: de eigenlijke commit.
- fc…: het tree-bestand voor de gewijzigde directory
1., met een lijst van alle bestanden in die directory (1 t/m 50).
- 76…: de eigenlijke inhoud van bestand
1/1 (nu de string 'baz').
- 92…: een andere tree voor de root-directory van de repo.
Er is dus sprake van één commit, twee trees en één bestand (blob).
Kleine repositories
Hoeveel ruimte kost een commit in een repository met slechts één klein bestand?
$ git init
$ echo foo > foo
$ git add foo
$ git commit -m 'initial'
$ du -sb .git
94208
$ echo bar > foo
$ git -am 'change1'
$ du -sb .git
102917
$ dc -e '102917 94208 - p'
8709
Dit komt neer op 8 Kb per commit, ongeveer de helft van de grootte van een repo met een grotere bestandsstructuur. Veel van deze omvang is overhead; bij grotere commits zullen de relatieve besparingen aanzienlijk zijn.
"Grote" bestanden
Wat gebeurt er bij het toevoegen van een "groot" binair bestand (580 Kb)?
$ cp /usr/bin/librewolf .
$ du -b librewolf
583840
$ git add .
$ git commit -m 'added librewolf'
$ du -sb .git
400790
$ dc -e '400790 102917 - p'
297873
Het binaire bestand wordt behoorlijk gecomprimeerd, waardoor de commit kleiner is dan het toegevoegde bestand (300 Kb voor een bestand van 580 Kb).
Bij een enorm tekst-bronbestand (16,2 Mb) is het resultaat nog indrukwekkender:
$ cp /usr/src/linux...mask.h .
$ du -b nbio_7_2_0_sh_mask.h
16415223
$ git add .
$ git commit -m 'huge source'
$ du -sb .git
2022978
$ dc -e '2022978 400790 - p'
1622188
Er is slechts 1,6 Mb nodig om een bronbestand van 16,4 Mb te commiten. Hoewel te verwachten was dat tekst goed zou comprimeren, is dit resultaat zeer indrukwekkend.
Hoe groot is een Git-commit?
Referentie: git-scm.com/docs/gitformat-loose
Een andere factor is dat Git objectbestanden uiteindelijk omzet in nog efficiëntere packfiles, waardoor extra ruimte wordt bespaard door redundantie tussen objecten te verwijderen.
Referentie: git-scm.com/book/en/v2/Git-Internals-Packfiles
Experimentele resultaten
Ter illustratie zijn er experimenten uitgevoerd met uitsluitend "losse objecten" (loose objects), waarbij geen packfiles zijn aangemaakt. Dit zijn de resultaten:
- 64.828 bytes:
git init
- 47.640 bytes: commiten van 250 kleine bestanden
- 17.145 bytes: commiten van een wijziging van 3 bytes in een bestand binnen een directory van 50 bestanden
- 8.709 bytes: commiten van een wijziging van 3 bytes in een repository met slechts één bestand
- 297.873 bytes: commiten van een binair bestand van 583.840 bytes
- 1.622.188 bytes: commiten van een enorm bronbestand van 16.415.223 bytes
Hieruit blijkt dat er kilobytes aan overhead zijn bij zeer kleine commits. De exacte hoeveelheid hangt af van de grootte van de bestands- of directorystructuur die bij de wijzigingen betrokken is.
Bij grotere commits met een klein aantal bestanden is de opgeslagen data in .git ongeveer 10% tot 50% van de ongecomprimeerde gecommitteerde bestanden. Met andere woorden: het is extreem efficiënt.
---
Uitgebreid logboek van de experimenten
Initialisatie en 250 bestanden
Hoe groot is een gloednieuwe, lege Git-directory (.git)?
$ git init
Initialized empty Git repository in /home/dave/tmp/foo/.git/
$ du -sb .git
64828 .git
Dit is ongeveer 64 Kb. (De du-opties zijn: s voor samenvatting en b voor het weergeven van bytes.)
Vervolgens worden er met een set geneste shell-loops veel kleine bestanden aangemaakt:
$ for d in {1..5};do
mkdir $d;
for f in {1..50};do
echo foo > $d/$f;
done;
done
Er zijn nu 250 bestanden in 5 directories die allemaal de string "foo" bevatten:
$ ls
1 2 3 4 5
$ cat 2/45
foo
Na het commiten van deze bestanden:
$ git add .
$ git commit -m 'initial commit'
$ du -sb .git
112468 .git
De commit heeft ongeveer 47 Kb toegevoegd aan de initiële lege repo. (Berekening via dc, de reverse-polish notation calculator: 112468 64828 - p = 47.640).
Wanneer er één kleine wijziging in één bestand wordt aangebracht en gecommit:
$ echo bar > 1/1
$ git commit -am 'changed 1/1'
$ du -sb .git
129613
$ dc -e '129613 112468 - p'
17145
Er is dus 17 Kb nodig om ongeveer 3 bytes aan wijzigingen in de repo op te slaan.
Waar komt deze omvang vandaan?
Om te analyseren waar de ruimte naartoe gaat, kijken we naar de grootte van de bestanden in .git vóór en na een wijziging.
Situatie ervoor:
4096 .git/objects/info
4096 .git/objects/pack
4115 .git/objects/25
4155 .git/objects/7b
4225 .git/objects/af
4115 .git/objects/57
4271 .git/objects/07
4177 .git/objects/61
4255 .git/objects/bb
4299 .git/objects/a8
45900 .git/objects
4336 .git/info
4096 .git/refs/tags
4137 .git/refs/heads
12329 .git/refs
4096 .git/branches
4411 .git/logs/refs/heads
8507 .git/logs/refs
12918 .git/logs
27538 .git/hooks
129613 .git
Na het wijzigen van bestand 1/1 naar de string 'baz' en het commiten daarvan:
Situatie erna:
4096 .git/objects/info
4096 .git/objects/pack
4115 .git/objects/25
4253 .git/objects/81 <-- nieuw 4 Kb
4155 .git/objects/7b
4225 .git/objects/af
4115 .git/objects/57
4271 .git/objects/07
4177 .git/objects/61
4302 .git/objects/fc <-- nieuw 4 Kb
4255 .git/objects/bb
4115 .git/objects/76 <-- nieuw 4 Kb
4299 .git/objects/a8
4177 .git/objects/92 <-- nieuw 4 Kb
62747 .git/objects
4336 .git/info
4096 .git/refs/tags
4137 .git/refs/heads
12329 .git/refs
4096 .git/branches
4561 .git/logs/refs/heads <-- toegevoegd 0.1 Kb
8657 .git/logs/refs
13218 .git/logs
27538 .git/hooks
146759 .git
Er zijn 4 nieuwe objecten aangemaakt voor een commit van één enkel bestand. Met behulp van de documentatie over het Git-datamodel (jvns.ca) en het commando git show kunnen deze objecten worden geïdentificeerd:
- 81…: de eigenlijke commit.
- fc…: het tree-bestand voor de gewijzigde directory
1., met een lijst van alle bestanden in die directory (1 t/m 50).
- 76…: de eigenlijke inhoud van bestand
1/1 (nu de string 'baz').
- 92…: een andere tree voor de root-directory van de repo.
Er is dus sprake van één commit, twee trees en één bestand (blob).
Kleine repositories
Hoeveel ruimte kost een commit in een repository met slechts één klein bestand?
$ git init
$ echo foo > foo
$ git add foo
$ git commit -m 'initial'
$ du -sb .git
94208
$ echo bar > foo
$ git -am 'change1'
$ du -sb .git
102917
$ dc -e '102917 94208 - p'
8709
Dit komt neer op 8 Kb per commit, ongeveer de helft van de grootte van een repo met een grotere bestandsstructuur. Veel van deze omvang is overhead; bij grotere commits zullen de relatieve besparingen aanzienlijk zijn.
"Grote" bestanden
Wat gebeurt er bij het toevoegen van een "groot" binair bestand (580 Kb)?
$ cp /usr/bin/librewolf .
$ du -b librewolf
583840
$ git add .
$ git commit -m 'added librewolf'
$ du -sb .git
400790
$ dc -e '400790 102917 - p'
297873
Het binaire bestand wordt behoorlijk gecomprimeerd, waardoor de commit kleiner is dan het toegevoegde bestand (300 Kb voor een bestand van 580 Kb).
Bij een enorm tekst-bronbestand (16,2 Mb) is het resultaat nog indrukwekkender:
$ cp /usr/src/linux...mask.h .
$ du -b nbio_7_2_0_sh_mask.h
16415223
$ git add .
$ git commit -m 'huge source'
$ du -sb .git
2022978
$ dc -e '2022978 400790 - p'
1622188
Er is slechts 1,6 Mb nodig om een bronbestand van 16,4 Mb te commiten. Hoewel te verwachten was dat tekst goed zou comprimeren, is dit resultaat zeer indrukwekkend.