Lettura di file di testo di grandi dimensioni con flussi in C #


96

Ho il bel compito di capire come gestire file di grandi dimensioni caricati nell'editor di script della nostra applicazione (è come VBA per il nostro prodotto interno per macro rapide). La maggior parte dei file ha una dimensione di circa 300-400 KB, il caricamento va bene. Ma quando superano i 100 MB il processo ha difficoltà (come ci si aspetterebbe).

Quello che succede è che il file viene letto e inserito in un RichTextBox che viene quindi esplorato - non preoccuparti troppo di questa parte.

Lo sviluppatore che ha scritto il codice iniziale sta semplicemente usando un StreamReader e sta facendo

[Reader].ReadToEnd()

che potrebbe richiedere un po 'di tempo per essere completato.

Il mio compito è spezzare questo bit di codice, leggerlo in blocchi in un buffer e mostrare una barra di avanzamento con un'opzione per annullarlo.

Alcuni presupposti:

  • La maggior parte dei file sarà di 30-40 MB
  • Il contenuto del file è testo (non binario), alcuni sono in formato Unix, altri DOS.
  • Una volta recuperato il contenuto, determiniamo quale terminatore viene utilizzato.
  • Nessuno si preoccupa una volta caricato il tempo necessario per il rendering nella casella di testo RTF. È solo il caricamento iniziale del testo.

Ora per le domande:

  • Posso semplicemente usare StreamReader, quindi controllare la proprietà Length (quindi ProgressMax) ed emettere una lettura per una dimensione del buffer impostata e scorrere in un ciclo while WHILST all'interno di un worker in background, in modo che non blocchi il thread dell'interfaccia utente principale? Quindi restituisci lo stringbuilder al thread principale una volta completato.
  • Il contenuto andrà a uno StringBuilder. posso inizializzare lo StringBuilder con la dimensione del flusso se la lunghezza è disponibile?

Queste (secondo le tue opinioni professionali) sono buone idee? Ho avuto alcuni problemi in passato con la lettura di contenuti da Streams, perché mancheranno sempre gli ultimi byte o qualcosa del genere, ma farò un'altra domanda se questo è il caso.


29
30-40 MB di file di script? Santo sgombro! Non vorrei dover rivedere il codice che ...
dthorpe

So che questa domanda è piuttosto vecchia, ma l'ho trovata l'altro giorno e ho testato la raccomandazione per MemoryMappedFile e questo è senza dubbio il metodo più veloce. Un confronto è la lettura di un file da 345 MB di linea 7.616.939 tramite un metodo readline che richiede più di 12 ore sulla mia macchina mentre l'esecuzione dello stesso caricamento e la lettura tramite MemoryMappedFile hanno impiegato 3 secondi.
csonon

Sono solo poche righe di codice. Guarda questa libreria che sto usando per leggere anche file da 25 GB e più grandi. github.com/Agenty/FileReader
Vikash Rathee

Risposte:


175

Puoi migliorare la velocità di lettura usando un BufferedStream, come questo:

using (FileStream fs = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
using (BufferedStream bs = new BufferedStream(fs))
using (StreamReader sr = new StreamReader(bs))
{
    string line;
    while ((line = sr.ReadLine()) != null)
    {

    }
}

AGGIORNAMENTO marzo 2013

Recentemente ho scritto codice per la lettura e l'elaborazione (ricerca di testo in) file di testo da 1 GB (molto più grandi dei file coinvolti qui) e ho ottenuto un significativo aumento delle prestazioni utilizzando un modello produttore / consumatore. L'attività del produttore ha letto in righe di testo utilizzando il BufferedStreame le ha passate a un'attività del consumatore separata che ha eseguito la ricerca.

L'ho usato come un'opportunità per imparare TPL Dataflow, che è molto adatto per codificare rapidamente questo modello.

Perché BufferedStream è più veloce

Un buffer è un blocco di byte nella memoria utilizzato per memorizzare nella cache i dati, riducendo così il numero di chiamate al sistema operativo. I buffer migliorano le prestazioni di lettura e scrittura. Un buffer può essere utilizzato per la lettura o la scrittura, ma mai contemporaneamente. I metodi di lettura e scrittura di BufferedStream gestiscono automaticamente il buffer.

AGGIORNAMENTO DI dicembre 2014: il tuo chilometraggio può variare

In base ai commenti, FileStream dovrebbe utilizzare internamente un BufferedStream . Quando questa risposta è stata fornita per la prima volta, ho misurato un significativo aumento delle prestazioni aggiungendo un BufferedStream. All'epoca stavo prendendo di mira .NET 3.x su una piattaforma a 32 bit. Oggi, prendendo di mira .NET 4.5 su una piattaforma a 64 bit, non vedo alcun miglioramento.

Relazionato

Mi sono imbattuto in un caso in cui lo streaming di un file CSV di grandi dimensioni generato nel flusso di risposta da un'azione ASP.Net MVC era molto lento. L'aggiunta di un BufferedStream ha migliorato le prestazioni di 100 volte in questo caso. Per ulteriori informazioni, vedere Output senza buffer molto lento


12
Amico, BufferedStream fa la differenza. +1 :)
Marcus

