veilletech.fr
9 nov. Feed du jour
#04 PERFORMANCE Article

Git : supprimer une référence, 18 % plus vite

Avant de croire une hypothèse de performance, mesurez-la.

Dans le backend files de Git, supprimer une référence empaquetée réécrit tout le fichier packed-refs — plusieurs gigaoctets et environ 2 s par suppression sur un dépôt de 15 millions de références comme celui de GitLab. Karthik Nayak a découvert, en comparant à une réécriture Rust, que le code C reformatait inutilement chaque référence intacte ; l'écrire verbatim avec fwrite() donne 18 % de gain. Le correctif est fusionné et arrive dans Git 2.98.

2 min de lectureavancévidéo 1:21
Partager
Sommaire4 sections
  1. Ce qui se passe
  2. D'où vient le gain
  3. Ce que ça change
  4. À retenir

Ce qui se passe

Dans Git, une référence est un pointeur nommé vers un objet (une branche, une étiquette). Le backend historique, dit files, stocke les références empaquetées dans un unique fichier trié, packed-refs. Problème : supprimer une entrée oblige à réécrire tout le fichier. Sur un dépôt géant, c'est lent. Celui de GitLab dépasse 15 millions de références ; packed-refs peut atteindre plusieurs gigaoctets. Sur un fichier de 700 Mo, une seule suppression prenait 2,14 s :

Terminal
❯ eza -l .git/packed-refs
.rw-r--r-- 700M karthik  8 Nov 15:16 .git/packed-refs

❯ time git update-ref -d refs/heads/branch-0000499999
Executed in    2.14 secs

D'où vient le gain

Karthik Nayak, qui travaille sur Git chez GitLab, ne cherchait pas cette optimisation. Pour mesurer le coût de la frontière entre C et Rust (l'objectif à terme : Git 3.0 rendra Rust obligatoire), il avait réécrit le backend files en Rust, puis comparé les deux. Surprise, la version Rust supprimait 17,7 % plus vite (30,6 ms contre 37,1 ms, à 100 000 références).

La cause tenait dans une inutilité du code C. Quand il recopie une référence qu'il ne modifie pas, il la reconstruisait ligne par ligne avec fprintf() et oid_to_hex(), là où la version Rust écrivait les octets tels quels. Le correctif écrit les enregistrements intacts directement, avec fwrite() :

Diff
 		if (cmp < 0) {
-			/* Pass the old reference through. */
-			if (write_packed_entry(out, iter->ref.name,
-					       iter->ref.oid, iter->ref.peeled_oid))
+			size_t len = packed_iter->pos - packed_iter->record_start;
+			if (fwrite(packed_iter->record_start, len, 1, out) != 1)
 				goto write_error;

Résultat : 18 % de gain (54,9 ms contre 66,5 ms), à égalité avec la version Rust (55,2 ms). La leçon dépasse le correctif : l'hypothèse « Rust est plus rapide » était fausse, c'était du travail superflu côté C — et il a fallu mesurer pour le voir.

Ce que ça change

Autrement dit : si vous migrez vers reftable, le problème disparaît ; d'ici là, vos suppressions sur un gros dépôt en files seront sensiblement plus rapides.

Source : Karthik Nayak, en anglais.