CodeGym /Cursos /C# SELF /Rendimiento y gestión manual del buffer

Rendimiento y gestión manual del buffer

C# SELF
Nivel 41 , Lección 3
Disponible

1. Introducción

Por qué sirve la bufferización y comparación de rendimiento

Ya sabemos que la bufferización es una estrategia que permite agrupar datos en «paquetes» grandes para no acceder al disco cientos de miles de veces, sino mucho menos, enviando muchos datos de una vez. Esto da un aumento de rendimiento notable, sobre todo con volúmenes de datos grandes.

Cuándo pensar en el rendimiento de I/O

  • Si trabajas con archivos grandes (gigabytes, terabytes — por ejemplo, la colección de logs de toda la vida de tu empresa).
  • Si necesitas latencia mínima (por ejemplo, procesamiento de logs en tiempo real).
  • Si el número de accesos a archivos es enorme (por ejemplo, renombrado/duplicado masivo de un archivo de fotos).
  • Para demostrar en una entrevista que entiendes enfoques modernos de optimización (y te gusta medir la velocidad, no sólo «hacer que funcione como sea»).

En .NET por defecto la mayoría de los streams de archivos ya tienen su propia bufferización, pero a veces hace falta un ajuste fino o una estrategia especial.

Principales «jugadores» — qué buffers existen

Clase Buffer por defecto Se puede cambiar tamaño Aplicabilidad
FileStream
Existe (4096 bytes) Sí (vía constructor) Stream de archivo base
BufferedStream
Sí (4096 bytes) Sí (vía constructor) «Wrapper» alrededor de un stream
StreamReader/Writer
Sí (1024/1024 bytes) Sí (constructor) Trabajo con texto
  • BufferedStream puede «empaquetar» otro stream para mejorar el rendimiento (por ejemplo, si el stream base no está bien bufferizado o necesitas un buffer mayor).
  • El tamaño del buffer es un compromiso entre velocidad y uso de RAM.

2. Ejemplos:

Comparemos tres enfoques para copiar un archivo grande:

  1. Sin buffer — byte a byte (malo, pero ilustrativo)
  2. Buffer por defectoFileStream estándar con CopyTo
  3. Gestión manual del buffer — pasamos el buffer nosotros mismos, optimizamos el tamaño

Para los experimentos creemos una utilidad simple para copiar archivos en nuestra app. Que el archivo se llame BigFile.bin.

class FileCopyBenchmarks
{
    // Copia byte a byte (anti-ejemplo — no lo hagas así)
    public static void CopyOneByte(string source, string dest)
    {
        using var input = new FileStream(source, FileMode.Open, FileAccess.Read);
        using var output = new FileStream(dest, FileMode.Create, FileAccess.Write);

        int b;
        while ((b = input.ReadByte()) != -1)
        {
            output.WriteByte((byte)b);
        }
    }

    // Copia usando el buffer por defecto de FileStream
    public static void CopyWithDefaultBuffer(string source, string dest)
    {
        using var input = new FileStream(source, FileMode.Open, FileAccess.Read);
        using var output = new FileStream(dest, FileMode.Create, FileAccess.Write);

        input.CopyTo(output); // Usa un buffer interno (normalmente 81920 bytes)
    }

    // Copia con control manual del buffer
    public static void CopyWithCustomBuffer(string source, string dest, int bufferSize = 1024 * 1024)
    {
        using var input = new FileStream(source, FileMode.Open, FileAccess.Read);
        using var output = new FileStream(dest, FileMode.Create, FileAccess.Write);

        byte[] buffer = new byte[bufferSize];
        int bytesRead;
        while ((bytesRead = input.Read(buffer, 0, buffer.Length)) > 0)
        {
            output.Write(buffer, 0, bytesRead);
        }
    }
}

¿Cuál de las funciones es más rápida? Vamos a medir los tiempos de ejecución.

Cómo medir correctamente el rendimiento

En .NET lo más sencillo para medir tiempo es usar Stopwatch:

static void Measure(Action action, string description)
{
    var sw = Stopwatch.StartNew();
    action();
    sw.Stop();
    Console.WriteLine($"{description}: {sw.ElapsedMilliseconds} ms");
}

