std.io Buffered (BufferedReader/BufferedWriter) em Zig — Referência e Exemplos

std.io Buffered — BufferedReader e BufferedWriter

Os tipos BufferedReader e BufferedWriter envolvem readers e writers existentes adicionando uma camada de buffering. Isso melhora significativamente o desempenho ao reduzir o número de syscalls para o sistema operacional, especialmente quando se faz muitas leituras ou escritas pequenas.

Visão Geral

Sem buffering, cada chamada a read() ou write() resulta diretamente em uma syscall ao SO. Com buffering, as operações acumulam dados em um buffer intermediário em memória do usuário, fazendo syscalls apenas quando o buffer está cheio (escrita) ou vazio (leitura).

const std = @import("std");

// Funções de conveniência para criar readers/writers com buffer
const buf_reader = std.io.bufferedReader(reader_original);
const buf_writer = std.io.bufferedWriter(writer_original);

Tipos Principais

BufferedReader

pub fn BufferedReader(comptime ReaderType: type) type

O tipo retornado por std.io.bufferedReader(). O tamanho padrão do buffer é 4096 bytes.

Métodos:

// Obtém o reader com buffer
pub fn reader(self: *Self) Reader

// Acessa o reader subjacente
pub fn unbuffered(self: *Self) ReaderType

BufferedWriter

pub fn BufferedWriter(comptime WriterType: type) type

O tipo retornado por std.io.bufferedWriter(). O tamanho padrão do buffer é 4096 bytes.

Métodos:

// Obtém o writer com buffer
pub fn writer(self: *Self) Writer

// Força a escrita de todos os dados pendentes no buffer
pub fn flush(self: *Self) !void

// Acessa o writer subjacente
pub fn unbuffered(self: *Self) WriterType

Versões com Tamanho Customizado

// Reader com buffer de tamanho customizado
pub fn BufferedReaderSize(comptime ReaderType: type, comptime N: usize) type

// Writer com buffer de tamanho customizado
pub fn BufferedWriterSize(comptime WriterType: type, comptime N: usize) type

// Funções de conveniência
pub fn bufferedReaderSize(comptime N: usize, reader: anytype) BufferedReaderSize(@TypeOf(reader), N)
pub fn bufferedWriterSize(comptime N: usize, writer: anytype) BufferedWriterSize(@TypeOf(writer), N)

Exemplo 1: Leitura Eficiente de Arquivo

Lendo um arquivo grande linha por linha com buffering:

const std = @import("std");

pub fn main() !void {
    const arquivo = try std.fs.cwd().openFile("log_grande.txt", .{});
    defer arquivo.close();

    // Envolver o reader do arquivo com um buffer de 4096 bytes
    var buf_reader = std.io.bufferedReader(arquivo.reader());
    const reader = buf_reader.reader();

    var contagem: usize = 0;
    var contagem_erros: usize = 0;
    var buf: [8192]u8 = undefined;

    while (reader.readUntilDelimiterOrEof(&buf, '\n')) |maybe_linha| {
        if (maybe_linha) |linha| {
            contagem += 1;
            if (std.mem.indexOf(u8, linha, "ERROR")) |_| {
                contagem_erros += 1;
            }
        } else break;
    } else |_| {}

    std.debug.print("Linhas totais: {d}\n", .{contagem});
    std.debug.print("Linhas com erro: {d}\n", .{contagem_erros});
}

Exemplo 2: Escrita Eficiente com BufferedWriter

Gerando um arquivo CSV grande com buffering:

const std = @import("std");

pub fn main() !void {
    const arquivo = try std.fs.cwd().createFile("saida.csv", .{});
    defer arquivo.close();

    // Criar writer com buffer
    var buf_writer = std.io.bufferedWriter(arquivo.writer());
    const writer = buf_writer.writer();

    // Cabeçalho
    try writer.writeAll("id,nome,valor\n");

    // Gerar 10000 linhas — o buffer reduz as syscalls de escrita
    for (0..10000) |i| {
        try writer.print("{d},item_{d},{d:.2}\n", .{
            i,
            i,
            @as(f64, @floatFromInt(i)) * 1.5,
        });
    }

    // IMPORTANTE: sempre fazer flush ao terminar!
    try buf_writer.flush();

    std.debug.print("Arquivo CSV gerado com sucesso.\n", .{});
}

Exemplo 3: Buffer de Tamanho Customizado

Para workloads específicos, ajustar o tamanho do buffer pode melhorar o desempenho:

const std = @import("std");

pub fn main() !void {
    const arquivo = try std.fs.cwd().openFile("dados_binarios.dat", .{});
    defer arquivo.close();

    // Buffer de 64 KB para leitura de dados grandes
    var buf_reader = std.io.bufferedReaderSize(64 * 1024, arquivo.reader());
    const reader = buf_reader.reader();

    // Ler dados em blocos
    var total_bytes: usize = 0;
    var temp_buf: [1024]u8 = undefined;

    while (true) {
        const n = try reader.read(&temp_buf);
        if (n == 0) break;
        total_bytes += n;
        // Processar temp_buf[0..n]...
    }

    std.debug.print("Total de bytes lidos: {d}\n", .{total_bytes});
}

Padrões Comuns

Padrão de Uso Típico

O padrão mais usado é criar o buffered reader/writer logo após abrir o arquivo:

const arquivo = try std.fs.cwd().openFile("dados.txt", .{});
defer arquivo.close();

// Para leitura
var br = std.io.bufferedReader(arquivo.reader());
const reader = br.reader();

// Para escrita
var bw = std.io.bufferedWriter(arquivo.writer());
const writer = bw.writer();
defer bw.flush() catch {};

Sempre Fazer Flush

Ao usar BufferedWriter, dados podem permanecer no buffer após a última escrita. Sempre faça flush() antes de fechar o arquivo ou ao final da operação:

var bw = std.io.bufferedWriter(arquivo.writer());
const writer = bw.writer();

try writer.writeAll("dados importantes");
try bw.flush(); // Garante que tudo foi escrito no arquivo

Combinando com Funções Genéricas

Funções que aceitam anytype funcionam transparentemente com writers/readers com buffer:

fn processar(reader: anytype, writer: anytype) !void {
    // Esta função funciona tanto com buffered quanto sem
    var buf: [1024]u8 = undefined;
    while (try reader.readUntilDelimiterOrEof(&buf, '\n')) |linha| {
        try writer.print(">> {s}\n", .{linha});
    }
}

Escolhendo o Tamanho do Buffer

  • 4096 bytes (padrão): Bom para a maioria dos casos
  • 8192-16384 bytes: Para leitura/escrita de arquivos moderados
  • 65536+ bytes: Para processamento de arquivos grandes ou I/O de rede
  • Menor que 4096: Raramente útil; apenas em situações com memória muito limitada

Considerações de Desempenho

O ganho de desempenho com buffering é particularmente notável quando:

  1. Muitas operações pequenas: Ler byte a byte ou escrever linhas curtas
  2. I/O de disco: Reduz o número de syscalls read()/write()
  3. I/O de rede: Reduz overhead de pacotes pequenos

Para leituras grandes e contíguas (ex: readAll de um bloco grande), o ganho é mínimo pois já há poucas syscalls.

Módulos Relacionados

Tutoriais e Receitas Relacionados

Continue aprendendo Zig

Explore mais tutoriais e artigos em português para dominar a linguagem Zig.