1. Introduzione
Prima di cominciare a usare una nuova classe, vale la pena capire perché esiste. Vediamo cosa succede quando lavoriamo con un file "direttamente" usando FileStream.
Quando chiami Read o Write su uno stream creato con FileStream, in realtà stai facendo una chiamata alla sottosistema disco del computer. Questo processo (soprattutto su vecchi hard disk, ma anche su SSD) è molto più lento rispetto alla RAM. Immagina che quando ordini le patatine al McDonalds, il cassiere corra ogni volta in magazzino a prendere un nuovo sacchetto. Immagina quanto sarebbe lunga la coda!
Se lavori con piccoli pezzi di dati, le frequenti chiamate al disco o alla rete portano a perdita di prestazioni. Più grande è il volume dei dati, più evidente diventa l'effetto.
Analogia rapida
Quindi, gli stream senza buffer sono come fare la spesa 10 volte per comprare uno yogurt alla volta. Uno stream bufferizzato è quando prendi direttamente un intero cestino di yogurt, riducendo il numero di viaggi al minimo.
2. La classe BufferedStream: uno sguardo iniziale
A cosa serve
BufferedStream è un wrapper attorno a qualsiasi stream (Stream) che mantiene un buffer intermedio in memoria. Quando scrivi dati, vanno prima nel buffer e solo quando il buffer è pieno vengono scaricati su disco in un'unica grande operazione. Lo stesso vale per la lettura: alla prima lettura carica un blocco significativo in memoria e poi restituisce pezzetti dalla memoria finché il buffer non si esaurisce.
Esempio di codice: creare un BufferedStream
Facciamo un esempio semplice. Supponiamo di voler scrivere 100.000 righe in un file:
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($"Striga numero {i}");
}
Console.WriteLine("Scrittura completata!");
Commento:
- Apriamo il file per scrittura tramite FileStream.
- Poi lo avvolgiamo in BufferedStream, e dopo in StreamWriter (che scrive righe di testo nello stream).
- Non appena il buffer si riempie, i dati vengono scaricati sul disco in un'unica operazione.
3. Come funziona la bufferizzazione "da dentro"
Spieghiamolo con uno schema:
[Il tuo codice] → [StreamWriter] → [BufferedStream] → [FileStream] → [File sul disco]
Quando chiami il metodo WriteLine() su StreamWriter, il testo viene prima scritto nel buffer interno, poi passa tramite BufferedStream — in un altro buffer — e solo quando il buffer è pieno o lo stream viene chiuso, i dati vengono scaricati sul disco.
Quanti byte in un "secchio"?
La dimensione del buffer di default è 4096 byte (4 KB), ma puoi specificarla esplicitamente:
int myBufferSize = 16 * 1024; // 16 KB
using var fileStream = new FileStream(filePath, FileMode.Create);
using var bufferedStream = new BufferedStream(fileStream, myBufferSize);
// ...
Pratica raccomandazione: Sui sistemi moderni ha senso usare buffer tra 8–64 KB. Per operazioni su file molto grandi anche di più. Ma non esagerare: se lavori su un microcontrollore con 128 KB di RAM, allora un buffer di 64 KB potrebbe essere una cattiva idea :)
4. Esperimento: confrontiamo la velocità con e senza buffer
Per capire quanto sia importante, scriviamo un test che confronta la scrittura tramite FileStream con e senza buffer:
using System.Diagnostics;
using System.Text;
string data = new string('X', 1000); // 1 000 caratteri
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); // Diretto su file – ogni volta chiamata al disco
}
}
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);
}
}
// Cronometriamo
Stopwatch sw = Stopwatch.StartNew();
WriteWithoutBuffer();
sw.Stop();
Console.WriteLine("Senza buffer: " + sw.ElapsedMilliseconds + " ms");
sw.Restart();
WriteWithBuffer();
sw.Stop();
Console.WriteLine("Con buffer: " + sw.ElapsedMilliseconds + " ms");
Output atteso:
Nella maggior parte dei casi con il buffer vedrai un incremento di velocità significativo! Soprattutto su HDD. Su SSD l'effetto c'è comunque ma meno drammatico.
5. Che buffer scegliere? Confronto e pratica
In .NET ci sono molte classi per la bufferizzazione. Facciamo un po' di chiarezza:
| Classe | Scopo | Buffer integrato? | Serve usare BufferedStream? |
|---|---|---|---|
|
Lavoro con file | Sì (da 4 KB) | Quasi mai necessario (ma possibile) |
|
Lavoro in rete | No | Altamente consigliato |
|
Lettura/scrittura di testo | Sì (da 1 KB) | Di solito non necessario |
|
Compressione/decompressione | No | Può/vale la pena per velocizzare |
Importante:
FileStream con il parametro del costruttore bufferSize è di fatto già uno stream bufferizzato. Se hai tu specificato un buffer sufficientemente grande, un secondo BufferedStream non darà molto vantaggio. Ma se usi un altro tipo di stream (per esempio uno network), allora BufferedStream è il tuo alleato.
6. Esempio: Copiare un file con BufferedStream
string source = "big_input.dat";
string dest = "big_output.dat";
int bufferSize = 64 * 1024; // 64 KB
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);
}
// Non dimenticare il flush – altrimenti gli ultimi byte potrebbero non finire su disco!
bufferedOutput.Flush();
Console.WriteLine("Copia completata!");
Commento:
- Leggiamo dati a blocchi grandi (64 KB) da un file tramite BufferedStream.
- Li scriviamo in un altro file, usando anche qui il buffer.
- Dopo il ciclo ricordati di chiamare Flush() per assicurare la scrittura degli ultimi dati.
7. Dettagli utili
Consiglio: quando davvero serve BufferedStream
- Se lavori con stream che non hanno buffer (per esempio NetworkStream, o implementazioni custom di Stream);
- Se lavori con grandi volumi di dati binari (per esempio copia di file, conversione formati, backup);
- Se stai ottimizzando codice esistente e vedi che il collo di bottiglia sono molte piccole operazioni di Write/Read.
Un po' su async e bufferizzazione
Con l'arrivo delle operazioni asincrone (ReadAsync/WriteAsync) la bufferizzazione resta utile, ma tieni presente: se usi metodi asincroni sopra un buffer, l'elaborazione avviene comunque in memoria e l'interazione fisica con il disco viene ulteriormente ridotta.
In .NET 8+ e .NET 9 la bufferizzazione è sempre più integrata, e la maggior parte delle classi ha già buffer di default. Però per compatibilità con stream di rete o implementazioni custom conviene ancora usare manualmente BufferedStream.
Di async parleremo più a fondo nel livello 58 :P
Schema visivo del funzionamento degli stream bufferizzati
flowchart LR
A[Il tuo codice] --> B[StreamReader/Writer]
B --> C[BufferedStream]
C --> D[FileStream]
D --> E[File/Dispositivo]
- A — Il tuo codice che chiama Write/Read.
- B — Stream di alto livello (manipola testo o dati).
- C — Bufferizzazione (raggruppa dati per migliorare la velocità).
- D — Implementazione concreta dello stream (file, rete).
- E — Dispositivo fisico (HDD, SSD, rete, ecc.).
Consigli pratici e trucchi
- Se scrivi una riga alla volta in un file (per esempio logging), è meglio specificare un buffer più grande della dimensione di una riga. Così i dati verranno scaricati in batch più grandi e più velocemente.
- Se ogni azione deve essere immediatamente persistita (es. log critici), chiama Flush() dopo ogni scrittura. Ma questo riduce i benefici della bufferizzazione!
- Se crei file temporanei che vengono subito cancellati, magari non importa se qualcosa rimane nel buffer — però fai attenzione se è importante che il file sia effettivamente scritto.
- Per file molto grandi (decine di gigabyte) non avere timore di aumentare il buffer fino a 1_048_576 byte (1 MB) o più — l'importante è avere RAM sufficiente.
8. Errori tipici e dettagli d'uso
Se adesso ti è venuta voglia di "mettere buffer ovunque" — non correre. Tutto con moderazione!
Un errore comune è dimenticare di chiamare Flush() o di chiudere lo stream quando serve. Se lo stream non è chiuso e il programma termina in modo anomalo, gli ultimi byte potrebbero restare nel buffer in RAM e non finire su disco. Per esempio, se stai loggando su file e il programma crasha, l'ultima riga potrebbe sparire.
BufferedStream di per sé non "capisce" la fine dei tuoi messaggi logici — aspetta solo che si accumuli la quantità di dati necessaria. Quindi per cose critiche (log, backup ecc.) è meglio chiamare periodicamente Flush():
bufferedStream.Flush(); // Forza lo svuotamento del buffer su disco
Se usi StreamWriter, ricordati che anche lui ha un buffer! Quindi con buffer annidati si fa doppia bufferizzazione (e non sempre è desiderabile). Spesso basta un solo livello di buffer, e se stai usando StreamWriter, un ulteriore BufferedStream potrebbe non essere necessario.
GO TO FULL VERSION