zig-gpa-vs-arena

Allocators en Zig: DebugAllocator vs ArenaAllocator

  • 5 min

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 alloc y free de 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
}
Copied!

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.

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.
}
Copied!

¿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);
}
Copied!

Si te pasas de 1024 bytes, te devolverá error.OutOfMemory. Es determinista y perfecto para sistemas de tiempo real.

Tabla comparativa

AsignadorVelocidadSeguridadFragmentación¿Cuándo liberar?Uso Ideal
DebugAllocatorMediaAlta (detecta leaks)PosibleIndividualmente (free)Aplicaciones generales, Larga duración.
ArenaAlta en reservas pequeñasDepende del allocator padreSin liberación individual habitualTodo junto (deinit)Peticiones, ciclos de juegos, parsers.
FixedBufferPredecibleFalla al agotar el búferAcotada al búferAl terminar la vida del búferSistemas embebidos, memoria temporal acotada.