CodeGym /Cursos /C# SELF /Uso de BufferedStream

Uso de BufferedStream

C# SELF
Nivel 41 , Lección 2
Disponible

1. Introducción

Antes de empezar a usar una clase nueva, vale la pena entender para qué sirve. Vamos a ver qué pasa cuando trabajamos con un archivo "directamente" usando FileStream.

Cuando llamas a Read o Write en un stream creado con FileStream, en realidad se hace una llamada al subsistema de disco del ordenador. Ese proceso (especialmente en discos duros antiguos, pero también en SSD modernos) es mucho más lento que trabajar con RAM. Imagínate que cuando pides papas fritas en McDonalds, el cajero corre al almacén por una bolsa nueva cada vez. ¡Imagina lo larga que sería la cola!

Si trabajas con trozos pequeños de datos, las llamadas frecuentes al disco o a la red reducen el rendimiento. Cuanto mayor sea el volumen de datos, más evidente será el efecto.

Analogía breve

Resulta que streams sin buffer son como ir a la tienda 10 veces para comprar un yogur cada vez. Un stream bufferizado es cuando te llevas una cesta entera de yogures y reduces al mínimo los viajes.

2. La clase BufferedStream: primera mirada

Para qué sirve

BufferedStream es un wrapper alrededor de cualquier stream (Stream) que mantiene un buffer intermedio en memoria. Cuando escribes datos, primero van al buffer y sólo cuando el buffer se llena se vuelcan al disco en una operación grande. Lo mismo ocurre al leer: en la primera lectura carga un trozo significativo en memoria y luego devuelve porciones desde la memoria hasta que el buffer se agota.

Ejemplo de código: crear un BufferedStream

Hagamos un ejemplo sencillo. Supongamos que tenemos que escribir 100 000 líneas en un archivo:


string filePath = "big_output.txt";
using var fileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write);
using var bufferedStream = new BufferedStream(fileStream);
using var writer = new StreamWriter(bufferedStream);

for (int i = 0; i < 100_000; i++)
{
    writer.WriteLine($"Line number {i}");
}

Console.WriteLine("¡Grabación completada!");

Comentario:

  • Abrimos el archivo para escritura mediante FileStream.
  • Luego lo envolvemos en BufferedStream, y después en StreamWriter (este escribe líneas de texto en el stream).
  • Cuando el buffer se llena, los datos se vuelcan al disco en un solo bloque.

3. Cómo funciona la bufferización "por dentro"

Desgranémoslo en un diagrama:


[Tu código] → [StreamWriter] → [BufferedStream] → [FileStream] → [Archivo en disco]

Cuando llamas al método WriteLine() de StreamWriter, el texto primero se escribe en un buffer interno, luego a través de BufferedStream — en otro buffer, y solo cuando el buffer se llena o el stream se cierra, los datos se vuelcan al disco.

¿Cuántos bytes en un "cubo"?

El tamaño de buffer por defecto es 4096 bytes (4 KB), pero se puede especificar explícitamente:


int myBufferSize = 16 * 1024; // 16 KB
using var fileStream = new FileStream(filePath, FileMode.Create);
using var bufferedStream = new BufferedStream(fileStream, myBufferSize);
// ...

Tip práctico: En sistemas modernos tiene sentido usar buffers de 8–64 KB. Para operaciones con archivos muy grandes, incluso más. Pero no te pases: si trabajas en un microcontrolador con 128 KB de RAM, un buffer de 64 KB puede ser una mala idea :)

4. Experimento: comparemos velocidad con buffer y sin él

Para entender lo importante que es, escribamos un test que compare escribir datos con FileStream con y sin buffer:


using System.Diagnostics;
using System.Text;

string data = new string('X', 1000); // 1 000 caracteres

void WriteWithoutBuffer()
{
    using var fs = new FileStream("no_buffer.txt", FileMode.Create, FileAccess.Write, FileShare.None, 4096, useAsync: false);
    for (int i = 0; i < 10_000; i++)
    {
        byte[] bytes = Encoding.UTF8.GetBytes(data);
        fs.Write(bytes, 0, bytes.Length); // Directo al archivo – cada vez acceso al disco
    }
}

void WriteWithBuffer()
{
    using var fs = new FileStream("with_buffer.txt", FileMode.Create, FileAccess.Write, FileShare.None);
    using var bs = new BufferedStream(fs, 16 * 1024);
    for (int i = 0; i < 10_000; i++)
    {
        byte[] bytes = Encoding.UTF8.GetBytes(data);
        bs.Write(bytes, 0, bytes.Length);
    }
}

// Medimos el tiempo
Stopwatch sw = Stopwatch.StartNew();
WriteWithoutBuffer();
sw.Stop();
Console.WriteLine("Sin buffer: " + sw.ElapsedMilliseconds + " ms");

sw.Restart();
WriteWithBuffer();
sw.Stop();
Console.WriteLine("Con buffer: " + sw.ElapsedMilliseconds + " ms");

Salida esperada:
En la mayoría de casos verás un aumento de velocidad con buffer. Especialmente si tienes HDD. En SSD el efecto también se nota, pero no tan dramáticamente.

5. ¿Qué buffer elegir? Comparativa y práctica

En .NET hay muchas clases relacionadas con bufferización. Aclarémoslo:

