CodeGym /Cours /C# SELF /Principe de la mise en buffer des données

Principe de la mise en buffer des données

C# SELF
Niveau 41, Leçon 1
Disponible

1. Introduction

Imaginez que vous écrivez une lettre au crayon, mais que vous n'avez qu'un tout petit reste de gomme, suffisant pour un seul mot à la fois. Tant que vous n'effacez pas — vous ne pouvez pas continuer. Vous préféreriez effacer plus à la fois, non ? La mise en buffer, c'est un peu ce "paquet-gomme" : elle permet de travailler sur de gros morceaux de données d'un coup, au lieu de tout faire morceau par morceau.

En programmation la mise en buffer, c'est le stockage temporaire des données en mémoire (dans un "buffer") avant qu'une opération de lecture ou d'écriture sur disque n'ait lieu. C'est comme un panier à linge : vous y mettez les chaussettes pendant la semaine et vous lancez la machine une fois pour tout laver, plutôt que de laver une chaussette à la fois. Résultat : vous gagnez du temps (et des ressources !).

Opérations d'I/O

Les accès au disque dur, SSD ou clé USB sont parmi les opérations les plus lentes pour le CPU. La RAM fonctionne environ mille fois plus vite ! Donc si à chaque appel de Write ou Read les données allaient directement sur le disque, votre programme ramerait comme Windows XP sur un vieux portable avec 512 Mo de RAM.

La mise en buffer existe pour réduire le nombre d'accès physiques au disque et améliorer les performances.

2. Comment fonctionne la mise en buffer pour l'I/O

Buffer — c'est juste un morceau de mémoire où les données sont temporairement placées. Voilà comment ça se passe :

Lors de l'écriture d'un fichier :

  • Votre code effectue plusieurs appels à Write().
  • Toutes les données sont d'abord accumulées dans le buffer.
  • Quand le buffer est plein ou qu'il faut terminer l'opération, le contenu du buffer est écrit sur le disque en un seul grand lot.

Lors de la lecture d'un fichier :

  • Vous demandez de lire un petit morceau de données.
  • Le système lit immédiatement un gros bloc depuis le fichier et le met dans le buffer.
  • Quand vous faites l'appel suivant, les données sont déjà dans le buffer, et il n'est pas nécessaire d'accéder au disque.

En conséquence :

  • Moins d'accès au disque.
  • Les lectures et écritures sont plus rapides.

3. Mise en buffer dans .NET : où ça s'applique

Dans .NET la plupart des streams d'I/O utilisent la mise en buffer par défaut :

  • StreamWriter / StreamReader
  • FileStream
  • BufferedStream
  • Même Console.Out !

Mais la taille du buffer et son usage peuvent (et souvent doivent) être configurés.

Pourquoi c'est important ?

Quand vous écrivez ou lisez de gros volumes de données (fichiers de logs, bases, traitement multimédia) — une mise en buffer bien réglée peut accélérer votre programme de plusieurs fois. Sans buffer, même un bon processeur commence à "bâiller" en attendant les données, comme un chat sous la pluie.

4. Exemple simple sans mise en buffer

Regardons d'abord à quoi ressemblerait l'écriture d'un fichier si on écrivait chaque octet séparément (à NE PAS faire !) :

string path = "slowfile.txt";
using (FileStream fs = new FileStream(path, FileMode.Create))
{
    for (int i = 0; i < 100000; i++)
    {
        fs.WriteByte((byte)'A'); // On écrit 1 octet à la fois !
    }
}
Console.WriteLine("C'est fait ! (mais très lentement)");

Dans cet exemple il y a 100 000 accès réels au disque ! Même un SSD se demandera "pourquoi tu me fais ça ?.."

Quelle taille de buffer choisir ?

Ça dépend de votre tâche :

  • Par défaut, .NET utilise souvent 4 KB ou 8 KB pour le buffering interne.
  • Pour de gros fichiers (100 MB et plus) on peut sans problème utiliser des buffers de 16 KB, 64 KB ou même 1 MB.
  • Trop grand buffer — c'est aussi mauvais : mémoire gaspillée et parfois aucun gain.

Règle d'or : mesurez (profiling), n'ayez pas d'intuition ! Parfois augmenter le buffer accélère de 10x, parfois ça n'a quasiment aucun effet.

5. Mise en buffer : accélérer l'I/O

