2010-01-29 10 views
70

Ho il piacevole compito di elaborare come gestire i file di grandi dimensioni caricati nell'editor di script della nostra applicazione (è come VBA per il nostro prodotto interno per macro veloci). La maggior parte dei file sono circa 300-400   KB, che è un buon caricamento. Ma quando vanno oltre 100   MB il processo ha difficoltà (come ci si aspetterebbe).Lettura di file di testo di grandi dimensioni con flussi in C#

Quello che succede è che il file viene letto e spostato in un RichTextBox che viene poi navigato - non preoccuparti troppo di questa parte.

Lo sviluppatore che ha scritto il codice iniziale è semplicemente utilizzando uno StreamReader e facendo

[Reader].ReadToEnd() 

che potrebbe richiedere molto tempo per completare.

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

Alcune ipotesi:

  • maggior parte dei file saranno 30-40   MB
  • Il contenuto del file è di testo (non binaria), alcuni sono formato Unix, alcuni sono DOS.
  • Una volta recuperato il contenuto, stabiliamo quale terminatore è utilizzato.
  • Nessuno è interessato dopo aver caricato il tempo necessario per eseguire il rendering in richtextbox. È solo il caricamento iniziale del testo.

Ora per le domande:

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

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

+26

30-40MB file di script? Santo sgombro! Odio dover rivedere il codice ... – dthorpe

+0

Sono solo poche righe di codice. Vedi questa libreria che sto usando per leggere anche 25gb e più file di grandi dimensioni. https://github.com/Agenty/FileReader/ – Vicky

risposta

6

Utilizzare un worker in background e leggere solo un numero limitato di righe. Leggi di più solo quando l'utente scorre.

E provare a non utilizzare mai ReadToEnd(). È una delle funzioni che pensi "perché l'hanno fatto?"; si tratta di un aiutante script kiddies' che va bene con le piccole cose, ma come vedi, mi fa schifo per file di grandi dimensioni ...

Quei ragazzi che ti dice di usare StringBuilder necessità di leggere il 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 stringa crea sempre un nuovo oggetto dalla stringa esistente e dai nuovi dati.Un oggetto StringBuilder mantiene un buffer per ospitare la concatenazione di nuovi dati. I nuovi dati vengono aggiunti alla fine del buffer se la stanza è 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 delle stringhe alloca sempre la memoria, mentre un'operazione di concatenazione StringBuilder assegna solo memoria se il buffer dell'oggetto StringBuilder è troppo piccolo per ospitare i nuovi dati. Di conseguenza, la classe String è preferibile per un'operazione di concatenazione se un numero fisso di oggetti String viene concatenato. 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 un numero arbitrario di stringhe è concatenato; ad esempio, se un loop concatena un numero casuale di stringhe di input dell'utente.

Ciò significa enorme dotazione di memoria, ciò che diventa grande uso del sistema di file di swap, che simula le sezioni del disco rigido di agire come la memoria RAM, ma un disco rigido è molto lenta.

L'opzione StringBuilder è perfetta per chi utilizza il sistema come utente mono, ma quando si hanno due o più utenti che leggono file di grandi dimensioni allo stesso tempo, si è verificato un problema.

+0

lontano voi ragazzi siete super veloci! purtroppo a causa del modo in cui la macro funziona, è necessario caricare l'intero stream. Come ho detto, non preoccuparti della parte del testo. È il caricamento iniziale che vogliamo migliorare. –

+0

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

2

Si potrebbe essere meglio utilizzare i file mappati in memoria di movimentazione here .. Il supporto di file di memoria mappata sarà in giro in .NET 4 (credo ... ho sentito che attraverso qualcun altro parlarne), quindi questa involucro che usa p/invoca per fare lo stesso lavoro ..

Edit: Vedi qui sul MSDN per come funziona, ecco la voce di blog che indica come è fatto nel prossimo .NET 4 quando esce come rilascio. Il collegamento che ho dato in precedenza è un wrapper attorno al pinvoke per raggiungere questo obiettivo. È possibile mappare l'intero file in memoria e visualizzarlo come una finestra scorrevole durante lo scorrimento del file.

4

Dai un'occhiata al seguente frammento di codice. Hai menzionato Most files will be 30-40 MB. Questo sostiene di lettura di 180 MB   in 1,4 secondi su un processore 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); 
     } 
    } 
} 

Original Article

+3

Questi tipi di test sono notoriamente inaffidabili. Leggere i dati dalla cache del file system quando si ripete il test. Questo è almeno un ordine di grandezza più veloce di un vero test che legge i dati dal disco. Un file da 180 MB non può richiedere meno di 3 secondi. Riavviare la macchina, eseguire il test una volta per il numero reale. –

+6

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 stream è terminato in precedenza. –

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(); 
    } 
} 
+3

Sposterei "var buffer = new char [1024]" dal ciclo: non è necessario creare un nuovo buffer ogni volta. Basta metterlo prima di "while (count> 0)". –

14

Si dice che è stato chiesto di mostrare una barra di avanzamento durante il caricamento di un file di grandi dimensioni. È perché gli utenti vogliono davvero vedere la percentuale esatta di caricamento del file, o solo perché vogliono un riscontro visivo che qualcosa stia accadendo?

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

