CodeGym /Cursos /C# SELF /Desempenho e controle manual do buffer

Desempenho e controle manual do buffer

C# SELF
Nível 41 , Lição 3
Disponível

1. Introdução

Por que bufferizar e comparação de desempenho

Já entendemos que a bufferização é uma estratégia que permite agrupar dados em grandes "lotes", para não acessar o disco centenas de milhares de vezes, mas bem menos, transferindo muitos dados de uma vez. Isso dá um ganho expressivo de performance, especialmente em volumes grandes de dados.

Quando pensar em performance de I/O

  • Se você trabalha com arquivos grandes (gigabytes, terabytes — tipo a coleção de logs acumulada ao longo dos anos da sua empresa).
  • Se é necessário tempo de resposta mínimo (por exemplo, processamento de log em tempo real).
  • Se o número de acessos a arquivos é enorme (por exemplo, renomeação/backup em massa de um arquivo de fotos).
  • Para demonstrar numa entrevista que você entende abordagens modernas de otimização (e curte medir velocidade, em vez de só "fazer pra funcionar").

No .NET, por padrão a maioria dos streams de arquivo já tem bufferização, mas às vezes precisamos de ajuste fino ou de uma estratégia especial.

Principais "atores" — que tipos de buffer existem

Classe Buffer padrão É possível alterar o tamanho Aplicabilidade
FileStream
Tem (4096 bytes) Sim (via construtor) Stream de arquivo básico
BufferedStream
Sim (4096 bytes) Sim (via construtor) "Wrapper" em torno de um stream
StreamReader/Writer
Sim (1024/1024 bytes) Sim (construtor) Trabalhar com texto
  • BufferedStream pode "embrulhar" outro stream para melhorar a performance (por exemplo, se o stream base tiver buffer ruim ou você precisar de um buffer maior).
  • O tamanho do buffer é um compromisso entre velocidade e consumo de RAM.

2. Exemplos:

Vamos comparar três abordagens para copiar um arquivo grande:

  1. Sem buffer — byte a byte (ruim, mas didático!)
  2. Buffer padrãoFileStream padrão com CopyTo
  3. Controle manual do buffer — passamos o buffer nós mesmos, otimizando o tamanho

Para os testes vamos criar uma utilidade simples de cópia de arquivos e adicioná-la ao app. Suponha que o arquivo se chame BigFile.bin.

class FileCopyBenchmarks
{
    // Cópia byte a byte (anti-exemplo — não faça assim!)
    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);
        }
    }

    // Cópia usando o buffer padrão do 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 buffer interno (normalmente 81920 bytes)
    }

    // Cópia com controle manual do 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);
        }
    }
}

Qual das funções é mais rápida? Vamos medir o tempo de execução.

Como medir performance corretamente

No .NET, para medir tempo o mais simples é usar Stopwatch:

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

Agora vamos tentar copiar o mesmo arquivo de formas diferentes:

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

// Crie antes o arquivo BigFile.bin (por exemplo, 100-500 MB) ou use qualquer arquivo grande.

Measure(() => FileCopyBenchmarks.CopyOneByte(source, dest1), "CopyOneByte (por 1 byte)");
Measure(() => FileCopyBenchmarks.CopyWithDefaultBuffer(source, dest2), "CopyWithDefaultBuffer (padrão)");
Measure(() => FileCopyBenchmarks.CopyWithCustomBuffer(source, dest3, 1024 * 1024), "CopyWithCustomBuffer (1 MB)");

Pontos perigosos e armadilhas

  • Se rodar muitas vezes seguidas, o cache do SO pode "aquecer" o disco, e medições subsequentes ficarão mais rápidas — para avaliação real é melhor reiniciar o programa e limpar o cache.
  • Se o arquivo for pequeno (10–20 KB), a vantagem da bufferização será imperceptível — quanto maior o arquivo, maior a diferença.
  • Se especificar um buffer muito grande (por exemplo, 100 MB), o consumo de memória pode disparar e o restante do sistema sofrer.

Visualização dos resultados: tabela

Método Tempo (ms) para arquivo de 500 MB
Byte a byte 100 000+
FileStream padrão / CopyTo 1 000 — 5 000
Buffer manual 1 MB 700 — 1 200

Números aproximados, mas a tendência é clara — quanto maior o buffer, menos acessos ao disco e maior a velocidade.

3. Anatomia do controle manual do buffer

Por que às vezes ainda queremos ajustar manualmente o tamanho do buffer? Uma analogia simples: você está mudando de apartamento. Dá pra levar as coisas num copo por vez, ou pegar uma caixa grande. Mas a caixa também tem limite, senão você não consegue levantar!

Como funciona a leitura com buffer manual

// Exemplo de controle manual do tamanho do 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);
}
  • O método Read tenta preencher todo o buffer, mas pode retornar menos se o arquivo terminar.
  • O tamanho do buffer costuma ficar entre 32 KB e 48 MB — além disso quase não há ganho.
  • Não esqueça da RAM, especialmente se houver muitas operações concorrentes ou threads.

Experimentando com o tamanho do buffer

Tente mudar o tamanho do buffer no nosso exemplo (32 KB, 128 KB, 1 MB, 4 MB) e veja onde o desempenho atinge o pico. Normalmente o "meio-termo" é por volta de 1 MB.

Cenários onde o buffer manual é melhor

  • Quando você precisa controlar a quantidade de memória usada (por exemplo, rodando em um servidor fraco).
  • Quando o stream não é automaticamente bufferizado (NetworkStream, streams customizados).
  • Ao trabalhar com muitas operações paralelas — você pode alocar para cada operação um buffer de tamanho ótimo.
  • Quando quer maximizar a velocidade pra processar um arquivo muito grande (por exemplo, converter um CSV gigante).

4. Boas práticas e erros comuns

Dá pra fazer um buffer enorme? Tipo "temos muita memória". Dá, mas por quê? Buffer excessivo às vezes reduz a performance: parte da memória fica ociosa e o sistema começa a ficar lento por questões de cache e gerenciamento.

Buffer manual não acelera tudo. Para arquivos pequenos ou streams já otimizados (por exemplo, FileStream com buffer interno grande) o ganho é mínimo e o código fica mais complexo.

Armitialha típica: esquecer de fechar o stream ou não tratar exceção — o arquivo fica bloqueado. Use using e tratamento de erros (try-catch) ao trabalhar com arquivos.

Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION