Leer archivos de texto grandes con secuencias en C#
Tengo la hermosa tarea de descubrir cómo manejar archivos grandes que se cargan en el editor de scripts de nuestra aplicación (es como VBA para nuestro producto interno para macros rápidas). La mayoría de los archivos tienen entre 300 y 400 KB, lo que se carga bien. Pero cuando superan los 100 MB, el proceso tiene dificultades (como era de esperar).
Lo que sucede es que el archivo se lee y se coloca en un RichTextBox por el que luego se navega; no se preocupe demasiado por esta parte.
El desarrollador que escribió el código inicial simplemente usa un StreamReader y hace
[Reader].ReadToEnd()
lo cual podría tardar bastante en completarse.
Mi tarea es dividir este fragmento de código, leerlo en fragmentos en un búfer y mostrar una barra de progreso con una opción para cancelarlo.
Algunas suposiciones:
- La mayoría de los archivos tendrán entre 30 y 40 MB.
- El contenido del archivo es texto (no binario), algunos tienen formato Unix y otros son DOS.
- Una vez recuperado el contenido, determinamos qué terminador se utiliza.
- A nadie le preocupa, una vez cargado, el tiempo que lleva renderizarse en el cuadro de texto enriquecido. Es solo la carga inicial del texto.
Ahora las preguntas:
- ¿Puedo simplemente usar StreamReader, luego verificar la propiedad Longitud (es decir, ProgressMax) y emitir una lectura para un tamaño de búfer establecido e iterar en un bucle while MIENTRAS estoy dentro de un trabajador en segundo plano, para que no bloquee el hilo principal de la interfaz de usuario? Luego, devuelva el generador de cadenas al hilo principal una vez que esté completo.
- El contenido irá a StringBuilder. ¿Puedo inicializar StringBuilder con el tamaño de la secuencia si la longitud está disponible?
¿Son estas (en su opinión profesional) buenas ideas? He tenido algunos problemas en el pasado con la lectura de contenido de Streams, porque siempre se pierden los últimos bytes o algo así, pero haré otra pregunta si este es el caso.
Puedes mejorar la velocidad de lectura usando un BufferedStream, como este:
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)
{
}
}
ACTUALIZACIÓN de marzo de 2013
Recientemente escribí código para leer y procesar (buscar texto en) archivos de texto de 1 GB (mucho más grandes que los archivos involucrados aquí) y logré una ganancia de rendimiento significativa al usar un patrón de productor/consumidor. La tarea del productor leyó líneas de texto usando BufferedStream
y las entregó a una tarea de consumidor separada que realizó la búsqueda.
Aproveché esto como una oportunidad para aprender TPL Dataflow, que es muy adecuado para codificar rápidamente este patrón.
Por qué BufferedStream es más rápido
Un búfer es un bloque de bytes en la memoria que se utiliza para almacenar datos en caché, lo que reduce la cantidad de llamadas al sistema operativo. Los buffers mejoran el rendimiento de lectura y escritura. Un buffer se puede utilizar para lectura o escritura, pero nunca para ambas simultáneamente. Los métodos de lectura y escritura de BufferedStream mantienen automáticamente el búfer.
ACTUALIZACIÓN de diciembre de 2014: Su millaje puede variar
Según los comentarios, FileStream debería utilizar un BufferedStream internamente. En el momento en que se proporcionó esta respuesta por primera vez, medí un aumento significativo en el rendimiento al agregar un BufferedStream. En ese momento, mi objetivo era .NET 3.x en una plataforma de 32 bits. Hoy en día, al apuntar a .NET 4.5 en una plataforma de 64 bits, no veo ninguna mejora.
Relacionado
Me encontré con un caso en el que la transmisión de un archivo CSV grande generado al flujo de respuesta desde una acción ASP.Net MVC era muy lenta. Agregar un BufferedStream mejoró el rendimiento 100 veces en este caso. Para obtener más información, consulte Salida sin búfer muy lenta.
Si lee las estadísticas de rendimiento y de referencia en este sitio web , verá que la forma más rápida de leer (porque la lectura, la escritura y el procesamiento son diferentes) un archivo de texto es el siguiente fragmento de código:
using (StreamReader sr = File.OpenText(fileName))
{
string s = String.Empty;
while ((s = sr.ReadLine()) != null)
{
//do your stuff here
}
}
Se compararon alrededor de 9 métodos diferentes, pero ese parece salir adelante la mayor parte del tiempo, incluso superando al lector almacenado en búfer, como han mencionado otros lectores.
Utilice un trabajador en segundo plano y lea solo un número limitado de líneas. Lea más solo cuando el usuario se desplace.
Y trate de no usar nunca ReadToEnd(). Es una de las funciones que piensas “¿para qué la hicieron?”; es un script de ayuda para niños que va bien con cosas pequeñas, pero como puedes ver, apesta para archivos grandes...
Esas personas que te dicen que uses StringBuilder necesitan leer el MSDN con más frecuencia:
Consideraciones de rendimiento
Los métodos Concat y AppendFormat concatenan datos nuevos a un objeto String o StringBuilder existente. Una operación de concatenación de objetos String siempre crea un nuevo objeto a partir de la cadena existente y los nuevos datos. Un objeto StringBuilder mantiene un búfer para acomodar la concatenación de nuevos datos. Los datos nuevos se agregan al final del búfer si hay espacio disponible; de lo contrario, se asigna un búfer nuevo y más grande, los datos del búfer original se copian al nuevo búfer y luego los nuevos datos se agregan al nuevo búfer. El rendimiento de una operación de concatenación para un objeto String o StringBuilder depende de la frecuencia con la que se produce una asignación de memoria.
Una operación de concatenación de String siempre asigna memoria, mientras que una operación de concatenación de StringBuilder solo asigna memoria si el búfer del objeto StringBuilder es demasiado pequeño para acomodar los nuevos datos. En consecuencia, la clase String es preferible para una operación de concatenación si se concatena un número fijo de objetos String. En ese caso, el compilador podría incluso combinar las operaciones de concatenación individuales en una sola operación. Es preferible un objeto StringBuilder para una operación de concatenación si se concatena un número arbitrario de cadenas; por ejemplo, si un bucle concatena un número aleatorio de cadenas de entrada del usuario.
Eso significa una gran asignación de memoria, lo que se convierte en un gran uso del sistema de archivos de intercambio, que simula secciones de su disco duro para que actúen como la memoria RAM, pero un disco duro es muy lento.
La opción StringBuilder parece adecuada para quienes usan el sistema como monousuario, pero cuando tienes dos o más usuarios leyendo archivos grandes al mismo tiempo, tienes un problema.