Io sollevo questo punto perché nella mia esperienza questo è spesso il caso. Quando stai scrivendo un programma di elaborazione dati, gli utenti saranno sicuramente interessati a una figura% completa, ma per gli aggiornamenti dell'interfaccia utente semplici ma lenti, è più probabile che vogliano sapere che il computer non si è bloccato.:-)

+2

Ma l'utente può annullare la chiamata ReadToEnd? –

+0

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

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); 
      } 
     } 
    } 
} 

si può chiamare usando la seguente:

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(); 

Mentre il file viene caricato, l'iteratore tornerà il numero progressivo 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é si desidera il testo, è sufficiente utilizzare BinaryReader per leggere i caratteri, in modo che i buffer si allineano correttamente durante la lettura di caratteri multibyte (UTF-8, , ecc.).

Tutto ciò senza l'utilizzo di attività in background, thread o complesse macchine a stati personalizzati.

142

È possibile migliorare la velocità di lettura utilizzando 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) 
    { 

    } 
} 

marzo 2013 AGGIORNAMENTO

Recentemente ho scritto il codice per la lettura e l'elaborazione (ricerca di testo in) 1   GB -i file di testo (molto più grandi dei file coinvolti qui) e hanno ottenuto un significativo aumento delle prestazioni utilizzando uno schema produttore/consumatore. L'attività del produttore ha letto righe di testo utilizzando lo BufferedStream e le ha trasferite a un'attività utente separata che ha effettuato 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 in memoria utilizzati per i dati di cache, riducendo così il numero di chiamate al sistema operativo. I buffer migliorano le prestazioni di lettura e scrittura. Un buffer può essere usato per leggere o scrivere, ma mai entrambi contemporaneamente. I metodi di lettura e scrittura di BufferedStream mantengono automaticamente il buffer.

dicembre 2014 UPDATE: Il tuo situazione potrebbe essere diversa

Sulla base delle osservazioni, FileStream deve usare un BufferedStream internamente. Al momento in cui questa risposta è stata fornita per la prima volta, ho misurato un significativo incremento delle prestazioni aggiungendo un BufferedStream. In quel momento stavo prendendo di mira .NET 3.x su una piattaforma a 32 bit. Oggi, con il targeting .NET 4.5 su una piattaforma a 64 bit, non vedo alcun miglioramento.

correlati

mi sono imbattuto in un caso in cui lo streaming di un grande, generato file CSV per il flusso di risposta da un'azione ASP.Net MVC era molto lento. Aggiunta di un BufferedStream miglioramento delle prestazioni di 100x in questa istanza.Per ulteriori vedere Unbuffered Output Very Slow

+10

Dude, BufferedStream fa la differenza. +1 :) – Marcus

+0

Molto più veloce di streamReader.ReadLine solo ... grazie mille Eric. Puoi anche spiegare perché è molto più veloce/o indicarmi risorse dove posso leggere a riguardo. Grazie in anticipo. – techExplorer

+1

C'è un costo per richiedere dati da un sottosistema di I/O.Nel caso di dischi rotanti, potrebbe essere necessario attendere che il piatto ruoti in posizione per leggere il prossimo pezzo di dati o, peggio, attendere che la testina del disco si muova. Mentre gli SSD non hanno parti meccaniche per rallentare, c'è ancora un costo operativo per-IO per accedervi. I flussi bufferizzati non si limitano a leggere le richieste di StreamReader, riducendo il numero di chiamate al sistema operativo e, infine, il numero di richieste IO separate. –

12

Se leggete il performance and benchmark stats on this website, vedrai che il modo più veloce per leggere (perché la lettura, la scrittura, e la lavorazione sono tutte diverse) 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 
    } 
} 

Tutti su circa 9 diversi metodi erano panchina ha segnato, ma che si sembrano uscire in anticipo la maggior parte del tempo, anche al di fuori eseguendo il lettore tamponata come altri lettori hanno menzionato.

+1

Questo ha funzionato bene per separare un file postgres 19GB per tradurlo in sql sintassi in più file. Grazie a postgres che non ha mai eseguito correttamente i miei parametri./sigh –

+0

La differenza di prestazioni qui sembra ripagare per file molto grandi, come i maggiori di 150 MB (inoltre dovresti davvero usare un 'StringBuilder' per caricarli in memoria, caricarli più velocemente dato che non crea una nuova stringa ogni volta che aggiungi caratteri) – b729sefc

7

Per i file binari, il modo più veloce di 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.

+0

Hai qualche prova concreta di questo? Perché OP dovrebbe usare questo su qualsiasi altra risposta? Si prega di scavare un po 'più a fondo e dare un po' più di dettaglio –

0

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 345MB linea 345MB tramite un metodo readline richiede più di 12 ore sulla mia macchina mentre si esegue lo stesso carico e si legge tramite MemoryMappedFile ci sono voluti 3 secondi.

0

Tutte le risposte eccellenti! tuttavia, per chi cerca una risposta, questi sembrano essere in qualche modo incompleti.

Come 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 è quello di lavorare con una lista 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 Tokenise e dividere la linea durante l'elaborazione. L'elenco delle stringhe ora può contenere volumi molto grandi di testo.

Problemi correlati