CodeGym /Cursos /C# SELF /Optimización del trabajo con archivos grandes

Optimización del trabajo con archivos grandes

C# SELF
Nivel 41, Lección 4
Disponible

1. Introducción

Cuando trabajas con archivos pequeños, a menudo no te enteras: escribiste, leíste — y listo. Pero en cuanto el tamaño del archivo empieza a superar al menos 100–500 MB, y mucho más si hablamos de gigabytes, aparecen efectos interesantes:

  • Las operaciones se vuelven más lentas, especialmente si trabajas con ellas "a lo bruto" — por ejemplo, File.ReadAllBytes() o File.WriteAllText() de repente empiezan a hacer que todo el programa se trabe.
  • Puedes quedarte sin memoria RAM, aparece OutOfMemoryException.
  • El sistema empieza a usar swap (intercambio), lo que puede ralentizar todo el equipo.
  • Las operaciones paralelas pueden generar carga excesiva en el disco.

En tareas reales esto ocurre con frecuencia:

  • Logs de servidores (gigabytes por día).
  • Procesamiento de grandes CSV o XML para export/import.
  • Trabajo con vídeo, audio, archivos comprimidos, binarios.
  • Copiado de ensamblados grandes o backups.

2. Estrategias de optimización al trabajar con archivos grandes

Antes de lanzarte a optimizar, aclaremos qué queremos acelerar o mejorar. Estas son las tareas más comunes:

  • "Leer/escribir un archivo lo más rápido posible sin matar el sistema".
  • "Procesar el archivo por partes para no cargar la memoria".
  • "No crear copias innecesarias de los datos en memoria".
  • "Procesar archivos grandes en paralelo (si es posible)".

Enfoques generales:

  • Lectura y escritura por streaming: usa streams y buffers, lee/escribe por trozos (FileStream, BufferedStream).
  • Operaciones directamente en disco — sin copias intermedias en memoria.
  • Gestiona buffers y memoria con cuidado: no guardes el archivo entero en RAM.
  • Operaciones asíncronas — si necesitas no bloquear el hilo principal (más en la siguiente lección).

3. Lectura y escritura en streaming: patrón básico

Principio principal: ¡Trabaja con archivos por partes! En C# moderno esto se hace fácilmente con clases como FileStream, BufferedStream, o en general cualquier trabajo vía streams.


// Abrimos un stream para lectura:
using FileStream fs = new FileStream("bigfile.bin", FileMode.Open, FileAccess.Read);
byte[] buffer = new byte[1024 * 1024]; // 1 MB
int bytesRead;

// Leemos hasta el final del archivo
while ((bytesRead = fs.Read(buffer, 0, buffer.Length)) > 0)
{
    // Aquí procesamos los datos leídos!
    // Por ejemplo, calculemos la suma de todos los bytes (solo como ejercicio)
    long sum = 0;
    for (int i = 0; i < bytesRead; i++)
        sum += buffer[i];

    Console.WriteLine($"Leídos {bytesRead} bytes, suma: {sum}");
}

Consejo: El tamaño del buffer (por ejemplo, 64 KB, 128 KB, 1 MB) se elige por prueba y error. Demasiado pequeño — tendrás muchas llamadas al disco. Demasiado grande — no siempre mejora el rendimiento y gasta memoria.

¿Por qué así? La lectura clásica con File.ReadAllBytes() o File.ReadAllText() sin dividir en partes intenta cargar todo en memoria. Si el archivo es enorme — el resultado es obvio: te cae un OutOfMemoryException.

4. BufferedStream: por qué y cuándo usarlo

Ya vimos esta clase en la lección anterior, pero conviene recordar: BufferedStream es un wrapper sobre otro stream que permite leer/escribir no byte a byte, sino en bloques.

Ejemplo de uso:


using var fileStream = new FileStream("bigfile.bin", FileMode.Open, FileAccess.Read);
using var bufferedStream = new BufferedStream(fileStream, 1024 * 128);