Le mot "buffering" dans le contexte des fichiers est le parent direct du concept "achat en gros". On ne transporte pas les bananes une par une, on prend une caisse entière.

Dans .NET presque tous les streams d'I/O utilisent le buffering par défaut, mais il y a des exceptions : quand vous gérez explicitement un FileStream et ses paramètres, ou quand vous travaillez dans des conditions "non réalistes" (par ex. buffer très petit ou absence de buffer).

Comment le buffering accélère l'I/O ?

Quand vous lisez ou écrivez un gros bloc de données, l'OS peut optimiser le travail : regrouper plusieurs opérations en une, réduire le nombre d'accès au disque, précharger le bloc suivant en mémoire (prefetching).

Illustration : Lecture de fichier — sans buffer et avec buffer

Variante Nb d'accès Temps, approximatif
Lecture par 1 octet 10 000 000 10 minutes
Lecture par 4096 octets 2 500 5 secondes

Estimations approximatives, mais l'ordre de grandeur est impressionnant !

6. FileStream et buffering dans .NET

La classe FileStream est l'outil le plus bas niveau pour travailler avec des fichiers, elle donne un contrôle maximal mais demande de l'attention. Elle a un constructeur qui permet de configurer la taille du buffer :

// FileMode.Open: ouvrir un fichier existant
// FileAccess.Read: lecture
// FileShare.Read: autoriser les autres à lire
// bufferSize: taille du buffer en octets
var fs = new FileStream("bigfile.txt", FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize: 8192)

    // On travaille plus vite avec le fichier

Par défaut FileStream utilise un buffer de 4096 octets, mais vous pouvez choisir une valeur plus grande si le fichier est volumineux (par ex. 16KB, 64KB ou même 1MB).

Conseil : ne mettez pas un buffer trop grand

Si le buffer est énorme, vous consommerez beaucoup de RAM sans gain de vitesse — les OS modernes savent déjà cacher des blocs. Un buffer optimal varie de 4KB à 128KB pour la plupart des tâches "domestiques".

Quand le problème de performance se manifeste-t-il particulièrement ?

  • Lors de la copie d'un grand nombre de petits fichiers (par ex. photos).
  • Lors de la lecture de gros fichiers en petits bouts (1 octet, 1 ligne sans buffering).
  • Lors de l'ouverture simultanée de nombreux fichiers (par ex. quand un script cherche du texte dans tous les logs du disque).
  • Lors du travail avec des partages réseau (latences + surcharge réseau).
  • Dans des opérations massives : archivage, sauvegarde, import/export de données.

7. Copier un fichier « à l'ancienne » et « rapide »

Comparons des approches qui, en pratique, impactent la vitesse du programme.

Très lent :

// ❌ Mauvais — lecture et écriture par un octet
using FileStream source = new FileStream("source.bin", FileMode.Open);
using FileStream dest = new FileStream("dest.bin", FileMode.Create);

int b;
while ((b = source.ReadByte()) != -1)
{
    dest.WriteByte((byte)b);
}

Beaucoup plus rapide :

// ✅ Bien — lecture et écriture en gros blocs
byte[] buffer = new byte[16 * 1024]; // 16 KB
int bytesRead;

using FileStream source = new FileStream("source.bin", FileMode.Open);
using FileStream dest = new FileStream("dest.bin", FileMode.Create);

while ((bytesRead = source.Read(buffer, 0, buffer.Length)) > 0)
{
    dest.Write(buffer, 0, bytesRead);
}

Mega-rapide (et simple) :

// 🚀 File.Copy — utilise en interne une mise en buffer optimisée
File.Copy("source.bin", "dest.bin");

Pourquoi apprendre les blocs alors ? Parce que parfois il faut non seulement copier, mais aussi traiter le contenu à la volée (filtrer des lignes, chiffrer des données, calculer des sommes).

Comparaison des temps d'exécution

Pour rendre l'expérience parlante, voici un tableau (valeurs approximatives, mais illustrant l'ordre de grandeur) :

Méthode Taille fichier 1GB Temps (Approx.)
Par 1 octet 1GB ~30 minutes
Par blocs 4KB 1GB ~20 secondes
File.Copy intégré 1GB ~5 secondes

Ne faites pas ce test sur des fichiers importants ou sur le SSD système — vous risquez d'avoir un "accord de non-agression" entre votre disque et vos nerfs.

8. Détails utiles

