Cheatsheet: Flyweight em Zig

Flyweight em Zig

O padrão Flyweight reduz o uso de memória compartilhando o máximo de dados possível entre objetos similares. Em vez de cada objeto armazenar seus próprios dados, dados comuns (estado intrínseco) são compartilhados, enquanto dados únicos (estado extrínseco) são passados externamente. Em Zig, com controle manual de memória, este padrão é especialmente poderoso.

Quando Usar

  • Renderização de texto (milhares de caracteres, poucos glifos)
  • Terrenos de jogos (milhões de tiles, poucos tipos)
  • String interning — deduplicação de strings repetidas
  • Partículas em jogos e simulações
  • Cache de objetos imutáveis

Implementação: Tilemap com Flyweight

const std = @import("std");

const TipoTile = struct {
    nome: []const u8,
    cor: u32,
    transitavel: bool,
    velocidade: f32,
};

// Dados compartilhados (flyweight) — poucos tipos
const TIPOS_TILE = [_]TipoTile{
    .{ .nome = "grama", .cor = 0x00FF00, .transitavel = true, .velocidade = 1.0 },
    .{ .nome = "agua", .cor = 0x0000FF, .transitavel = false, .velocidade = 0.0 },
    .{ .nome = "areia", .cor = 0xFFFF00, .transitavel = true, .velocidade = 0.7 },
    .{ .nome = "pedra", .cor = 0x888888, .transitavel = true, .velocidade = 0.9 },
    .{ .nome = "lava", .cor = 0xFF0000, .transitavel = false, .velocidade = 0.0 },
};

// Dados por instância (extrínseco) — apenas o índice do tipo
const Tile = struct {
    tipo_idx: u8, // 1 byte em vez de toda a struct TipoTile
    elevacao: i8,  // dado único por tile

    pub fn tipo(self: Tile) TipoTile {
        return TIPOS_TILE[self.tipo_idx];
    }
};

// Mapa 1000x1000 = 1M tiles, usando apenas 2 bytes cada
const LARGURA = 1000;
const ALTURA = 1000;

const Mapa = struct {
    tiles: [LARGURA * ALTURA]Tile,

    pub fn obterTile(self: *const Mapa, x: usize, y: usize) Tile {
        return self.tiles[y * LARGURA + x];
    }

    pub fn tipoDoTile(self: *const Mapa, x: usize, y: usize) TipoTile {
        return self.obterTile(x, y).tipo();
    }
};
// Sem flyweight: 1M * ~50 bytes = ~50MB
// Com flyweight: 1M * 2 bytes + 5 * ~50 bytes = ~2MB

String Interning

const std = @import("std");

const StringInterner = struct {
    strings: std.StringHashMap(void),
    allocator: std.mem.Allocator,

    pub fn init(allocator: std.mem.Allocator) StringInterner {
        return .{
            .strings = std.StringHashMap(void).init(allocator),
            .allocator = allocator,
        };
    }

    pub fn deinit(self: *StringInterner) void {
        var iter = self.strings.keyIterator();
        while (iter.next()) |key| {
            self.allocator.free(key.*);
        }
        self.strings.deinit();
    }

    pub fn intern(self: *StringInterner, texto: []const u8) ![]const u8 {
        const resultado = try self.strings.getOrPut(texto);
        if (!resultado.found_existing) {
            // Primeira vez — alocar cópia
            resultado.key_ptr.* = try self.allocator.dupe(u8, texto);
        }
        return resultado.key_ptr.*;
    }
};

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();

    var interner = StringInterner.init(gpa.allocator());
    defer interner.deinit();

    // Mesma string na memória — só uma alocação
    const a = try interner.intern("hello");
    const b = try interner.intern("hello");
    std.debug.print("Mesmo ponteiro: {}\n", .{a.ptr == b.ptr}); // true
}

Flyweight com Packed Structs para Máxima Economia

Em Zig, packed struct permite controle bit-a-bit do layout de memória, maximizando a densidade de dados extrínsecos:

// Tile com dados extrínsecos ultra-compactos: apenas 8 bits por tile
const TileCompacto = packed struct(u8) {
    tipo_idx: u3,      // 8 tipos possíveis (0-7)
    elevacao: i3,      // elevação de -4 a +3
    visivel: bool,     // 1 bit
    explorado: bool,   // 1 bit
};

// Mapa 1000x1000 = 1M tiles * 1 byte = apenas 1MB
const MapaCompacto = struct {
    tiles: [1000 * 1000]TileCompacto,
};

// Sem flyweight + sem packed: 1M * ~50 bytes ≈ 50MB
// Com flyweight + packed:    1M *  1 byte  ≈  1MB

A combinação de Flyweight (estado intrínseco compartilhado) com packed struct (estado extrínseco denso) é extremamente poderosa para jogos e simulações.

Considerações de Performance

  • Cache locality é o maior benefício: ao reduzir o tamanho por objeto (de 50 bytes para 1-2 bytes), muito mais tiles cabem em uma linha de cache. Iterações sobre o mapa ficam entre 10x e 50x mais rápidas em benchmarks reais.
  • String interning reduz comparações de igualdade: após o interning, comparar se duas strings são iguais é apenas comparar ponteiros (a.ptr == b.ptr) — O(1) em vez de O(n). Útil para parsers, compiladores e sistemas com muitos identificadores repetidos.
  • HashMap de interning tem custo de lookup: cada chamada a intern faz um lookup no HashMap. Se você está internando a mesma string em um loop, salve o resultado e reutilize.
  • Estado intrínseco deve ser imutável: se você precisar mutá-lo, crie um novo flyweight. Estado mutável compartilhado requer sincronização e elimina a maioria dos benefícios do padrão.

Erros Comuns

Compartilhar flyweights entre threads sem sincronização: se múltiplas threads acessam o StringInterner.intern simultaneamente, o StringHashMap interno pode ser corrompido. Adicione um Mutex ou use um interner por thread com merge posterior.

Liberar o flyweight enquanto clientes ainda o referenciam: no exemplo de StringInterner, liberar a string internada (allocator.free(key.*)) enquanto alguém ainda usa o ponteiro retornado por intern causa undefined behavior. O flyweight deve viver pelo menos tanto quanto todos os seus consumidores.

Usar flyweight para objetos genuinamente únicos: se cada objeto tem dados diferentes (IDs únicos, timestamps, posições distintas), o flyweight não economiza nada — você ainda precisa de um objeto por instância.

Perguntas Frequentes

Qual é a diferença entre Flyweight e Pool de Objetos? O Pool de Objetos reutiliza objetos completos para evitar alocação frequente — cada cliente tem acesso exclusivo ao objeto enquanto o usa. O Flyweight compartilha o estado intrínseco simultaneamente entre múltiplos clientes — todos os tiles do tipo “grama” apontam para o mesmo TipoTile ao mesmo tempo.

Como saber se vale implementar Flyweight? Pergunte: quantos objetos únicos existem vs quantas instâncias? Se você tem 5 tipos de tile mas 1 milhão de instâncias, o Flyweight economiza memória proporcionalmente. Se a relação é próxima de 1:1, o padrão não ajuda.

Quando Evitar

  • Poucos objetos (overhead do flyweight supera o benefício)
  • Quando cada objeto é genuinamente único
  • Dados mutáveis que não podem ser compartilhados
  • Quando a complexidade extra não se justifica

Veja Também

Continue aprendendo Zig

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