byte[] buffer = new byte[1024 * 128];
int bytesRead;
while ((bytesRead = bufferedStream.Read(buffer, 0, buffer.Length)) > 0)
{
    // Procesamiento de datos
}

En algunos casos usar BufferedStream acelera, especialmente si lees el archivo de a pocos bytes y el sistema de archivos prefiere accesos por bloques.

Dato interesante: En versiones recientes de .NET FileStream ya incluye buffering por defecto, así que el uso manual de BufferedStream aporta más ventaja con streams "crudos", por ejemplo de red o dispositivos no estándar.

5. Lectura y escritura de archivos de texto grandes

Con archivos binarios está claro: lee y escribe por bloques. ¿Y con archivos de texto grandes, como CSV, logs, JSON?

Aquí ayudan StreamReader para lectura y StreamWriter para escritura.

Lectura línea a línea:


using var reader = new StreamReader("biglog.txt");
string? line;
while ((line = reader.ReadLine()) != null)
{
    // Procesamiento de la línea
    if (line.Contains("ERROR"))
        Console.WriteLine("Detectado ERROR: " + line);
}

¿Por qué es buena idea?

  • No guardamos todo el archivo en memoria.
  • El buffering dentro de StreamReader ya viene optimizado.

Escritura por líneas:


using var writer = new StreamWriter("output.txt");
for (int i = 0; i < 1000000; i++)
    writer.WriteLine($"Esta es la línea número {i}");

Arquitectura de lectura por streaming


   [Archivo en disco]
          |
     [FileStream]
          |
  [BufferedStream (opcional)]
          |
   [StreamReader/StreamWriter (para texto)]
          |
   [Tu código: procesamiento de datos]

6. Aplicación práctica

Sigamos con una app para procesar logs. Ahora la archivación de logs antiguos (más de 7 días) no será solo mover archivos: los vamos a comprimir en un solo archivo. Si un archivo es enorme — lo leeremos y escribiremos por partes para no "hundir" la memoria.

Para simplificar usaremos una copia estándar con streams:


void CopyLargeFile(string sourcePath, string destPath)
{
    using var sourceStream = new FileStream(sourcePath, FileMode.Open, FileAccess.Read);
    using var destStream = new FileStream(destPath, FileMode.Create, FileAccess.Write);
    byte[] buffer = new byte[1024 * 256]; // 256 KB
    int bytesRead;
    while ((bytesRead = sourceStream.Read(buffer, 0, buffer.Length)) > 0)
    {
        destStream.Write(buffer, 0, bytesRead);
        // ¡Podemos añadir aquí una barra de progreso!
    }
}

¿Dónde se usa esto?

  • Copiado de backups
  • Combinar logs por día o mes
  • Preprocesado de archivos (por ejemplo, filtrado de líneas)

7. ¿Cómo medir la eficacia del buffer?

A veces quieres saber: "¿Cuánto se ha acelerado?" Se puede medir midiendo el tiempo de ejecución:


var watch = System.Diagnostics.Stopwatch.StartNew();
CopyLargeFile("source.bin", "dest.bin");
watch.Stop();
Console.WriteLine($"Tiempo de copiado: {watch.Elapsed.TotalSeconds} segundos");

Tamaños típicos de buffer:

  • 4 KB — bloque mínimo del sistema de archivos.
  • 64 KB/128 KB — en la práctica funcionan bien casi siempre.
  • 1 MB y más — justificado solo en SSDs muy rápidos y archivos enormes.

Prueba diferentes tamaños de buffer tú mismo.

8. Buscar y procesar datos en archivos grandes

Si necesitas no solo copiar sino buscar una línea, un número, una frase — en archivos grandes lo más eficiente es hacerlo por porciones.

Ejemplo: encontrar todas las líneas con errores en un log enorme


using var reader = new StreamReader("server.log");
using var writer = new StreamWriter("errors.txt");
string? line;
while ((line = reader.ReadLine()) != null)
{
    if (line.Contains("ERROR"))
        writer.WriteLine(line); // Solo las líneas necesarias van al resultado
}