2
C'è un costo per la richiesta di dati da un sottosistema IO. Nel caso di dischi rotanti, potrebbe essere necessario attendere che il piatto ruoti in posizione per leggere il blocco di dati successivo o, peggio, attendere che la testina del disco si muova. Sebbene gli SSD non abbiano parti meccaniche per rallentare le cose, c'è ancora un costo per operazione di I / O per accedervi. I flussi bufferizzati leggono più di quanto richiesto dallo StreamReader, riducendo il numero di chiamate al sistema operativo e, in definitiva, il numero di richieste di I / O separate.
Eric J.

4
Veramente? Questo non fa differenza nel mio scenario di test. Secondo Brad Abrams non c'è alcun vantaggio nell'usare BufferedStream su un FileStream.
Nick Cox

2
@ NickCox: i risultati possono variare in base al sottosistema IO sottostante. Su un disco rotante e un controller del disco che non ha i dati nella sua cache (e anche i dati non memorizzati nella cache da Windows), la velocità è enorme. La colonna di Brad è stata scritta nel 2004. Recentemente ho misurato miglioramenti effettivi e drastici.
Eric J.

3
Questo è inutile secondo: stackoverflow.com/questions/492283/… FileStream utilizza già un buffer internamente.
Erwin Mayer

21

Se leggi le statistiche sulle prestazioni e sui benchmark su questo sito web , vedrai che il modo più veloce per leggere (perché lettura, scrittura ed elaborazione sono tutti diversi) un file di testo è il seguente frammento di codice:

using (StreamReader sr = File.OpenText(fileName))
{
    string s = String.Empty;
    while ((s = sr.ReadLine()) != null)
    {
        //do your stuff here
    }
}

In tutto, circa 9 metodi diversi sono stati contrassegnati da un banco di prova, ma quello sembra uscire in vantaggio per la maggior parte del tempo, anche eseguendo il lettore bufferizzato come hanno menzionato altri lettori.


2
Questo ha funzionato bene per rimuovere un file postgres da 19 GB per tradurlo in sintassi sql in più file. Grazie ragazzo postgres che non ha mai eseguito correttamente i miei parametri. / sigh
Damon Drake

La differenza di prestazioni qui sembra ripagare per file molto grandi, come più grandi di 150 MB (inoltre dovresti davvero usare a StringBuilderper caricarli in memoria, si carica più velocemente in quanto non crea una nuova stringa ogni volta che aggiungi caratteri)
Joshua G

15

Dici che ti è stato chiesto di mostrare una barra di avanzamento durante il caricamento di un file di grandi dimensioni. È perché gli utenti vogliono veramente vedere l'esatta percentuale di caricamento dei file o semplicemente perché vogliono un feedback visivo che qualcosa sta accadendo?

Se quest'ultimo è vero, la soluzione diventa molto più semplice. Basta farlo reader.ReadToEnd()su un thread in background e visualizzare una barra di avanzamento di tipo marquee invece di una corretta.

Sollevo questo punto perché nella mia esperienza questo è spesso il caso. Quando si scrive un programma di elaborazione dati, gli utenti saranno sicuramente interessati a una cifra% completa, ma per aggiornamenti dell'interfaccia utente semplici ma lenti, è più probabile che vogliano solo sapere che il computer non si è arrestato in modo anomalo. :-)


2
Ma l'utente può annullare la chiamata ReadToEnd?
Tim Scarborough

@ Tim, ben individuato. In tal caso, torniamo al StreamReaderciclo. Tuttavia, sarà ancora più semplice perché non è necessario leggere in anticipo per calcolare l'indicatore di avanzamento.
Christian Hayter

8

Per i file binari, il modo più veloce per leggerli che ho trovato è questo.

 MemoryMappedFile mmf = MemoryMappedFile.CreateFromFile(file);
 MemoryMappedViewStream mms = mmf.CreateViewStream();
 using (BinaryReader b = new BinaryReader(mms))
 {
 }

