1. Introduction
Avant de commencer à utiliser une nouvelle classe, il vaut la peine de comprendre pourquoi elle existe. Voyons ce qui se passe quand on travaille avec un fichier "directement" via FileStream.
Quand vous appelez Read ou Write sur un stream créé avec FileStream, il y a en réalité un accès au sous-système disque de l'ordinateur. Ce processus lui-même (surtout sur les anciens disques durs, mais aussi sur les SSD modernes) est bien plus lent que la RAM. Imaginez que lorsque vous commandez des frites chez McDonalds, le caissier court à chaque fois chercher un nouveau paquet au stock. Imaginez la taille de la file d'attente !
Si vous travaillez avec de petits morceaux de données, des accès fréquents au disque ou au réseau entraînent une perte de performance. Plus le volume de données est grand, plus l'effet est visible.
Analogie rapide
Les streams sans buffer, c'est à peu près comme aller faire les courses 10 fois pour acheter un yaourt à la fois. Un stream bufferisé, c'est quand vous prenez tout un panier de yaourts d'un coup, réduisant le nombre de voyages au minimum.
2. La classe BufferedStream : premier aperçu
À quoi ça sert
BufferedStream est un wrapper autour de n'importe quel stream (Stream) qui maintient un buffer intermédiaire en mémoire. Quand vous écrivez des données, elles vont d'abord dans le buffer, et seulement quand le buffer est plein elles sont vidées sur le disque en une seule opération. Pareil pour la lecture : à la première lecture il charge un gros morceau en mémoire, puis renvoie des petits morceaux depuis la mémoire tant que le buffer n'est pas épuisé.
Exemple de code : création d'un BufferedStream
Faisons un exemple simple. Supposons qu'on doive écrire 100 000 lignes dans un fichier :
string filePath = "big_output.txt";
using var fileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write);
using var bufferedStream = new BufferedStream(fileStream);
using var writer = new StreamWriter(bufferedStream);
for (int i = 0; i < 100_000; i++)
{
writer.WriteLine($"Stroka nomer {i}");
}
Console.WriteLine("Zapic zavershena!");
Commentaire :
- Nous ouvrons le fichier pour écriture via FileStream.
- Puis nous l'enveloppont dans un BufferedStream, et ensuite dans un StreamWriter (qui écrit des lignes de texte dans le stream).
- Dès que le buffer est plein, les données sont vidées sur le disque en une seule fois.
3. Comment fonctionne la bufferisation "de l'intérieur"
Expliquons ça avec un schéma :
[Votre code] → [StreamWriter] → [BufferedStream] → [FileStream] → [Fichier sur disque]
Quand vous appelez la méthode WriteLine() sur StreamWriter, le texte est d'abord écrit dans son buffer interne, puis via BufferedStream dans un autre buffer, et seulement quand le buffer est plein ou que le stream est fermé, les données sont vidées sur le disque.
Combien de bytes dans un "seau" ?
La taille du buffer par défaut est 4096 octets (4 Ko), mais on peut la spécifier explicitement :
int myBufferSize = 16 * 1024; // 16 Ko
using var fileStream = new FileStream(filePath, FileMode.Create);
using var bufferedStream = new BufferedStream(fileStream, myBufferSize);
// ...
Astuce pratique : Sur les systèmes modernes, il est raisonnable d'utiliser des buffers de 8–64 Ko. Pour des opérations sur des fichiers très volumineux, plus grand peut être mieux. Mais ne vous emballez pas : si vous travaillez sur un microcontrôleur avec 128 Ko de RAM, alors un buffer de 64 Ko est une mauvaise idée :)
4. Expérience : comparons la vitesse avec et sans buffer
Pour comprendre l'importance, écrivons un test qui compare l'écriture via FileStream avec et sans buffer :
using System.Diagnostics;
using System.Text;
string data = new string('X', 1000); // 1 000 caractères
void WriteWithoutBuffer()
{
using var fs = new FileStream("no_buffer.txt", FileMode.Create, FileAccess.Write, FileShare.None, 4096, useAsync: false);
for (int i = 0; i < 10_000; i++)
{
byte[] bytes = Encoding.UTF8.GetBytes(data);
fs.Write(bytes, 0, bytes.Length); // Directement sur le fichier – accès disque à chaque fois
}
}
void WriteWithBuffer()
{
using var fs = new FileStream("with_buffer.txt", FileMode.Create, FileAccess.Write, FileShare.None);
using var bs = new BufferedStream(fs, 16 * 1024);
for (int i = 0; i < 10_000; i++)
{
byte[] bytes = Encoding.UTF8.GetBytes(data);
bs.Write(bytes, 0, bytes.Length);
}
}
// Mesure du temps
Stopwatch sw = Stopwatch.StartNew();
WriteWithoutBuffer();
sw.Stop();
Console.WriteLine("Bez bufera: " + sw.ElapsedMilliseconds + " ms");
sw.Restart();
WriteWithBuffer();
sw.Stop();
Console.WriteLine("S buferom: " + sw.ElapsedMilliseconds + " ms");
Sortie attendue :
Dans la plupart des cas avec un buffer vous verrez un gain de vitesse notable ! Surtout si vous avez un HDD. Sur SSD l'effet est aussi présent, mais moins dramatique.
5. Quel buffer choisir ? Comparaison et pratique
En .NET il y a beaucoup de classes impliquées dans la bufferisation. Essayons d'éclaircir :
| Classe | Usage | Buffer intégré ? | Faut-il utiliser BufferedStream ? |
|---|---|---|---|
|
Travail avec fichiers | Oui (4 Ko) | Presque pas nécessaire (mais possible) |
|
Travail réseau | Non | Fortement souhaitable |
|
Lecture/écriture de texte | Oui (à partir de 1 Ko) | Généralement pas nécessaire |
|
Compression/décompression | Non | Possible/recommandé pour accélérer |
Important :
FileStream avec le paramètre constructeur bufferSize est déjà en pratique un stream bufferisé. Si vous avez spécifié vous-même un buffer assez grand, ajouter un BufferedStream supplémentaire n'apportera pas beaucoup. Mais si vous utilisez un autre stream (par exemple réseau), alors BufferedStream est votre ami.
6. Exemple : copie de fichier avec BufferedStream
string source = "big_input.dat";
string dest = "big_output.dat";
int bufferSize = 64 * 1024; // 64 Ko
using var inputStream = new FileStream(source, FileMode.Open, FileAccess.Read);
using var outputStream = new FileStream(dest, FileMode.Create, FileAccess.Write);
using var bufferedInput = new BufferedStream(inputStream, bufferSize);
using var bufferedOutput = new BufferedStream(outputStream, bufferSize);
byte[] buffer = new byte[bufferSize];
int bytesRead;
while ((bytesRead = bufferedInput.Read(buffer, 0, buffer.Length)) > 0)
{
bufferedOutput.Write(buffer, 0, bytesRead);
}
// N'oubliez pas le flush – sinon les derniers octets ne seront pas écrits sur le disque !
bufferedOutput.Flush();
Console.WriteLine("Kopirovanie zaversheno!");
Commentaire :
- On lit des blocs larges (64 Ko) depuis un fichier via BufferedStream.
- On les écrit dans un autre fichier, en utilisant aussi un buffer.
- Après la fin de la boucle, appelez obligatoirement Flush() pour écrire les dernières données.
7. Nuances utiles
Astuce : quand BufferedStream est vraiment utile
- Si vous travaillez avec des streams qui n'ont pas de buffer (par exemple des streams réseau, ou des héritages custom de Stream);
- Si vous traitez de gros volumes de données binaires (par exemple copie de fichiers, conversion de formats, sauvegarde);
- Si vous optimisez du code existant et voyez que le goulet d'étranglement est un grand nombre de petites opérations Write/Read.
Un mot sur l'asynchronie et la bufferisation
Avec l'arrivée des opérations asynchrones (ReadAsync/WriteAsync), la bufferisation reste utile, mais gardez en tête : si vous utilisez des méthodes asynchrones au-dessus d'un buffer, le traitement reste en mémoire et l'interaction physique avec le disque est encore plus réduite.
Dans .NET 8+ et .NET 9 la bufferisation est intégrée de plus en plus profondément, et la plupart des classes ont déjà des buffers par défaut. Mais pour la compatibilité avec les streams réseau ou vos propres implémentations, il reste pertinent d'utiliser manuellement BufferedStream.
Vous en apprendrez plus sur l'asynchronie au niveau 58 :P
Schéma visuel de fonctionnement des streams bufferisés
flowchart LR
A[Votre code] --> B[StreamReader/Writer]
B --> C[BufferedStream]
C --> D[FileStream]
D --> E[Fichier/Appareil]
- A — Votre code qui appelle Write/Read.
- B — Stream de haut niveau (travaille avec du texte ou des données).
- C — Bufferisation (regroupe les données pour augmenter la vitesse).
- D — Implémentation concrète du stream (fichier, réseau).
- E — Dispositif physique (disque dur, SSD, réseau, etc.).
Conseils et trucs pratiques
- Si vous écrivez une ligne à la fois dans un fichier (par exemple du logging), il vaut mieux définir explicitement une taille de buffer supérieure à la taille d'une ligne. Ça permettra de vider de plus gros paquets de données plus rapidement.
- Si chaque action doit être instantanément écrite (par exemple des logs critiques), appelez Flush() après chaque écriture. Mais ça réduit l'avantage de la bufferisation !
- Si vous créez des fichiers temporaires qui sont supprimés juste après création, il se peut que ce ne soit pas critique si quelque chose reste dans le buffer — mais faites attention si c'est important que le fichier soit bien écrit.
- Quand vous travaillez avec des fichiers très volumineux (par ex. dizaines de gigaoctets), n'hésitez pas à augmenter la taille du buffer jusqu'à 1_048_576 octets (1 Mo) ou plus — l'essentiel est d'avoir assez de RAM.
8. Erreurs typiques et subtilités d'utilisation
Si vous sentez l'envie de "mettre un buffer partout" — ne vous précipitez pas. Tout est bon avec modération !
Une erreur fréquente est d'oublier d'appeler Flush() ou de fermer le stream quand il le faut. Si le stream n'est pas fermé et que le programme se termine brutalement, les derniers octets peuvent rester dans le buffer en RAM et ne pas être écrits sur le disque. Par exemple, si vous écrivez des logs et que le programme plante, la dernière écriture peut disparaître.
BufferedStream en lui-même ne "voit" pas la fin de vos messages logiques — il attend juste qu'une portion suffisante de données s'accumule. Donc pour les choses critiques (logging, backups, etc.) mieux appeler périodiquement Flush() :
bufferedStream.Flush(); // Force le buffer à vider les données sur le disque
Si vous utilisez StreamWriter, il a son propre buffer ! Donc en usage imbriqué la bufferisation peut se faire deux fois (et ce n'est pas toujours souhaitable). Souvent, un seul niveau de buffer suffit, et si vous utilisez StreamWriter, un BufferedStream supplémentaire peut être superflu.
GO TO FULL VERSION