Ahora probemos a copiar el mismo archivo con distintos métodos:

string source = "BigFile.bin";
string dest1 = "copy1.bin";
string dest2 = "copy2.bin";
string dest3 = "copy3.bin";

// Crea de antemano BigFile.bin (por ejemplo, 100-500 MB) o usa cualquier archivo grande.

Measure(() => FileCopyBenchmarks.CopyOneByte(source, dest1), "CopyOneByte (por 1 byte)");
Measure(() => FileCopyBenchmarks.CopyWithDefaultBuffer(source, dest2), "CopyWithDefaultBuffer (estándar)");
Measure(() => FileCopyBenchmarks.CopyWithCustomBuffer(source, dest3, 1024 * 1024), "CopyWithCustomBuffer (1 MB)");

Puntos peligrosos y trampas

  • Si ejecutas muchas veces seguidas, la cache del SO puede «calentar» el disco y las mediciones siguientes saldrán más rápidas — para una evaluación real es mejor reiniciar la app y limpiar la cache.
  • Si el archivo es pequeño (10–20 KB), las ventajas de bufferizar no se notarán — cuanto más grande el archivo, mayor la diferencia.
  • Si pones un buffer demasiado grande (por ejemplo, 100 MB), el consumo de memoria puede subir mucho y el resto del sistema sufrir.

Visualización de resultados: tabla

Método Tiempo (ms) con archivo de 500 MB
Byte a byte 100 000+
FileStream estándar / CopyTo 1 000 — 5 000
Buffer manual 1 MB 700 — 1 200

Las cifras son orientativas, pero la tendencia es clara — cuanto mayor el buffer, menos accesos al disco y mayor velocidad.

3. Anatomía de la gestión manual del buffer

¿Por qué a veces quieres ajustar el tamaño del buffer? Una analogía simple: te mudas a un piso nuevo. Puedes llevar las cosas de una cuchara en una mano, o meter muchas cosas en una caja grande. Pero la caja tiene límite, ¡si es demasiado grande no la puedes levantar!

Cómo funciona la lectura con buffer manual

// Ejemplo de control manual del tamaño del buffer
int bufferSize = 1024 * 1024; // 1 MB
byte[] buffer = new byte[bufferSize];
int read;
while ((read = inputStream.Read(buffer, 0, buffer.Length)) > 0)
{
    outputStream.Write(buffer, 0, read);
}
  • El método Read intenta rellenar todo el buffer, pero puede devolver menos si el archivo termina.
  • El tamaño del buffer típicamente se elige entre 32 KB y 48 MB — más allá de eso casi no hay ganancia.
  • No olvides la memoria RAM, sobre todo si hay muchas operaciones o muchos threads.

Experimenta con el tamaño del buffer

Prueba a cambiar el tamaño del buffer en nuestro ejemplo (32 KB, 128 KB, 1 MB, 4 MB) y mira dónde está el pico de rendimiento. Normalmente la «zona dorada» está cerca de 1 MB.

Escenarios donde el buffer manual es más útil

  • Cuando necesitas controlar la memoria usada (por ejemplo, la app corre en un servidor modesto).
  • Cuando el stream no se bufferiza automáticamente (NetworkStream, streams personalizados).
  • Al trabajar con muchas operaciones paralelas — puedes dar a cada operación su propio buffer optimizado.
  • Cuando quieres exprimir al máximo el procesamiento de archivos muy grandes (p. ej. transformar un CSV enorme).

4. Buenas prácticas y errores típicos

¿Se puede hacer el buffer gigantesco? «Memoria hay a montones». Puedes, pero ¿para qué? Un buffer demasiado grande puede incluso empeorar el rendimiento: parte de la memoria quedará inútil y el sistema puede entorpecerse por problemas de caching.

La bufferización manual no acelera todo. Para archivos pequeños o streams ya optimizados (por ejemplo, FileStream con un buffer interno grande) la ganancia será mínima y el código más complejo.

Trampa típica: olvidar cerrar el stream o no manejar excepciones — el archivo se quedará bloqueado. Usa using y manejo de errores (try-catch) al trabajar con archivos.

Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION