Contexte du tamponnage

Les flux de sortie standard sont généralement pleinement tamponnés ou tamponnés par ligne. En mode terminal, la bibliothèque C sélectionne automatiquement le tamponnage par ligne, ce qui entraîne un write à chaque caractère \n. En dehors du terminal, le tampon est plein, les écritures ne sont effectuées que lorsque le tampon est saturé ou lorsqu’un fflush explicite est invoqué. Cette différence crée un goulet d’étranglement notable dans les applications non interactives qui écrivent fréquemment des lignes, comme unzip qui affiche chaque nom de fichier extrait.

Comportement des bibliothèques standards

Dans glibc, le tampon par ligne est appliqué de façon littérale : chaque appel à puts génère deux appels write, un pour la partie avant le \n et un autre pour le caractère de fin de ligne. L’exemple suivant montre le résultat d’un strace :

write(1, "a\n", 2) = 2
write(1, "b\n", 2) = 2
write(1, "c\n", 2) = 2

Python utilise la même règle : le tampon est vidé dès qu’un \n apparaît après l’ajout de nouvelles données, ce qui peut produire des écritures partielles de lignes.

Implémentation d’un tampon à échéance

L’article propose de remplacer la logique « flush‑on‑newline » par une stratégie basée sur un délai. Le tampon conserve les drapeaux BUF_LINE (activer le mode ligne) et BUF_ERR (indique une erreur d’écriture). La fonction append copie les octets dans le tampon, déclenchant un flush dès que la capacité est dépassée. Après chaque ajout, si le mode ligne est actif, la fonction has_newline recherche un \n et, le cas échéant, vide le tampon.

Le mécanisme d’échéance ajoute un champ deadline et une fonction deadline_flush :

typedef struct {
    int64_t deadline;
    // ...
} Buf;

void deadline_flush(Buf *b) {
    if ((b->flags & BUF_LINE) && b->len) {
        int64_t now = os_monotonic_clock();
        if (b->deadline && now > b->deadline) {
            flush(b);
            b->deadline = 0;
        } else {
            b->deadline = now + DEADLINE; // ~100 ms
        }
    }
}

Le délai (DEADLINE) est choisi en fonction de la perception humaine, typiquement 100 ms. Tant que le tampon reçoit des données avant l’expiration, les lignes s’accumulent et sont écrites en un seul appel write, réduisant drastiquement le nombre d’appels système.

Impact sur les performances

En pratique, l’approche permet d’afficher les lignes dès qu’une interaction humaine est attendue, tout en conservant la performance du tampon plein. Dans le cas de unzip, les noms de fichiers sont agrégés et écrits en blocs, éliminant le coût de plusieurs appels write sans introduire de latence perceptible. Aucun thread supplémentaire n’est requis ; le contrôle de l’échéance peut être invoqué entre les phases de travail (par exemple, après chaque bloc d’inflation). Le résultat est un programme qui « ne bloque pas » sur le terminal tout en exécutant plus rapidement que le modèle traditionnel basé sur les nouvelles lignes.