Nei miei test è centinaia di volte più veloce.


2
Hai qualche prova concreta di questo? Perché OP dovrebbe usarlo su qualsiasi altra risposta? Si prega di scavare un po 'più a fondo e fornire un po' più di dettagli
Dylan Corriveau

7

Utilizza un background worker e leggi solo un numero limitato di righe. Leggi di più solo quando l'utente scorre.

E prova a non usare mai ReadToEnd (). È una delle funzioni che pensi "perché l'hanno fatta?"; è un aiutante di script kiddies che va bene con le piccole cose, ma come vedi, fa schifo per i file di grandi dimensioni ...

Quei ragazzi che ti dicono di usare StringBuilder hanno bisogno di leggere MSDN più spesso:

Considerazioni sulle prestazioni
I metodi Concat e AppendFormat concatenano entrambi i nuovi dati a un oggetto String o StringBuilder esistente. Un'operazione di concatenazione di oggetti String crea sempre un nuovo oggetto dalla stringa esistente e dai nuovi dati. Un oggetto StringBuilder mantiene un buffer per accogliere la concatenazione di nuovi dati. I nuovi dati vengono aggiunti alla fine del buffer se lo spazio è disponibile; in caso contrario, viene allocato un nuovo buffer più grande, i dati dal buffer originale vengono copiati nel nuovo buffer, quindi i nuovi dati vengono aggiunti al nuovo buffer. Le prestazioni di un'operazione di concatenazione per un oggetto String o StringBuilder dipendono dalla frequenza con cui si verifica un'allocazione di memoria.
Un'operazione di concatenazione String alloca sempre memoria, mentre un'operazione di concatenazione StringBuilder alloca memoria solo se il buffer dell'oggetto StringBuilder è troppo piccolo per contenere i nuovi dati. Di conseguenza, la classe String è preferibile per un'operazione di concatenazione se viene concatenato un numero fisso di oggetti String. In tal caso, le singole operazioni di concatenazione potrebbero anche essere combinate in un'unica operazione dal compilatore. Un oggetto StringBuilder è preferibile per un'operazione di concatenazione se viene concatenato un numero arbitrario di stringhe; ad esempio, se un ciclo concatena un numero casuale di stringhe di input dell'utente.

Ciò significa un'enorme allocazione di memoria, che diventa un grande uso del sistema di file di scambio, che simula sezioni del tuo disco rigido in modo che agiscano come la memoria RAM, ma un disco rigido è molto lento.

L'opzione StringBuilder va bene per chi usa il sistema come monoutente, ma quando due o più utenti leggono file di grandi dimensioni contemporaneamente, hai un problema.


voi ragazzi siete super veloci! sfortunatamente a causa del modo in cui funziona la macro, l'intero flusso deve essere caricato. Come ho già detto, non preoccuparti per la parte richtext. È il caricamento iniziale che vogliamo migliorare.
Nicole Lee

così puoi lavorare in più parti, leggere le prime X righe, applicare la macro, leggere le seconde righe X, applicare la macro e così via ... se spieghi cosa fa questa macro, possiamo aiutarti con maggiore precisione
Tufo

5

Questo dovrebbe essere sufficiente per iniziare.

class Program
{        
    static void Main(String[] args)
    {
        const int bufferSize = 1024;

        var sb = new StringBuilder();
        var buffer = new Char[bufferSize];
        var length = 0L;
        var totalRead = 0L;
        var count = bufferSize; 

        using (var sr = new StreamReader(@"C:\Temp\file.txt"))
        {
            length = sr.BaseStream.Length;               
            while (count > 0)
            {                    
                count = sr.Read(buffer, 0, bufferSize);
                sb.Append(buffer, 0, count);
                totalRead += count;
            }                
        }

        Console.ReadKey();
    }
}

4
Vorrei spostare "var buffer = new char [1024]" fuori dal ciclo: non è necessario creare un nuovo buffer ogni volta. Mettilo prima di "while (count> 0)".
Tommy Carlier,

4

Dai un'occhiata al seguente frammento di codice. Hai menzionato Most files will be 30-40 MB. Questo afferma di leggere 180 MB in 1,4 secondi su un Intel Quad Core:

private int _bufferSize = 16384;

