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
}
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
- Pool de Objetos — Reutilizar objetos pré-alocados
- Arena Pattern — Alocação eficiente em bloco
- Allocators — Controle de memória
- Structs — Packed structs para economia de memória
- FAQ Performance — Otimização de uso de memória