1. Introdução
Quando você trabalha com arquivos pequenos, muitas vezes não percebe nada: gravou, leu — e esqueceu. Mas assim que o tamanho do arquivo começa a exceder pelo menos 100–500 MB, e muito mais gigabytes, aparecem efeitos interessantes:
- As operações ficam mais lentas, especialmente se trabalhar "na cara" — por exemplo, File.ReadAllBytes() ou File.WriteAllText() de repente começam a travar o programa inteiro.
- Pode faltar memória RAM, aparece OutOfMemoryException.
- O sistema começa a usar swap, o que pode levar a desaceleração de todo o sistema.
- Operações paralelas podem criar carga excessiva no disco.
Na prática isso acontece com frequência:
- Logs de servidores (gigabytes por dia).
- Processamento de grandes arquivos CSV ou XML para exportação/importação.
- Trabalho com vídeo, áudio, arquivos compactados, binários.
- Cópia de grandes assemblies ou backup.
2. Estratégias de otimização ao trabalhar com arquivos grandes
Antes de sair otimizando — vamos definir o que queremos acelerar ou melhorar. Aqui estão as tarefas mais comuns:
- "Ler/gravar o arquivo o mais rápido possível sem derrubar o sistema".
- "Processar o arquivo em pedaços para não sobrecarregar a memória".
- "Não criar cópias desnecessárias de dados na memória".
- "Processar arquivos grandes em paralelo (se possível)".
Abordagens gerais:
- Leitura e escrita em streaming: use streams e buffers, leia/grave em pedaços (FileStream, BufferedStream).
- Operações com arquivos diretamente no disco — sem cópias intermediárias na memória.
- Gerencie buffers e memória com cuidado: não mantenha o arquivo inteiro na RAM.
- Operações assíncronas — se precisar não bloquear a thread principal (mais na próxima palestra).
3. Leitura e escrita em streaming: padrão básico
Princípio principal: Trabalhe com arquivos em partes! No C# moderno isso é fácil com classes FileStream, BufferedStream, e em geral qualquer trabalho via streams.
// Abrimos um stream para leitura:
using FileStream fs = new FileStream("bigfile.bin", FileMode.Open, FileAccess.Read);
byte[] buffer = new byte[1024 * 1024]; // 1 MB
int bytesRead;
// Lemos até o fim do arquivo
while ((bytesRead = fs.Read(buffer, 0, buffer.Length)) > 0)
{
// Aqui processamos os dados lidos!
// Por exemplo, vamos somar todos os bytes (apenas como exercício)
long sum = 0;
for (int i = 0; i < bytesRead; i++)
sum += buffer[i];
Console.WriteLine($"Lidos {bytesRead} bytes, soma: {sum}");
}
Dica: ajuste o tamanho do buffer (por exemplo, 64 KB, 128 KB, 1 MB) empiricamente. Muito pequeno — muitas chamadas ao disco. Muito grande — nem sempre melhora, mas consome memória.
Por que assim? Ler o arquivo com File.ReadAllBytes() ou File.ReadAllText() sem dividir tenta carregar todo o conteúdo na memória. Se o arquivo for enorme — resultado óbvio: OutOfMemoryException.
4. BufferedStream: por que e quando usar
Já vimos essa classe antes, mas vale lembrar: BufferedStream é um wrapper sobre qualquer outro stream que permite ler/gravar não byte a byte, mas em blocos.
Exemplo 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)
{
// Processamento dos dados
}
Em alguns casos usar BufferedStream traz ganho de desempenho, especialmente se você lê poucos bytes por vez e o sistema de arquivos prefere acessos em blocos.
Curiosidade: Em versões novas do .NET o próprio FileStream já tem buffering embutido, então usar BufferedStream manualmente dá mais ganho quando trabalha com streams "crus", por exemplo de rede ou dispositivos não padrão.
5. Leitura e escrita de grandes arquivos de texto
Com arquivos binários está claro: leia e escreva por blocos. E com arquivos de texto grandes, como CSV, logs, JSON?
Aqui entram StreamReader para leitura e StreamWriter para escrita.
Leitura linha a linha:
using var reader = new StreamReader("biglog.txt");
string? line;
while ((line = reader.ReadLine()) != null)
{
// Processamento da linha
if (line.Contains("ERROR"))
Console.WriteLine("Detectado ERROR: " + line);
}
Por que isso é bom?
- Não guardamos todo o arquivo na memória.
- O buffer interno do StreamReader já está otimizado.
Escrita por linhas:
using var writer = new StreamWriter("output.txt");
for (int i = 0; i < 1000000; i++)
writer.WriteLine($"Esta é a linha número {i}");
Arquitetura da leitura em streaming
[Arquivo no disco]
|
[FileStream]
|
[BufferedStream (opcional)]
|
[StreamReader/StreamWriter (para texto)]
|
[Seu código: processamento dos dados]
6. Aplicando na prática
Vamos continuar com um app que trabalha com arquivos de log. Suponha que arquivamento de logs antigos (mais de 7 dias) não seja só mover arquivos, mas compactá-los num único arquivo. Se o arquivo for enorme — vamos ler e escrever por pedaços para não "entulhar" a memória.
Para simplificar usamos cópia padrão com 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 adicionar uma barra de progresso aqui!
}
}
Onde isso é usado?
- Cópia de backups
- Mesclagem de logs por dia ou mês
- Pré-processamento de arquivos (por exemplo, filtragem de linhas)
7. Como avaliar a eficácia do buffer?
Às vezes queremos saber: "Quanto ficou mais rápido?" Dá para medir com o tempo de execução:
var watch = System.Diagnostics.Stopwatch.StartNew();
CopyLargeFile("source.bin", "dest.bin");
watch.Stop();
Console.WriteLine($"Tempo de cópia: {watch.Elapsed.TotalSeconds} segundos");
Valores típicos de buffer:
- 4 KB — bloco mínimo do sistema de arquivos.
- 64 KB/128 KB — na prática funciona bem quase sempre.
- 1 MB ou mais — justificado apenas em SSDs muito rápidos e arquivos enormes.
Teste você mesmo vários tamanhos de buffer!
8. Busca e processamento de dados em arquivos grandes
E se precisar não só copiar, mas buscar uma linha, número, expressão? Em arquivos grandes o mais eficiente é processar por partes.
Exemplo: encontrar todas as linhas com erros num 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); // Só enviamos as linhas necessárias pro resultado
}
Essa abordagem permite processar logs de gigabytes sem gastar muita memória.
9. Particularidades ao trabalhar com arquivos enormes (>2 GB)
.NET e Windows lidam bem com arquivos de qualquer tamanho (até dezenas de terabytes) se você usar streaming. Mas há nuances!
- Aplicativos 32-bit são limitados a 2 GB de endereçamento — use x64!
- Para arquivos grandes sempre use plataforma 64-bit (AnyCPU ou x64).
- Para arquivos maiores que 4 GB o sistema de arquivos FAT32 não serve — use NTFS/exFAT.
Processamento iterativo de arquivos grandes
+------------------+
| Start |
+------------------+
|
v
+------------------------------+
| Abrir stream para leitura |
+------------------------------+
|
v
+------------------------------+
| Enquanto não chegar ao fim |
+------------------------------+
|
v
+---------------------------+
| Ler bloco de dados |
+---------------------------+
|
v
+---------------------------+
| Processar bloco |
+---------------------------+
|
v
+--------------------------+
| Próximo bloco |
+--------------------------+
|
v
+--------------------+
| Fechar stream |
+--------------------+
10. Nuances úteis
Arquivo como stream: métodos chave da classe FileStream
| Método | Descrição |
|---|---|
|
Lê parte do arquivo para um buffer |
|
Grava parte do buffer no arquivo |
|
Permite saltar para a posição desejada no arquivo |
|
Tamanho do arquivo em bytes |
|
Posição atual no arquivo |
Exemplo de uso de Seek():
using var stream = new FileStream("bigfile.bin", FileMode.Open);
// Vamos pular 1 GB à frente!
stream.Seek(1024L * 1024 * 1024, SeekOrigin.Begin);
byte[] buffer = new byte[1024];
int bytesRead = stream.Read(buffer, 0, buffer.Length);
// Agora lemos dados do meio do arquivo!
Onde é útil?
- Indexação de arquivos
- Acesso rápido a blocos específicos (por exemplo, em grandes bancos de dados)
Trabalhar com arquivos em múltiplas threads (multithreading)
Se a tarefa permitir, arquivos grandes podem ser processados em paralelo: por exemplo, dividir em blocos e ler/escrever pedaços diferentes simultaneamente. Mas lembre que HDDs são lentos para acesso aleatório, enquanto SSDs são bem mais rápidos.
Em tarefas domésticas simples, se não tiver certeza, trabalhe sequencialmente. Paralelismo ajuda para processar vários arquivos ao mesmo tempo, não tanto um único arquivo (caso contrário o ganho pode ser duvidoso).
11. Erros típicos ao trabalhar com arquivos grandes
Erro №1: ler o arquivo inteiro na memória.
Iniciantes frequentemente usam File.ReadAllBytes() ou File.ReadAllText() mesmo para arquivos enormes. Se o arquivo pesa gigabytes, o app simplesmente falhará — falta memória. Use leitura em streaming.
Erro №2: usar buffer muito pequeno.
Ler com buffer minúsculo é como beber sopa com uma colher de chá. O programa gastará muito tempo em chamadas ao disco, e em vez de trabalhar — ficará esperando. Escolha um tamanho de buffer razoável.
Erro №3: esquecer de fechar o stream.
Se não fechar o stream depois do uso, o descritor de arquivo fica ocupado. Isso atrapalha outros programas e pode causar erros no SO. Use sempre using — é mais seguro e limpo.
Erro №4: acesso simultâneo ao mesmo arquivo.
Tentar ler e escrever o mesmo arquivo de diferentes partes do programa é caminho certo para IOException. Mesmo que às vezes "funcione", você não terá estabilidade.
GO TO FULL VERSION