Clase Para qué se usa ¿Buffer incorporado? ¿Necesitas usar BufferedStream?
FileStream
Trabajo con archivos Sí (con 4 KB) Casi no hace falta (pero se puede)
NetworkStream
Trabajo con red No Muy recomendable
StreamReader/Writer
Lectura/escritura de texto Sí (desde 1 KB) Normalmente no hace falta
GZipStream
Compresión/descompresión No Puede/conviene para acelerar

Importante:
FileStream con el parámetro del constructor bufferSize es, en esencia, ya un stream con buffer. Si has especificado un buffer suficientemente grande, añadir otro BufferedStream no dará mucha ganancia. Pero si usas otro tipo de stream (por ejemplo, de red), BufferedStream es tu amigo.

6. Ejemplo: Copiar un archivo usando BufferedStream


string source = "big_input.dat";
string dest = "big_output.dat";

int bufferSize = 64 * 1024; // 64 KB

using var inputStream = new FileStream(source, FileMode.Open, FileAccess.Read);
using var outputStream = new FileStream(dest, FileMode.Create, FileAccess.Write);
using var bufferedInput = new BufferedStream(inputStream, bufferSize);
using var bufferedOutput = new BufferedStream(outputStream, bufferSize);

byte[] buffer = new byte[bufferSize];
int bytesRead;
while ((bytesRead = bufferedInput.Read(buffer, 0, buffer.Length)) > 0)
{
    bufferedOutput.Write(buffer, 0, bytesRead);
}
// ¡No olvides flush – si no, los últimos bytes no llegarán al disco!
bufferedOutput.Flush();

Console.WriteLine("¡Copia completada!");

Comentario:

  • Leemos datos en bloques grandes (64 KB) desde un archivo usando BufferedStream.
  • Los escribimos en otro archivo, también usando buffer.
  • Al final del bucle llamamos obligatoriamente a Flush() para volcar los últimos datos.

7. Matices útiles

Consejo: cuándo realmente necesitas BufferedStream

  • Si trabajas con streams que no tienen buffer (por ejemplo, streams de red, o implementaciones personalizadas de Stream);
  • Si manejas grandes volúmenes de datos binarios (por ejemplo, copia de archivos, conversión de formatos, backups);
  • Si optimizas código existente y ves que el cuello de botella son muchas operaciones pequeñas de Write/Read.

Un poco sobre asincronía y bufferización

Con la llegada de operaciones asíncronas (ReadAsync/WriteAsync) la bufferización sigue siendo útil, pero ten en cuenta: si usas métodos asíncronos sobre un buffer, el procesamiento sigue pasando por memoria, y la interacción física con el disco se minimiza aún más.

En .NET 8+ y .NET 9 la bufferización está integrada cada vez más, y la mayoría de clases tienen buffers por defecto. Pero para compatibilidad con streams de red o tus propias implementaciones sigue siendo útil usar BufferedStream manualmente.

Más sobre asincronía lo verás en el nivel 58 :P

Esquema visual del trabajo de streams bufferizados

flowchart LR
    A[Tu código] --> B[StreamReader/Writer]
    B --> C[BufferedStream]
    C --> D[FileStream]
    D --> E[Archivo/Dispositivo]
  • A — Tu código que llama a Write/Read.
  • B — Stream de alto nivel (trabaja con texto o datos).
  • C — Bufferización (agrupa datos para mejorar velocidad).
  • D — Implementación concreta del stream (archivo, red).
  • E — Dispositivo físico (disco duro, SSD, red, etc.).

Consejos y trucos prácticos

  1. Si escribes una línea por vez en un archivo (por ejemplo, logging), mejor especificar explícitamente un tamaño de buffer mayor que el tamaño de una línea. Así podrás volcar grandes lotes más rápido.
  2. Si cada acción debe persistirse inmediatamente (por ejemplo, logs críticos), llama a Flush() después de cada escritura. Pero eso reduce la ventaja de la bufferización.
  3. Si creas archivos temporales que se borran justo después de crearlos, puede no importar si queda algo en el buffer — pero ten cuidado si es importante que el archivo esté realmente escrito.
  4. Al trabajar con archivos muy grandes (por ejemplo, decenas de gigabytes) no dudes en aumentar el tamaño del buffer hasta 1_048_576 bytes (1 MB) o más — lo importante es que haya suficiente RAM.

8. Errores típicos y matices de uso

Si ahora te entraron ganas de "poner buffer en todas partes" — no te precipites. Todo con medida.

Un error frecuente es olvidar llamar a Flush() o cerrar el stream cuando es necesario. Si el stream no se ha cerrado y el programa termina de forma abrupta, los últimos bytes pueden quedarse en el buffer en RAM y no llegar al disco. Por ejemplo, si escribes logs y la aplicación cae, la última entrada puede perderse.

BufferedStream por sí solo no "ve" el fin de tus mensajes lógicos — simplemente espera a acumular una porción del tamaño necesario. Por eso, para cosas críticas (logs, backups, etc.) es mejor llamar periódicamente a Flush():

bufferedStream.Flush(); // Obliga al buffer a volcar los datos al disco

Si usas StreamWriter, éste tiene su propio buffer. Es decir, cuando los usas anidados hay doble bufferización (y eso no siempre es bueno). A menudo un solo nivel de buffer es suficiente, y si usas StreamWriter el BufferedStream adicional puede no ser necesario.

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