Este enfoque permite procesar logs de gigabytes sin gastar mucha memoria.

9. Particularidades al trabajar con archivos enormes (>2 GB)

.NET y Windows gestionan bien archivos de cualquier tamaño (incluso decenas de terabytes) si usas lectura en streaming. Pero hay matices:

  • Aplicaciones de 32 bits están limitadas a 2 GB de direccionamiento — usa x64!
  • Para archivos grandes siempre usa plataforma de 64 bits (AnyCPU o x64).
  • Para archivos mayores de 4 GB el sistema de archivos FAT32 no sirve — necesitas NTFS/exFAT.

Procesamiento iterativo de archivos grandes


+------------------+
|    Start         |
+------------------+
        |
        v
+------------------------------+
| Abrir stream para lectura    |
+------------------------------+
        |
        v
+------------------------------+
| Mientras no sea fin de archivo|
+------------------------------+
        |
        v
+---------------------------+
| Leer bloque de datos      |
+---------------------------+
        |
        v
+---------------------------+
| Procesar bloque           |
+---------------------------+
        |
        v
+--------------------------+
| Siguiente bloque         |
+--------------------------+
        |
        v
+--------------------+
| Cerrar stream      |
+--------------------+

10. Matices útiles

Archivo como stream: métodos clave de FileStream

Método Descripción
Read
Lee una parte del archivo al buffer
Write
Escribe parte del buffer al archivo
Seek
Permite saltar a una posición específica en el archivo
Length
Tamaño del archivo en bytes
Position
Posición actual en el archivo

Ejemplo de uso de Seek():


using var stream = new FileStream("bigfile.bin", FileMode.Open);
// ¡Saltar 1 GB hacia adelante!
stream.Seek(1024L * 1024 * 1024, SeekOrigin.Begin);

byte[] buffer = new byte[1024];
int bytesRead = stream.Read(buffer, 0, buffer.Length);
// Ahora leemos datos desde el medio del archivo!

¿Dónde sirve?

  • Indexación de archivos
  • Acceso rápido a bloques necesarios (por ejemplo, en grandes bases de datos)

Trabajo con archivos en varios hilos (multithreading)

Si la tarea lo permite, puedes procesar archivos grandes en paralelo: por ejemplo, dividirlos en bloques y leer/escribir partes diferentes simultáneamente. Pero hay que entender que los discos duros mecánicos (HDD) son lentos con accesos aleatorios, mientras que los SSD son mucho más rápidos.

En tareas domésticas simples, si no estás seguro, trabaja de forma secuencial. El paralelismo ayuda cuando procesas varios archivos a la vez, no tanto para un único archivo (donde la ganancia puede ser dudosa).

11. Errores típicos al trabajar con archivos grandes

Error nº1: leer todo el archivo en memoria.
Los novatos suelen usar File.ReadAllBytes() o File.ReadAllText() incluso para archivos enormes. Si el archivo pesa gigabytes, la aplicación fallará por falta de memoria. Usa lectura en streaming.

Error nº2: usar un buffer demasiado pequeño.
Leer con un buffer diminuto es como beber sopa con una cucharilla de té. El programa gastará mucho tiempo en accesos al disco y estará esperando en vez de trabajar. Escoge un tamaño de buffer razonable.

Error nº3: olvidar cerrar el stream.
Si no cierras el stream al terminar, el descriptor de archivo queda ocupado. Esto impide a otros programas acceder y puede generar errores a nivel del SO. Usa siempre using — es más seguro y limpio.

Error nº4: acceso concurrente al mismo archivo.
Intentar leer y escribir el mismo archivo desde distintas partes del programa es una vía segura hacia IOException. Aunque a veces "funcione", no tendrás estabilidad.

2
Tarea
C# SELF, nivel 41, lección 4
Bloqueada
Copiar un archivo grande
Copiar un archivo grande
1
Cuestionario/control
Principio de bufferización de datos, nivel 41, lección 4
No disponible
Principio de bufferización de datos
Optimización de entrada/salida
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION