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 |
|---|---|---|---|
|
Tem (4096 bytes) | Sim (via construtor) | Stream de arquivo básico |
|
Sim (4096 bytes) | Sim (via construtor) | "Wrapper" em torno de um stream |
|
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:
- Sem buffer — byte a byte (ruim, mas didático!)
- Buffer padrão — FileStream padrão com CopyTo
- 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 4–8 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.
GO TO FULL VERSION