Pregunta

Estoy usando C # para leer un archivo CSV de texto plano de ~ 120 MB. Inicialmente hice el análisis leyéndolo línea por línea, pero recientemente determiné que leer primero todo el contenido del archivo en la memoria era varias veces más rápido. El análisis ya es bastante lento porque el CSV tiene comas incrustadas entre comillas, lo que significa que tengo que usar una división de expresiones regulares. Este es el único que he encontrado que funciona de manera confiable:

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

Para realizar el análisis después de leer todo el contenido en la memoria, hago una división de cadena en el carácter de nueva línea para obtener una matriz que contiene cada línea. Sin embargo, cuando hago esto en el archivo de 120 MB, obtengo una System.OutOfMemoryException . ¿Por qué se queda sin memoria tan rápido cuando mi computadora tiene 4 GB de RAM? ¿Hay una mejor manera de analizar rápidamente un CSV complicado?

¿Fue útil?

Solución

Puede obtener una excepción OutOfMemoryException para prácticamente cualquier tamaño de asignación. Cuando asignas una parte de la memoria, realmente estás pidiendo una parte continua de la memoria del tamaño solicitado. Si no se puede cumplir, verá una excepción OutOfMemoryException.

También debe tener en cuenta que, a menos que esté ejecutando Windows de 64 bits, su memoria RAM de 4 GB se divide en espacio de kernel de 2 GB y espacio de usuario de 2 GB, por lo que su aplicación .NET no puede acceder a más de 2 GB por defecto.

Al realizar operaciones de cadena en .NET, se arriesga a crear muchas cadenas temporales debido al hecho de que las cadenas .NET son inmutables. Por lo tanto, puede ver que el uso de la memoria aumenta drásticamente.

Otros consejos

No lance su propio analizador a menos que tenga que hacerlo. He tenido suerte con este:

Un lector rápido de CSV

Si nada más puede mirar debajo del capó y ver cómo alguien más lo hace.

Si ha leído el archivo completo en una cadena, probablemente debería usar un StringReader .

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

Esto debería ser lo mismo que transmitir desde un archivo con la diferencia de que el contenido ya está en la memoria.

Editar después de probar

Intenté lo anterior con un archivo de 140MB donde el procesamiento consistió en incrementar la longitud variable con line.Length. Esto tomó alrededor de 1.6 segundos en mi computadora. Después de esto intenté lo siguiente:

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

El resultado fue de alrededor de 1 segundo.

Por supuesto, su kilometraje puede variar, especialmente si está leyendo desde una unidad de red o si su procesamiento tarda lo suficiente como para que el disco duro busque otro lugar. Pero también si está utilizando FileStream para leer el archivo y no está almacenando en búfer. StreamReader proporciona almacenamiento en búfer que mejora enormemente la lectura.

Es posible que no puedas asignar un solo objeto con esa cantidad de memoria contigua, ni deberías poder hacerlo. La transmisión es la forma habitual de hacer esto, pero tienes razón en que podría ser más lento (aunque no creo que normalmente deba ser mucho más lento).

Como compromiso, puede intentar leer una parte más grande del archivo (pero no todo) de una vez, con una función como StreamReader.ReadBlock () , y procesar cada parte en girar.

Como dicen otros carteles, OutOfMemory se debe a que no puede encontrar una porción contigua de memoria del tamaño solicitado.

Sin embargo, usted dice que hacer el análisis línea por línea fue varias veces más rápido que leerlo todo de una vez y luego realizar el procesamiento. Esto solo tiene sentido si estaba siguiendo el enfoque ingenuo de hacer lecturas de bloqueo, por ejemplo (en pseudocódigo):

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

En su lugar, debe usar la transmisión, donde su transmisión se completa con llamadas Write () desde un hilo alternativo que está leyendo el archivo, por lo que la lectura del archivo no está bloqueada por lo que hace su ProcessLine (), y viceversa. Eso debería estar a la par con el rendimiento de leer todo el archivo de una vez y luego realizar el procesamiento.

Probablemente debería probar el Perfilador CLR para determinar el uso real de la memoria. Puede ser que haya otros límites de memoria distintos a la memoria RAM de su sistema. Por ejemplo, si se trata de una aplicación IIS, su memoria está limitada por los grupos de aplicaciones.

Con esta información de perfil, es posible que necesite utilizar una técnica más escalable como la transmisión del archivo CSV que intentó originalmente.

Se está quedando sin memoria en la pila, no el montón.

Podría intentar refactorizar su aplicación de modo que esté procesando la entrada en "fragmentos" más manejables de datos en lugar de procesar 120 MB a la vez.

Estoy de acuerdo con la mayoría de todos aquí, necesitas usar streaming.

No sé si alguien lo ha dicho hasta ahora, pero deberías fijarte en un método de extención.

Y sé que, sin duda, la mejor técnica de división de CSV en .NET / CLR es este

Esa técnica me generó + 10GB de salida XML desde el CSV de entrada, incluidos filtros de entrada de gran calidad y todo, más rápido que cualquier otra cosa que haya visto.

Debería leer un fragmento en un búfer y trabajar en eso. Luego lee otro fragmento y así sucesivamente.

Hay muchas bibliotecas por ahí que lo harán de manera eficiente para usted. Mantengo uno llamado CsvHelper . Hay muchos casos extremos que debe manejar, como cuando una coma o un final de línea está en el medio de un campo.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top