D'où viennent encore les ralentissements ?

En plus de la physique du disque et d'un mauvais choix de bloc, il y a d'autres raisons pour lesquelles un programme peut être lent :

  • Ouvrir et fermer des fichiers "à la volée" (mieux vaut ouvrir une fois, travailler, puis fermer).
  • Exécuter I/O sur le thread principal de l'application (ça fige l'UI si vous êtes en Windows Forms/WPF/MAUI).
  • Manque de mémoire : le système commence à "swapper" des pages entre la RAM et le disque — double pénalité.
  • Antivirus, indexeurs Windows, processus en arrière-plan — parfois ils "saisissent" votre fichier et ralentissent tout invisiblement.

Application pratique

Dans un vrai projet : si vous développez un logiciel de traitement de fichiers (logs, médias, documents), un service cloud de stockage, un collecteur de rapports, des backups — vous serez à 100% confronté à la question "comment faire des I/O rapides ?". Utiliser la mise en buffer, de gros blocs et des outils prêts à l'emploi comme File.Copy fait partie des bases de l'efficacité sur les fichiers.

En entretien : on peut vous demander — "Expliquez pourquoi lire un fichier octet par octet est un antipattern ?" Ou comment accélérer une copie massive de fichiers. Avoir de l'expérience et des connaissances sur le buffering vous aide à répondre avec assurance, donner des exemples et proposer des solutions.

Au boulot : il arrive que tout roulait bien, puis ça devienne lent après le passage d'un SSD à un partage réseau, ou après une mise à jour d'OS. En sachant comment fonctionne l'I/O, vous identifierez facilement la cause et proposerez des optimisations.

Comment accélérer l'I/O : conseils pratiques

  • Utilisez toujours l'I/O bufferisé (BufferedStream, configurer le buffer dans FileStream).
  • Lire et écrire en gros blocs (à partir de 4KB et plus).
  • Minimisez le nombre d'ouvertures/fermetures de fichiers — ouvrez une fois, travaillez, puis fermez.
  • Si possible, utilisez les méthodes asynchrones (ReadAsync, WriteAsync) — elles n'accélèrent pas l'I/O en lui-même, mais elles permettent à votre appli de ne pas bloquer pendant l'opération.
  • Si vous traitez des très gros fichiers — explorez Memory<T>, Span<T>.
  • Faites confiance aux fonctions intégrées : File.Copy, File.Move, etc. — sous le capot elles utilisent des appels système optimisés.

Buffering dans les classes .NET

Regardons un petit tableau — qui bufferise comment :

Classe Buffer par défaut Buffer configurable
FileStream
Oui Oui (constructeur)
StreamWriter
Oui Oui (via constructeur)
StreamReader
Oui Oui
BufferedStream
Non (simple wrapper) Oui
BinaryWriter/Reader
Oui Non

Presque rien dans .NET ne fonctionne sans buffer — parce que ce serait inefficace.

Quand forcer le "flush" du buffer

Parfois les données restent dans le buffer et vous voulez qu'elles soient écrites tout de suite sur le disque. Par exemple, vous écrivez un log et l'application plante. Que faire ?

Dans ces cas on appelle la méthode .Flush() :

using var fs = new FileStream("log.txt", FileMode.Append);
using var writer = new StreamWriter(fs);
writer.WriteLine("Quelque chose d'important");
writer.Flush(); // Forcer l'écriture du buffer sur le disque tout de suite

Flush — c'est comme crier "Allez, on range tout dans le dossier, fini la saleté !". Toutes les données non sauvegardées seront effectivement écrites.

9. Questions pratiques : erreurs et subtilités courantes

Une des plus grandes déceptions chez les débutants : "Pourquoi j'ai écrit dans le fichier et il est vide ?!" La raison : les données n'ont pas encore été "flushées" depuis le buffer. Le programme bufferise beaucoup et n'écrit pas toujours immédiatement sur le disque. Évitez ça en appelant Flush() ou en fermant le stream (Dispose()).

Autre problème : vous ouvrez un gros fichier en écriture et vous allouez un buffer gigantesque alors que la RAM est faible — le programme commence à ralentir. Un buffer trop grand n'est pas toujours bon, ne pas en abuser.

2
Mission
C# SELF, niveau 41, leçon 1
Bloqué
Copie de fichier par grands blocs
Copie de fichier par grands blocs
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION