1. Introduzione
Quando lavori con file piccoli, spesso non ti accorgi di nulla: scrivi, leggi — e dimentichi. Ma non appena la dimensione del file comincia a superare anche solo 100–500 MB, e ancor più i gigabyte, emergono effetti interessanti:
- Le operazioni diventano più lente, soprattutto se lavori "alla vecchia": ad esempio, File.ReadAllBytes() o File.WriteAllText() improvvisamente possono rallentare l'intero programma.
- La memoria RAM potrebbe non bastare, appare OutOfMemoryException.
- Il sistema comincia a usare lo swap, il che può rallentare tutto il sistema.
- Operazioni parallele possono generare carico eccessivo sul disco.
Nelle attività reali questo succede spesso:
- Log dei server (gigabyte al giorno).
- Elaborazione di grandi file CSV o XML per import/export.
- Lavoro con video, audio, archivi, file binari.
- Copia di grandi assembly o backup.
2. Strategie di ottimizzazione per file grandi
Prima di lanciarsi nell'ottimizzazione — definiamo cosa vogliamo velocizzare o migliorare. Ecco i compiti più comuni:
- "Leggere/scrivere il file il più velocemente possibile senza uccidere il sistema".
- "Elaborare il file a pezzi per non caricare tutta la memoria".
- "Non creare copie inutili dei dati in memoria".
- "Elaborare file grandi in parallelo (se possibile)".
Approcci generali:
- Lettura e scrittura stream-based: usa stream e buffer, leggi/scrivi a pezzi (FileStream, BufferedStream).
- Operazioni sui file direttamente su disco — senza copie intermedie in memoria.
- Gestione parsimoniosa di buffer e memoria: non tenere l'intero file in RAM.
- Operazioni asincrone — se non vuoi bloccare il thread principale (più dettagli nella lezione successiva).
3. Lettura e scrittura stream-based: pattern base
Principio principale: Lavora con i file a pezzi! In C# moderno è facile con classi come FileStream, BufferedStream, e più in generale usando gli stream.
// Apriamo lo stream per la lettura:
using FileStream fs = new FileStream("bigfile.bin", FileMode.Open, FileAccess.Read);
byte[] buffer = new byte[1024 * 1024]; // 1 MB
int bytesRead;
// Leggiamo finché non arriviamo alla fine del file
while ((bytesRead = fs.Read(buffer, 0, buffer.Length)) > 0)
{
// Qui elaboriamo i dati letti!
// Ad esempio, contiamo la somma di tutti i byte (solo come esercizio)
long sum = 0;
for (int i = 0; i < bytesRead; i++)
sum += buffer[i];
Console.WriteLine($"Letti {bytesRead} byte, somma: {sum}");
}
Consiglio: La dimensione del buffer (per esempio 64 KB, 128 KB, 1 MB) va scelta sperimentalmente. Troppo piccolo — ci saranno molte chiamate al disco. Troppo grande — non sempre migliora le prestazioni e consuma più memoria.
Perché così? La lettura classica con File.ReadAllBytes() o File.ReadAllText() senza suddividere tenta di caricare tutto in memoria. Se il file è enorme — il risultato è ovvio: OutOfMemoryException garantita.
4. BufferedStream: perché e quando serve
Abbiamo già visto questa classe in una lezione precedente, ma è utile ricordare: BufferedStream è un wrapper attorno a qualsiasi altro stream che permette di leggere/scrivere non byte singoli ma blocchi.
Esempio d'uso:
using var fileStream = new FileStream("bigfile.bin", FileMode.Open, FileAccess.Read);
using var bufferedStream = new BufferedStream(fileStream, 1024 * 128);
byte[] buffer = new byte[1024 * 128];
int bytesRead;
while ((bytesRead = bufferedStream.Read(buffer, 0, buffer.Length)) > 0)
{
// Elaborazione dei dati
}
In alcuni casi l'uso di BufferedStream dà un'accelerazione, specialmente se leggi il file pochi byte alla volta e il file system preferisce accessi a blocchi.
Curiosità: In versioni recenti di .NET FileStream ha già bufferizzazione integrata, quindi l'uso manuale di BufferedStream porta il maggior beneficio con stream "raw", per esempio di rete o device non standard.
5. Lettura e scrittura di grandi file di testo
Con i file binari è chiaro: leggi e scrivi a blocchi. Ma cosa fare con i file di testo grandi, come CSV, log, JSON?
Qui vengono in soccorso StreamReader per la lettura e StreamWriter per la scrittura.
Lettura riga per riga:
using var reader = new StreamReader("biglog.txt");
string? line;
while ((line = reader.ReadLine()) != null)
{
// Elaborazione della riga
if (line.Contains("ERROR"))
Console.WriteLine("Rilevato ERROR: " + line);
}
Perché è una buona idea?
- Non teniamo tutto il file in memoria.
- La bufferizzazione interna di StreamReader è già ottimizzata.
Scrittura per righe:
using var writer = new StreamWriter("output.txt");
for (int i = 0; i < 1000000; i++)
writer.WriteLine($"Questa è la riga numero {i}");
Architettura della lettura stream-based
[File sul disco]
|
[FileStream]
|
[BufferedStream (opzionale)]
|
[StreamReader/StreamWriter (per testo)]
|
[Il tuo codice: elaborazione dati]
6. Mettiamolo in pratica
Continuiamo a sviluppare la nostra applicazione per log. Supponiamo che l'archiviazione dei log vecchi (più vecchi di 7 giorni) non sia solo uno spostamento, ma la compressione in un unico archivio. Se il file è enorme — leggiamo e scriviamo a pezzi per non "seppellire" la memoria.
Per semplicità usiamo una copia standard tramite stream:
void CopyLargeFile(string sourcePath, string destPath)
{
using var sourceStream = new FileStream(sourcePath, FileMode.Open, FileAccess.Read);
using var destStream = new FileStream(destPath, FileMode.Create, FileAccess.Write);
byte[] buffer = new byte[1024 * 256]; // 256 KB
int bytesRead;
while ((bytesRead = sourceStream.Read(buffer, 0, buffer.Length)) > 0)
{
destStream.Write(buffer, 0, bytesRead);
// Possiamo aggiungere una barra di progresso qui!
}
}
Dove si usa?
- Copia dei backup
- Unione di log per giorno o mese
- Pre-elaborazione dei file (per esempio filtraggio di righe)
7. Come valutare l'efficacia del buffer?
A volte vuoi sapere: "Quanto è diventato più veloce?" Puoi misurarlo con il tempo di esecuzione:
var watch = System.Diagnostics.Stopwatch.StartNew();
CopyLargeFile("source.bin", "dest.bin");
watch.Stop();
Console.WriteLine($"Tempo di copia: {watch.Elapsed.TotalSeconds} secondi");
Valori tipici per il buffer:
- 4 KB — blocco minimo del file system.
- 64 KB/128 KB — nella pratica funzionano bene quasi sempre.
- 1 MB e oltre — giustificato solo su SSD molto veloci e file molto grandi.
Prova tu stesso diverse dimensioni del buffer!
8. Ricerca ed elaborazione dati in file grandi
Se devi non solo copiare, ma cercare una riga, un numero, una frase? Nei file grandi è più efficiente farlo a porzioni.
Esempio: trovare tutte le righe con errori in un log enorme
using var reader = new StreamReader("server.log");
using var writer = new StreamWriter("errors.txt");
string? line;
while ((line = reader.ReadLine()) != null)
{
if (line.Contains("ERROR"))
writer.WriteLine(line); // Scriviamo solo le righe utili nel risultato
}
Questo approccio permette di processare log da gigabyte in su senza consumare un sacco di memoria.
9. Particolarità per file enormi (>2 GB)
.NET e Windows lavorano bene con file di qualsiasi dimensione (anche decine di terabyte) se usi la lettura stream-based. Ma ci sono dettagli!
- Applicazioni a 32 bit sono limitate a 2 GB di indirizzamento — usa x64!
- Per file grandi usa sempre piattaforma a 64 bit (AnyCPU o x64).
- Per file più grandi di 4 GB il file system FAT32 non va bene — serve NTFS/exFAT.
Elaborazione iterativa di file grandi
+------------------+
| Start |
+------------------+
|
v
+------------------------------+
| Apri lo stream per la lettura|
+------------------------------+
|
v
+------------------------------+
| Finché non raggiungi EOF |
+------------------------------+
|
v
+---------------------------+
| Leggi un blocco di dati |
+---------------------------+
|
v
+---------------------------+
| Elabora il blocco |
+---------------------------+
|
v
+--------------------------+
| Blocca successivo |
+--------------------------+
|
v
+--------------------+
| Chiudi lo stream |
+--------------------+
10. Dettagli utili
File come stream: metodi chiave di FileStream
| Metodo | Descrizione |
|---|---|
|
Legge una parte del file nel buffer |
|
Scrive una parte del buffer nel file |
|
Permette di saltare alla posizione desiderata nel file |
|
Dimensione del file in byte |
|
Posizione corrente nel file |
Esempio d'uso di Seek():
using var stream = new FileStream("bigfile.bin", FileMode.Open);
// Spostiamoci in avanti di 1 GB!
stream.Seek(1024L * 1024 * 1024, SeekOrigin.Begin);
byte[] buffer = new byte[1024];
int bytesRead = stream.Read(buffer, 0, buffer.Length);
// Ora leggiamo dati dal mezzo del file!
Dove può servire?
- Indicizzazione dei file
- Accesso rapido a blocchi specifici (per es. in grandi DB)
Lavorare con file in più thread (multithreading)
Se il problema lo permette, puoi processare file grandi in parallelo: ad esempio dividere il file in blocchi e leggere/scrivere parti diverse contemporaneamente. Ma tieni presente che gli HDD lavorano male con accessi casuali, mentre gli SSD sono molto più veloci.
Per compiti domestici, se non sei sicuro, lavora sequenzialmente. Il parallelismo è utile quando processi più file contemporaneamente, non quasi mai per un singolo file (altrimenti il guadagno è dubbio).
11. Errori tipici lavorando con file grandi
Errore №1: leggere tutto il file in memoria.
I principianti spesso usano File.ReadAllBytes() o File.ReadAllText() anche per file enormi. Se il file pesa gigabyte, l'applicazione termina con errore — memoria insufficiente. Usa lettura stream-based.
Errore №2: usare un buffer troppo piccolo.
Leggere con un buffer minuscolo è come bere la zuppa con un cucchiaino. Il programma passerà molto tempo in attesa di I/O invece di lavorare. Scegli dimensioni di buffer ragionevoli.
Errore №3: dimenticare di chiudere lo stream.
Se non chiudi lo stream dopo l'uso, il file descriptor rimane occupato. Questo impedisce ad altri programmi di accedere al file e può causare errori a livello OS. Usa sempre using — è più sicuro e pulito.
Errore №4: accesso simultaneo allo stesso file.
Cercare di leggere e scrivere lo stesso file da parti diverse del programma è una via sicura verso IOException. Anche se a volte "funziona", non otterrai stabilità.
GO TO FULL VERSION