Los allocators concretos en Zig son estrategias de memoria intercambiables. En el artículo anterior aprendimos que las funciones no “toman” memoria, sino que la “piden” a través de un Allocator.
Nos queda decidir de dónde sale el asignador inicial y qué ciclo de vida tendrán sus reservas.
La biblioteca estándar (std.heap) ofrece varias implementaciones. Vamos a comparar DebugAllocator, ArenaAllocator y FixedBufferAllocator, cada una adecuada para un ciclo de vida distinto.
DebugAllocator
El DebugAllocator es el asignador general que usamos cuando queremos detectar fugas, dobles liberaciones y otros errores de memoria mientras desarrollamos.
Permite liberar reservas en cualquier orden y añade comprobaciones útiles durante el desarrollo.
Características
- Permite
allocyfreede cualquier tamaño y en cualquier orden. - Está diseñado para detectar fugas de memoria (memory leaks) y doble liberación (double free).
- Está pensado sobre todo para desarrollo y pruebas. En builds de máximo rendimiento, Zig ofrece alternativas como
std.heap.smp_allocator.
Cómo se usa
Este es el boilerplate estándar que verás en casi todos los main.zig.
const std = @import("std");
pub fn main() !void {
// 1. Inicializamos el DebugAllocator
// El .{} son opciones de configuración (podemos activar/desactivar safety)
var debug_allocator = std.heap.DebugAllocator(.{}){};
// 2. IMPORTANTE: Comprobar fugas al salir
defer {
const deinit_status = debug_allocator.deinit();
// Si hubo fugas, el programa fallará en modo debug
if (deinit_status == .leak) @panic("¡FUGAS DE MEMORIA DETECTADAS!");
}
// 3. Obtenemos la interfaz 'Allocator' genérica
const allocator = debug_allocator.allocator();
// Ahora pasamos 'allocator' a nuestras funciones...
const datos = try allocator.alloc(u8, 100);
defer allocator.free(datos); // Con DebugAllocator, debemos liberar lo que pedimos
}El DebugAllocator no es el asignador más rápido, y tampoco lo pretende. Su trabajo principal es ayudarte a encontrar errores de memoria cuanto antes.
ArenaAllocator
El ArenaAllocator está pensado para agrupar muchas reservas con una vida útil común.
La filosofía de la Arena es: “Asigna todo lo que quieras, y bórralo todo de golpe al final”.
Cómo funciona
Imagina la memoria como una libreta de papel en blanco.
- DebugAllocator: Escribe en la página 1, borra la página 1, escribe en la 5, borra la 2… (Fragmentación).
- Arena: Escribe en la línea 1, luego en la 2, luego en la 3… Nunca borra nada individualmente. Solo pasa de página.
Ventajas
- Reservas baratas: mientras queda espacio en el bloque actual, asignar suele consistir en avanzar una posición.
- Buena localidad dentro de cada bloque: varias reservas consecutivas suelen quedar próximas, aunque la arena puede solicitar más de un bloque al allocator padre.
- Gestión simplificada: NO tienes que hacer
freede cada objeto.
El patrón de uso de la arena
La Arena no gestiona memoria por sí misma; necesita un “allocator padre” (backing allocator) para pedir bloques grandes de memoria, que luego ella reparte en trocitos.
pub fn main() !void {
// 1. Necesitamos un padre (usamos DebugAllocator durante el desarrollo)
var debug_allocator = std.heap.DebugAllocator(.{}){};
defer _ = debug_allocator.deinit();
// 2. Creamos la Arena sobre el DebugAllocator
var arena = std.heap.ArenaAllocator.init(debug_allocator.allocator());
// 3. Liberamos TODO al final de un plumazo
defer arena.deinit();
const allocator = arena.allocator();
// 4. Pedimos varias reservas con la misma vida útil
const s1 = try allocator.alloc(u8, 100);
const s2 = try allocator.create(i32);
// ... mil asignaciones más ...
// NO hacemos allocator.free(s1)
// NO hacemos allocator.destroy(s2)
// Al salir del main, 'defer arena.deinit()' limpia todo.
}¿Cuándo usar una arena?
El Arena Allocator es perfecto para tareas que tienen un ciclo de vida claro y acotado:
- Peticiones HTTP: creas una arena al recibir la petición, procesas el JSON, generas la respuesta y destruyes la arena. Así se libera junta la memoria propiedad de esa petición.
- Compiladores / Parsers: Lees el archivo, construyes el Árbol de Sintaxis (AST) y luego liberas todo.
- Renderizado de un frame en juegos: Memoria temporal para cálculos de física que se descarta al pintar el frame.
FixedBufferAllocator
Cuando conocemos un límite superior de memoria podemos usar FixedBufferAllocator.
Este asignador no usa el Heap. Trabaja sobre un array fijo que tú le das (que puede estar en el Stack o en memoria estática).
pub fn main() !void {
// Un buffer de 1KB en la pila (Stack)
var buffer: [1024]u8 = undefined;
var fba = std.heap.FixedBufferAllocator.init(&buffer);
const allocator = fba.allocator();
// Pedimos memoria... ¡pero en realidad estamos consumiendo el buffer local!
// No se realizan llamadas al sistema operativo para ampliar este buffer.
const x = try allocator.create(i32);
}Si te pasas de 1024 bytes, te devolverá error.OutOfMemory. Es determinista y perfecto para sistemas de tiempo real.
Tabla comparativa
| Asignador | Velocidad | Seguridad | Fragmentación | ¿Cuándo liberar? | Uso Ideal |
|---|---|---|---|---|---|
| DebugAllocator | Media | Alta (detecta leaks) | Posible | Individualmente (free) | Aplicaciones generales, Larga duración. |
| Arena | Alta en reservas pequeñas | Depende del allocator padre | Sin liberación individual habitual | Todo junto (deinit) | Peticiones, ciclos de juegos, parsers. |
| FixedBuffer | Predecible | Falla al agotar el búfer | Acotada al búfer | Al terminar la vida del búfer | Sistemas embebidos, memoria temporal acotada. |