private void ReadFile(string filename)
{
    StringBuilder stringBuilder = new StringBuilder();
    FileStream fileStream = new FileStream(filename, FileMode.Open, FileAccess.Read);

    using (StreamReader streamReader = new StreamReader(fileStream))
    {
        char[] fileContents = new char[_bufferSize];
        int charsRead = streamReader.Read(fileContents, 0, _bufferSize);

        // Can't do much with 0 bytes
        if (charsRead == 0)
            throw new Exception("File is 0 bytes");

        while (charsRead > 0)
        {
            stringBuilder.Append(fileContents);
            charsRead = streamReader.Read(fileContents, 0, _bufferSize);
        }
    }
}

Articolo originale


3
Questo tipo di test è notoriamente inaffidabile. Leggerai i dati dalla cache del file system quando ripeti il ​​test. È almeno un ordine di grandezza più veloce di un test reale che legge i dati dal disco. Un file da 180 MB non può richiedere meno di 3 secondi. Riavvia la tua macchina, esegui il test una volta per il numero reale.
Hans Passant

7
la riga stringBuilder.Append è potenzialmente pericolosa, è necessario sostituirla con stringBuilder.Append (fileContents, 0, charsRead); per assicurarti di non aggiungere 1024 caratteri completi anche quando lo streaming è terminato prima.
Johannes Rudolph

@JohannesRudolph, il tuo commento mi ha appena risolto un bug. Come sei arrivato al numero 1024?
HeyJude

3

Potrebbe essere meglio usare la gestione dei file mappati in memoria qui .. Il supporto per i file mappati in memoria sarà disponibile in .NET 4 (penso ... l'ho sentito attraverso qualcun altro che ne parla), quindi questo wrapper che usa p / invoca per fare lo stesso lavoro ..

Modifica: vedi qui su MSDN per come funziona, ecco il post di blog che indica come è fatto nel prossimo .NET 4 quando uscirà come rilascio. Il collegamento che ho fornito in precedenza è un involucro attorno al pinvoke per ottenere ciò. È possibile mappare l'intero file in memoria e visualizzarlo come una finestra scorrevole durante lo scorrimento del file.


2

Tutte ottime risposte! tuttavia, per chi cerca una risposta, queste sembrano essere alquanto incomplete.

Poiché una stringa standard può solo di dimensione X, da 2 Gb a 4 Gb a seconda della configurazione, queste risposte non soddisfano realmente la domanda dell'OP. Un metodo consiste nel lavorare con un elenco di stringhe:

List<string> Words = new List<string>();

using (StreamReader sr = new StreamReader(@"C:\Temp\file.txt"))
{

string line = string.Empty;

while ((line = sr.ReadLine()) != null)
{
    Words.Add(line);
}
}

Alcuni potrebbero voler tokenizzare e dividere la linea durante l'elaborazione. L'elenco delle stringhe ora può contenere volumi di testo molto grandi.


1

Un iteratore potrebbe essere perfetto per questo tipo di lavoro:

public static IEnumerable<int> LoadFileWithProgress(string filename, StringBuilder stringData)
{
    const int charBufferSize = 4096;
    using (FileStream fs = File.OpenRead(filename))
    {
        using (BinaryReader br = new BinaryReader(fs))
        {
            long length = fs.Length;
            int numberOfChunks = Convert.ToInt32((length / charBufferSize)) + 1;
            double iter = 100 / Convert.ToDouble(numberOfChunks);
            double currentIter = 0;
            yield return Convert.ToInt32(currentIter);
            while (true)
            {
                char[] buffer = br.ReadChars(charBufferSize);
                if (buffer.Length == 0) break;
                stringData.Append(buffer);
                currentIter += iter;
                yield return Convert.ToInt32(currentIter);
            }
        }
    }
}

Puoi chiamarlo usando quanto segue:

string filename = "C:\\myfile.txt";
StringBuilder sb = new StringBuilder();
foreach (int progress in LoadFileWithProgress(filename, sb))
{
    // Update your progress counter here!
}
string fileData = sb.ToString();

Quando il file viene caricato, l'iteratore restituirà il numero di avanzamento da 0 a 100, che puoi utilizzare per aggiornare la barra di avanzamento. Una volta terminato il ciclo, StringBuilder conterrà il contenuto del file di testo.

Inoltre, poiché vuoi del testo, possiamo semplicemente usare BinaryReader per leggere i caratteri, il che assicurerà che i tuoi buffer si allineino correttamente durante la lettura di caratteri multibyte ( UTF-8 , UTF-16 , ecc.).

Tutto questo senza utilizzare attività in background, thread o complesse macchine a stati personalizzate.


Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.