Domanda

Sto usando C # per leggere un file CSV in testo normale di ~ 120 MB. Inizialmente ho eseguito l'analisi leggendolo riga per riga, ma recentemente ho determinato che la lettura dell'intero contenuto del file in memoria era prima più volte più veloce. L'analisi è già piuttosto lenta perché il CSV ha virgole incorporate tra virgolette, il che significa che devo usare una divisione regex. Questo è l'unico che ho trovato che funziona in modo affidabile:

string[] fields = Regex.Split(line, 
@",(?!(?<=(?:^|,)\s*\x22(?:[^\x22]|\x22\x22|\\\x22)*,)
(?:[^\x22]|\x22\x22|\\\x22)*\x22\s*(?:,|$))");
// from http://regexlib.com/REDetails.aspx?regexp_id=621

Per eseguire l'analisi dopo aver letto l'intero contenuto in memoria, eseguo una suddivisione in stringhe sul carattere di nuova riga per ottenere un array contenente ciascuna riga. Tuttavia, quando lo faccio sul file da 120 MB, ottengo un System.OutOfMemoryException . Perché la memoria si esaurisce così rapidamente quando il mio computer ha 4 GB di RAM? Esiste un modo migliore per analizzare rapidamente un CSV complicato?

È stato utile?

Soluzione

Puoi ottenere OutOfMemoryException per qualsiasi dimensione di allocazione. Quando si alloca un pezzo di memoria, si richiede davvero un pezzo di memoria continuo della dimensione richiesta. Se ciò non può essere onorato vedrai una OutOfMemoryException.

Dovresti anche essere consapevole del fatto che, a meno che tu non stia eseguendo Windows a 64 bit, la tua RAM da 4 GB è divisa in 2 GB di spazio kernel e 2 GB di spazio utente, quindi l'applicazione .NET non può accedere a più di 2 GB per impostazione predefinita.

Quando si eseguono operazioni sulle stringhe in .NET, si rischia di creare molte stringhe temporanee a causa del fatto che le stringhe .NET sono immutabili. Pertanto, è possibile che l'utilizzo della memoria aumenti in modo piuttosto drammatico.

Altri suggerimenti

Non eseguire il rollup del proprio parser a meno che non sia necessario. Ho avuto fortuna con questo:

Un lettore CSV veloce

Se non altro puoi guardare sotto il cofano e vedere come lo fa qualcun altro.

Se hai letto l'intero file in una stringa, probabilmente dovresti usare un StringReader .

StringReader reader = new StringReader(fileContents);
string line;
while ((line = reader.ReadLine()) != null) {
    // Process line
}

Questo dovrebbe essere approssimativamente uguale allo streaming da un file con la differenza che il contenuto è già in memoria.

Modifica dopo il test

Ho provato quanto sopra con un file di 140 MB in cui l'elaborazione consisteva nell'incrementare la variabile della lunghezza con line.Length. Questo ha impiegato circa 1,6 secondi sul mio computer. Dopo questo ho provato quanto segue:

System.IO.StreamReader reader = new StreamReader("D:\\test.txt");
long length = 0;
string line;
while ((line = reader.ReadLine()) != null)
    length += line.Length;

Il risultato è stato di circa 1 secondo.

Ovviamente il tuo chilometraggio può variare, specialmente se stai leggendo da un'unità di rete o l'elaborazione richiede abbastanza tempo perché il disco rigido possa cercare altrove. Ma anche se stai usando FileStream per leggere il file e non stai bufferizzando. StreamReader fornisce il buffering che migliora notevolmente la lettura.

Potresti non essere in grado di allocare un singolo oggetto con quella memoria contigua, né dovresti aspettarti di poterlo fare. Lo streaming è il modo normale per farlo, ma hai ragione sul fatto che potrebbe essere più lento (anche se non penso che di solito dovrebbe essere molto più lento.)

Come compromesso, potresti provare a leggere una porzione più grande del file (ma non ancora l'intera cosa) contemporaneamente, con una funzione come StreamReader.ReadBlock () , ed elaborare ogni porzione in girare.

Come dicono altri poster, OutOfMemory è perché non riesce a trovare un pezzo contiguo di memoria della dimensione richiesta.

Tuttavia, dici che eseguire l'analisi riga per riga è stata parecchie volte più veloce che leggerla tutta in una volta e poi fare la tua elaborazione. Questo ha senso solo se stavi perseguendo l'approccio ingenuo di fare letture di blocco, ad esempio (in pseudo codice):

while(! file.eof() )
{
    string line = file.ReadLine();
    ProcessLine(line);
}

Dovresti invece usare lo streaming, dove il tuo flusso è compilato da chiamate Write () da un thread alternativo che sta leggendo il file, quindi il file letto non è bloccato da qualunque cosa faccia ProcessLine () e viceversa. Ciò dovrebbe essere alla pari con la prestazione di leggere l'intero file in una sola volta e quindi di eseguire l'elaborazione.

Probabilmente dovresti provare Profiler CLR per determinare l'utilizzo effettivo della memoria. È possibile che esistano limiti di memoria diversi dalla RAM di sistema. Ad esempio, se si tratta di un'applicazione IIS, la memoria è limitata dai pool di applicazioni.

Con queste informazioni sul profilo potresti scoprire che devi utilizzare una tecnica più scalabile come lo streaming del file CSV che hai tentato originariamente.

Stai esaurendo la memoria nello stack, non nell'heap.

Potresti provare a ricodificare la tua app in modo tale da elaborare l'input in "blocchi" più gestibili " di dati anziché elaborare 120 MB alla volta.

Sono d'accordo con la maggior parte di tutti qui, è necessario utilizzare lo streaming.

Non so se qualcuno abbia detto finora, ma dovresti guardare un metodo di estinzione.

E so, senza dubbio, la migliore tecnica di divisione CSV su .NET / CLR è questo

Questa tecnica mi ha generato + 10 GB di output XML dall'input CSV, inclusi filtri di input estesi e tutto il resto, più velocemente di qualsiasi altra cosa che abbia mai visto.

Dovresti leggere un pezzo in un buffer e lavorarci su. Quindi leggi un altro pezzo e così via.

Ci sono molte librerie là fuori che lo faranno in modo efficiente per te. Ne mantengo uno chiamato CsvHelper . Esistono molti casi limite che è necessario gestire, ad esempio quando una virgola o una fine di riga si trova nel mezzo di un campo.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top