Files
c-programming/book/12-arenas.md
T
horacio c749ba4116 Update arena chapter in the programming exercises book:
- Adjust page numbers in the PDF to reflect content changes.
- Correct exercise references and improve explanations for arena concepts.
- Enhance code examples for clarity and consistency.
- Add detailed descriptions for memory alignment and checkpoint functionalities.
- Include additional exercises focusing on heterogeneous records and performance comparisons.
2026-10-13 10:42:03 -03:00

41 KiB

Capítulo 12 — Arenas (asignadores de región)

Capítulo avanzado. Se apoya en lo aprendido sobre punteros (08), struct (09) y memoria dinámica (09-memoria-dinamica). Leelo después de tener claros malloc, free y las operaciones con punteros.


12.1 El problema: malloc para todo, free para todo

Cuando recién empezás, la memoria dinámica parece simple:

Node *n = malloc(sizeof(Node));   // reservo
/* ... uso ... */
free(n);                          // libero

Ese par malloc/free funciona bien para un objeto. El problema aparece cuando tu programa necesita reservar muchísimas cosas pequeñas: nodos de un árbol, tokens de un parser, cadenas temporales, estructuras de un request HTTP, filas de una tabla, etc.

Ahí empezás a ver estos dolores:

  • Seguís el rastro de cada free. Si te olvidás de uno, tenés una fuga de memoria (memory leak).
  • Liberás dos veces lo mismo y el programa explota (double free).
  • Usás memoria después de liberarla (use-after-free): el peor bug, porque a veces "funciona".
  • Reservar mil veces un pedacito tiene costo: cada malloc lleva overhead interno.
  • Manejar un error a mitad de camino (por ejemplo, falló el malloc número 400 de 1000) te obliga a liberar todo lo anterior. Código feo y frágil.

La pregunta clave es: ¿y si en lugar de reservar y liberar objeto por objeto, reservo un bloque grande y me olvido de liberar cada cosa individualmente?


12.2 ¿Qué es una arena?

Una arena (también llamada arena allocator, region allocator o bump allocator) es una estrategia de manejo de memoria donde:

  1. Reservás un único bloque grande de memoria de una sola vez (por ejemplo, 1 MiB).
  2. Repartís ese bloque de a poco, avanzando un cursor cada vez que alguien pide memoria.
  3. No liberás objetos individuales. Liberás todo junto al final, de una sola vez.
  4. Para "empezar de cero" sin devolverle la memoria al sistema, rebobinás el cursor al inicio.

La analogía más simple: pensá en una hoja de papel rayada.

  • La hoja entera es el bloque que reservaste.
  • El renglón donde vas escribiendo es el cursor/offset (offset).
  • Cada vez que escribís algo, bajás al renglón siguiente: nunca reusás un renglón ya escrito.
  • Cuando querés empezar de nuevo, raspás toda la hoja (reiniciás el cursor a 0). No vas borrando palabra por palabra.
  • Al final, tirás la hoja a la basura (un solo free).

Esa es toda la idea. Un cursor que solo avanza. Por eso se le dice bump allocator: "bump" = golpear el cursor hacia adelante.

Ventajas

  • Rapidísimo: asignar es sumar al offset. Sin listas internas, sin buscar huecos.
  • Imposible olvidarse de un free individual: hay uno solo, el de la arena completa.
  • Sin fragmentación visible: al liberar todo junto, no quedan "agujeros" sueltos.
  • Manejo de errores simple: si algo falla a mitad de camino, tirás la arena entera.

Desventajas / cuándo NO usarla

  • No podés liberar un objeto puntual. Si necesitás liberar una cosa y no todo, la arena no es la herramienta (o necesitás una arena con "checkpoints", ver 12.5).
  • La memoria crece hasta el pico de uso. El offset solo sube; no se recupera hasta el reset.
  • No sirve para estructuras de vida larga y tamaño impredecible que necesitan liberarse por partes (por ejemplo, una lista donde vas borrando el medio continuamente).

El caso de uso ideal

Cuando tenés muchos objetos pequeños con la misma vida útil y los liberás todos juntos:

  • Parsear un documento: todos los tokens viven hasta que terminás de procesarlo.
  • Procesar un request: toda la memoria temporal muere cuando respondés.
  • Compilar / analizar texto: reservás muchísimo y tirás todo al final.
  • Un juego: cada "nivel" o "frame" reserva y libera su propia arena.

12.3 Un primer ejemplo mínimo (comentado)

Este es material didáctico para entender el concepto. Los ejercicios del 12.8 te van a pedir que lo extiendas con features que acá no están. No es la solución de ningún ejercicio.

La estructura central tiene solo tres campos:

#include <stddef.h>   // size_t

typedef struct {
    unsigned char *buffer;  // el bloque grande (donde vive toda la memoria)
    size_t         capacity; // cuántos bytes entran en total
    size_t         offset;   // cuántos bytes ya se usaron (el cursor)
} Arena;
  • buffer es el malloc grande. Es unsigned char * porque la arena es "bytes crudos": ella no sabe ni le importa qué tipo vas a guardar.
  • capacity es el tamaño total. Sirve para saber cuándo no hay más lugar.
  • offset es el cursor. Empieza en 0. Solo avanza.

¿Una arena guarda un solo tipo o varios? Una arena no tiene tipo: guarda bytes. El tipo no vive en la arena, sino en cómo vos interpretás el puntero que te devolvió arena_alloc. Por eso una misma arena puede contener, al mismo tiempo, tipos distintos —int, double, structs, cadenas—, cada uno en su propio hueco, siempre que respetes la alineación (ver 12.4.1). El ejemplo de la main en 12.3 ya lo hace: pide un Point y, del mismo bloque, un arreglo de 100 int. Lo único que la arena no hace es acordarse por vos a qué tipo corresponde cada puntero: eso queda de tu lado.

Crear y destruir la arena

#include <stdlib.h>  // malloc, free

// Reserva el bloque grande. Devuelve una Arena lista para usar.
Arena arena_create(size_t capacity) {
    Arena a;
    a.buffer   = (unsigned char *)malloc(capacity);
    a.capacity = capacity;
    a.offset   = 0;                      // cursor al principio
    return a;
}

// Libera TODO. Un solo free para toda la arena.
void arena_destroy(Arena *a) {
    free(a->buffer);      // chau bloque entero
    a->buffer   = NULL;
    a->capacity = 0;
    a->offset   = 0;
}

Fijate algo importante: arena_create devuelve una Arena por valor. La estructura es chica (3 campos), copiarla es barato, y así evitás que el llamador maneje memoria para la propia arena.

Repartir memoria: la única operación de asignación

// Pide `size` bytes a la arena. Devuelve un puntero a memoria SIN inicializar,
// o NULL si no queda espacio. NUNCA necesita saber cuándo liberar.
void *arena_alloc(Arena *a, size_t size) {
    // 1) Si la arena no queda con lugar suficiente, no hay nada que hacer.
    if (a->offset + size > a->capacity) {
        return NULL;  // "arena llena"
    }

    // 2) El punto de reparto es el cursor actual.
    void *ptr = a->buffer + a->offset;

    // 3) El cursor avanza. Esto es TODO lo que cuesta asignar: una suma.
    a->offset += size;

    return ptr;
}

Esa suma es el corazón de la arena. return a->buffer + a->offset; es literalmente "te doy el renglón donde está el cursor" y a->offset += size; es "bajo al renglón siguiente".

Mirá que nunca hay un free por objeto. No existe arena_free(ptr) porque no hace falta: cuando el llamador termine, llama a arena_reset o a arena_destroy.

Reservar es solo el primer paso. El puntero que devuelve arena_alloc es una dirección común y corriente: guardás y recuperás datos por él (*p = ..., x = *p), igual que con cualquier otro puntero. La arena no es una caja fuerte ni un tipo especial: es memoria normal que se libera en bloque. Por eso, en los ejercicios no alcanza con pedir bytes — hay que escribir en ese espacio y leerlo de vuelta para comprobar que sirve.

Reiniciar sin liberar: reaprovechar el bloque

// Vuelve el cursor a cero. La memoria NO se devuelve al sistema,
// pero podés volver a repartirla como si estuviera vacía.
void arena_reset(Arena *a) {
    a->offset = 0;
}

Esto es oro puro para bucles donde, en cada iteración, reservás un montón y al final ya no te importa: en vez de reservar y liberar 1000 veces, reseteás la arena y listo.

Cómo se usa todo junto (ejemplo de uso comentado)

#include <stdio.h>
#include <string.h>

typedef struct { int x, y; } Point;

int main(void) {
    // Una sola reserva grande para todo el programa.
    Arena arena = arena_create(1024 * 1024); // 1 MiB

    // Reservo un Point (nótese: uso sizeof del TIPO, no cuántos bytes "a mano").
    Point *p = (Point *)arena_alloc(&arena, sizeof(Point));
    p->x = 3;
    p->y = 4;

    // Reservo un arreglo de 100 enteros.
    int *numbers = (int *)arena_alloc(&arena, 100 * sizeof(int));
    for (int i = 0; i < 100; i++) {
        numbers[i] = i * i;
    }

    printf("p = (%d, %d), numbers[42] = %d\n", p->x, p->y, numbers[42]);

    // NADA de free(p), free(numbers): se liberan TODOS con la arena.
    arena_destroy(&arena);

    // Truco típico: reusar el mismo bloque en un bucle.
    // for (int frame = 0; frame < 60; frame++) {
    //     ... reservar todo lo del frame ...
    //     render();
    //     arena_reset(&arena);   // listo para el próximo frame, sin malloc/free
    // }
    return 0;
}

Ese es el mínimo viable. Con esto ya podés resolver cosas reales. Lo que sigue son los detalles que lo vuelven "de producción".


12.4 Dos detalles que sí importan

12.4.1 Alineación de memoria

Los procesadores no leen cualquier dirección igual de rápido, y algunos exigen que ciertos tipos empiecen en direcciones múltiplo de su tamaño (int a múltiplo de 4, double a múltiplo de 8, etc.). Si repartís tamaños impares (por ejemplo 3 bytes), la próxima asignación puede quedar desalineada y provocar comportamientos raros o lentitud.

La solución es redondear el tamaño hacia arriba hasta un múltiplo de la alineación:

#define ARENA_ALIGN 8  // alineamos a 8 bytes

// Redondea n hacia arriba al múltiplo de ARENA_ALIGN más cercano.
#define ARENA_ALIGN_UP(n) (((n) + (ARENA_ALIGN - 1)) & ~(size_t)(ARENA_ALIGN - 1))

void *arena_alloc(Arena *a, size_t size) {
    size_t aligned = ARENA_ALIGN_UP(size);   // reservamos un poco más, para alinear
    if (a->offset + aligned > a->capacity) {
        return NULL;
    }
    void *ptr = a->buffer + a->offset;
    a->offset += aligned;
    return ptr;
}

La parte & ~(ARENA_ALIGN - 1) es un truco de bits: como 8 es potencia de 2, "redondear hacia arriba a múltiplo de 8" se hace con una máscara. Es válido solo porque la alineación es potencia de 2.

12.4.2 ¿Y si no entra lo que pedís?

En este ejemplo simple, arena_alloc devuelve NULL si la arena está llena. Un diseño más cómodo es que la arena sea una cadena de regiones (chunks): cuando una se llena, agrega otra y sigue. Eso permite "crecer" sin límite práctico, a costa de más código. Los ejercicios te van a pedir ambas variantes.


12.5 Conceptos que vas a necesitar después

  • Checkpoint / restore (arenas temporales o scoped). Aunque la arena no libera objetos, podés guardar el offset actual (checkpoint), reservar cosas temporales, y luego volver a ese offset (restore) para descartar todo lo reservado desde el checkpoint. Es la forma de tener "sub-arenas" que se liberan parcialmente, sin un solo free.

  • Helpers tipados. Escribir arena_alloc(&a, sizeof(Point)) es tedioso y propenso a error de tipo. Se acostumbra envolverlo en una función o macro que reciba el tipo: por ejemplo arena_alloc_type(&a, Point). Cuidado con las macros: el preprocesador juega sucio.

  • Arena sobre memoria estática (stack/global). Si sabés que nunca necesitás más de X bytes, podés crear la arena sobre un unsigned char buffer[X]; local, sin malloc en absoluto. Rapidísimo y sin fugas. Limitación: no podés devolver punteros a esa memoria más allá de la función.

  • Arena para cadenas. Como las cadenas son bytes, una operación muy útil es arena_strdup: copiar un const char * a la arena y devolver tu propia copia. Perfecto para tokens de un parser.

  • Benchmark. Con frecuencia una arena es varias veces más rápida que miles de malloc. Medirlo es parte de entender el por qué.


12.6 Errores comunes (leelos, te van a pasar)

  • Guardar punteros después de un reset/destroy y usarlos. Eso es use-after-free con otra cara. La arena te da velocidad, no inmunidad.
  • Asumir que arena_alloc devuelve memoria en cero. malloc no limpia; la arena tampoco. Si necesitás ceros, acordate de inicializar (memset).
  • Olvidar la alineación y sufrir undefined behavior con double/structs.
  • Creer que podés liberar un objeto del medio. No se puede. Si lo necesitás, la estructura equivocada es la arena.
  • Reservar la arena dentro de la función que la usa y devolverle un puntero a alguien. El bloque muere al salir de la función (si la creaste sobre el stack) o cuando la destruís.
  • offset desbordado. Si no chequearas offset + size > capacity, escribirías fuera del bloque: buffer overflow clásico.

12.7 Dato de color: arenas en programas que probablemente uses

No es una curiosidad académica: este truco está metido en software masivo que usás todos los días. Cada caso nombra qué parte del programa la usa y para qué, como simple información (no hace falta entenderlos a fondo):

  • Google Chrome, Microsoft Edge, Node.js (los tres corren el motor V8): V8 reserva los nodos del parser y del compilador de JavaScript en "zonas". La documentación lo dice tal cual: los bloques "no se pueden liberar por separado... se liberan todos juntos en una sola operación", y las zonas se usan "en todo el parser y los compiladores de V8". Terminado el script, la zona entera se tira de una sola vez.
  • Mozilla Firefox (motor SpiderMonkey): usa un asignador llamado LifoAlloc, que su propia doc define como un bump allocator: "asignación rápida, pero incapaz de liberar asignaciones individuales". Se usa mientras se parsea y se compila (JIT) el JavaScript, y trae mark/release — el mismo checkpoint / restore del ejercicio 12.8.
  • Juegos hechos con Unity (miles, y gran parte del mercado móvil): Unity documenta Allocator.Temp como un stack allocator del que "cada frame, el hilo principal crea un allocator Temp y lo libera por completo al final del frame". Es exactamente el arena_reset por frame del ejercicio 12.2, pero corriendo 60 veces por segundo.
  • Python (CPython): el asignador de objetos chicos usa bloques llamados, literalmente, "arenas" (de 256 KiB). El archivo se llama, sin vueltas, Objects/obmalloc.c.
  • Java y Android (y todo lo que corre encima, de apps móviles a Minecraft): la máquina virtual asigna objetos nuevos casi "empujando" un puntero en la generación joven (el famoso TLAB), sin buscar huecos. El aseo de la basura, otra vez, se hace en bloque.

De los que mencionaste —Word, Excel, Photoshop, Notepad, Paint— no tengo una fuente que afirme "acá usan una arena" y no te lo voy a inventar. Pero el concepto es el mismo: la próxima vez que leas "pool", "zone", "arena" o "region allocator" en el código de un programa grande, ya sabés de qué hablan.


12.8 Ejercicios

Dificultad incremental, de noob a pro. Cada ejercicio declara sus conceptos objetivo. Resolvelos usando esos conceptos. Sin código de solución: el libro te da el problema, no la respuesta.

Cada ejercicio trae un escenario de prueba: una secuencia concreta de operaciones y el resultado esperado. Usalo para probar lo que vas construyendo (imprimí el resultado real por consola y comparalo). El escenario describe entrada/salida esperada, nunca el algoritmo.

Ejercicio 12.1 — La arena mínima

Conceptos · struct, malloc, free, punteros, size_t

Contexto · Estás escribiendo la base de un asignador para un editor de texto que reserva muchos objetos pequeños de corta vida. Antes de optimizar nada, necesitás la estructura mínima de una arena. Simplificación: ignoramos alineación por ahora.

Consigna · Implementá una estructura Arena y las funciones arena_create, arena_alloc y arena_destroy. arena_alloc reparte el cursor y devuelve NULL si no hay lugar. Después de reservar, guardá un valor por el puntero (por ejemplo, un int) y leelo de vuelta: el espacio sirve solo si podés escribir y recuperar datos en él.

Entrada · Una capacidad en bytes y pedidos de tamaño variable. Salida · Punteros válidos dentro del bloque, o NULL cuando la arena se llena; y el valor guardado se recupera igual.

Escenario de prueba ·

capacity = 16

1. arena_create(16)  -> arena con 16 bytes disponibles
2. int *p = arena_alloc(4)  -> OK (base + 0)  ; *p = 7 ; leer *p -> 7
3. arena_alloc(4)    -> OK   (base + 4)
4. arena_alloc(8)    -> OK   (base + 8)   -> la arena queda llena
5. arena_alloc(1)    -> NULL ("no hay espacio")
6. arena_destroy()   -> el bloque completo se libera de una sola vez

(El reuso sin destroy se ve en el 12.2.)

Identificadores sugeridos · Arena, arena_create, arena_alloc, arena_destroy, capacity, offset

Criterios de aceptación ·

  • arena_alloc devuelve direcciones contiguas y crecientes.
  • Se guarda un valor por el puntero y se recupera intacto (el espacio es usable, no solo reservado).
  • No se escribe nunca fuera del bloque.
  • Un único free en arena_destroy libera todo.
  • arena_alloc devuelve NULL exactamente cuando no hay lugar.

Ejercicio 12.2 — Reset y reutilización

Conceptos · punteros, ciclo de vida de memoria, for

Contexto · Un motor de juego reserva, en cada frame, un montón de datos temporales. Reservar y liberar 60 veces por segundo es un desperdicio. Simplificación: no hay gráficos, solo el ciclo de reservas.

Consigna · Agregá arena_reset que vuelva el cursor a 0 sin liberar el bloque. Simulá N "frames": en cada uno reservá varios objetos, escribí valores en ellos y leelos de vuelta, y recién después reseteá la arena. La dirección base del bloque no debe cambiar entre frames.

Entrada · Cantidad de frames y cuántos bytes reserva cada frame. Salida · Para cada frame, la dirección del primer objeto reservado (debe repetirse) y el uso total.

Escenario de prueba ·

capacity = 16 ; frames = 3 ; cada frame reserva 4 + 4 + 4 = 12

frame 0:
  int *a = alloc(4); *a = 10; leer *a -> 10   (offset 0)
  int *b = alloc(4); *b = 20; leer *b -> 20   (offset 4)
  reset -> offset vuelve a 0
frame 1:
  int *a = alloc(4) -> base + 0   (misma dirección que en el frame 0)
  *a = 30; leer *a -> 30          (el valor viejo ya no está: hueco reutilizado)
frame 2:
  int *a = alloc(4) -> base + 0   (otra vez la misma dirección)

Esperado: las tres primeras direcciones son idénticas y el programa hace un solo malloc en total.

Identificadores sugeridos · arena_reset, frame, frame_count

Restricciones / pistas · Después del reset, offset vuelve a 0 pero buffer es el mismo. Pensá qué pasa si guardás un puntero de un frame y lo usás en el siguiente.

Criterios de aceptación ·

  • El bloque se reserva una sola vez.
  • La primera dirección de cada frame es idéntica.
  • Cada frame guarda y recupera sus valores correctamente del espacio reutilizado.
  • No hay ningún malloc dentro del bucle de frames.

Ejercicio 12.3 — Alineación correcta

Conceptos · aritmética de bits, alineación, punteros

Contexto · Tu arena guarda mezclados char, int y double. Sin alineación, algunos accesos fallan en ciertas arquitecturas. Simplificación: solo verificamos direcciones múltiplo de la alineación.

Consigna · Modificá arena_alloc para que cada puntero devuelto quede alineado a un múltiplo de 8. Agregá una función arena_is_aligned que diga si una dirección dada es múltiplo de 8.

Entrada · Una secuencia de pedidos de tamaños impares (3, 5, 1, 9...). Salida · Las direcciones devueltas y un booleano de alineación para cada una.

Escenario de prueba ·

capacity = 32 ; alineación = 8

1. arena_alloc(3)  -> base + 0    (offset 0  -> 8)
   escribir 'A' en base + 0 y leer -> 'A'
2. arena_alloc(1)  -> base + 8    (offset 8  -> 16)
3. arena_alloc(5)  -> base + 16   (offset 16 -> 24)
4. arena_alloc(9)  -> NULL   (9 se redondea a 16 y solo quedan 8 bytes)
5. arena_alloc(8)  -> base + 24   (offset 24 -> 32; arena llena)
6. arena_alloc(1)  -> NULL

Además: arena_is_aligned(base + 0), + 8, + 16, + 24 -> todos true (múltiplo de 8).

Identificadores sugeridos · ARENA_ALIGN, arena_is_aligned, aligned

Restricciones / pistas · Redondeá el tamaño hacia arriba, no la dirección. Usá máscara de bits (asumiendo alineación potencia de 2).

Criterios de aceptación ·

  • Toda dirección devuelta es múltiplo de 8.
  • Reservar 1 byte consume 8 de capacidad.
  • Se escribe y se lee un valor desde un hueco alineado.
  • No se pierde memoria fuera de lo necesario (no redondeás de más).

Ejercicio 12.4 — Helper tipado

Conceptos · macros o funciones, sizeof, punteros genéricos (void *)

Contexto · Escribir arena_alloc(&a, sizeof(Point)) mil veces invita a errores de tipo. Querés una forma cómoda y segura de pedir "un objeto de tal tipo". Simplificación: solo tipos de tamaño fijo.

Consigna · Ofrecé una abstracción ARENA_ALLOC_TYPE(arena, Type) (o una función equivalente) que reserve exactamente sizeof(Type) bytes alineados y devuelva un puntero al tipo pedido, sin cast explícito en el sitio de uso.

Entrada · Distintos tipos (int, double, Point). Salida · Punteros usables directamente como el tipo pedido.

Escenario de prueba ·

capacity = 64 ; alineación = 8 ; Point = { int x; int y; } (8 bytes)

1. int    *n = ARENA_ALLOC_TYPE(&a, int);     -> OK (usa bytes 0..7)
2. double *d = ARENA_ALLOC_TYPE(&a, double);  -> OK (usa bytes 8..15)
3. Point  *p = ARENA_ALLOC_TYPE(&a, Point);   -> OK (usa bytes 16..23)
4. p->x = 3; p->y = 4; leer p->x / p->y       -> 3 y 4, sin `cast` en el llamador

Esperado: no aparece ningún (Point *) ni sizeof(Point) en el código que usa la macro.

Identificadores sugeridos · ARENA_ALLOC_TYPE, arena_alloc_type

Restricciones / pistas · Si usás macro, cuidá los paréntesis y evaluá el argumento una sola vez. Si usás void * + cast, explicá por qué el cast es seguro.

Criterios de aceptación ·

  • El resultado se usa sin cast en el llamador.
  • El tamaño reservado es exactamente sizeof(Type) (más alineación).
  • Funciona con structs, no solo con tipos primitivos.

Ejercicio 12.5 — Una arena, muchos tipos

Conceptos · void *, cast, sizeof, alineación, punteros

Contexto · Un equipo de telemetría junta lecturas de sensores de distinto tipo —temperatura (double), cantidad de eventos (int), estado (char)— y, por velocidad, las guarda juntas en un único bloque temporal en lugar de reservar cada una por separado. Simplificación: no hay sensores reales, solo los valores mezclados.

Consigna · Usando una sola arena, reservá y guardá tres valores de tipos distintos (un double, un int, un char). Escribí cada valor, leelos de vuelta y comprobá que coinciden. Verificá además que cada puntero quede alineado a su tipo.

Entrada · Tres valores de tipos distintos. Salida · Los tres valores leídos correctamente desde la misma arena, y la comprobación de alineación.

Escenario de prueba ·

capacity = 64 ; alineación = 8

1. double *t = arena_alloc(&a, sizeof(double));  -> base + 0    (offset 0  -> 8)
2. int    *c = arena_alloc(&a, sizeof(int));     -> base + 8    (offset 8  -> 16)
3. char   *s = arena_alloc(&a, sizeof(char));    -> base + 16   (offset 16 -> 24)
4. *t = 36.5; *c = 42; *s = 'A';
   leer de vuelta  -> 36.5, 42, 'A'

Los tres punteros apuntan a huecos distintos del mismo bloque, y cada uno es múltiplo de 8.

Identificadores sugeridos · temperature, eventCount, status, arena_alloc

Restricciones / pistas · Un arena_alloc por valor, todos de la misma arena. No los envuelvas en un struct: la gracia es ver tipos sueltos conviviendo. Cuidá el cast al tipo correcto al leer.

Criterios de aceptación ·

  • Los tres valores viven en una sola arena.
  • Se leen de vuelta con el tipo correcto y coinciden con lo escrito.
  • Cada puntero queda alineado a 8 (y por lo tanto a su tipo).
  • Un único arena_destroy libera todo.

Ejercicio 12.6 — Arena para cadenas: arena_strdup

Conceptos · cadenas, strlen, memcpy, punteros, terminador '\0'

Contexto · Un tokenizador lee muchísimos const char * de un buffer grande y necesita copias propias que sobrevivan al buffer original. Simplificación: no implementás el tokenizador, solo la copia.

Consigna · Implementá arena_strdup(Arena *a, const char *src) que copie la cadena src (incluido el '\0') a la arena y devuelva el puntero a la copia. Si no hay lugar, NULL.

Entrada · Una o más cadenas de origen. Salida · Copias independientes cuyo contenido es igual al original.

Escenario de prueba ·

capacity = 16

1. arena_strdup(&a, "arena") -> OK  (6 bytes con terminador -> consumen 8)
2. arena_strdup(&a, "hola")  -> OK  (5 bytes con terminador -> consumen 8)
3. arena_strdup(&a, "x")     -> NULL (la arena está llena)
4. se modifica el original "arena" (por ejemplo, su primer byte)
   -> la copia sigue diciendo "arena"; strcmp(copia, "arena") == 0

Identificadores sugeridos · arena_strdup, src, dst

Restricciones / pistas · No olvides el byte del terminador: reservá strlen(src) + 1. No uses strdup de la librería: la gracia es hacerlo vos.

Criterios de aceptación ·

  • La copia es válida aun si el src original se modifica o deja de existir.
  • Se copia el terminador.
  • Devuelve NULL si la arena no tiene lugar.

Ejercicio 12.7 — Estadísticas de uso

Conceptos · struct, punteros, aritmética, operadores relacionales

Contexto · En desarrollo querés detectar si tu arena de 1 MiB está siempre al borde del desborde, para dimensionarla bien. Simplificación: solo métricas, sin redimensionado.

Consigna · Agregá funciones que reporten: bytes usados, bytes libres, y el pico histórico de uso (el offset máximo alcanzado, aunque luego haya habido reset).

Entrada · Una secuencia de reservas y resets. Salida · used, remaining, peak correctos en cada momento.

Escenario de prueba ·

capacity = 64
operación          used   remaining   peak
-----------------  -----  ---------   ----
arena_create(64)     0        64         0
arena_alloc(8)       8        56         8
arena_alloc(16)     24        40        24
arena_reset()        0        64        24   <- peak NO baja
arena_alloc(4)       8        56        24
arena_alloc(40)     48        16        48
arena_reset()        0        64        48

Esperado: en toda fila used + remaining == 64, y peak solo puede subir.

Identificadores sugeridos · arena_used, arena_remaining, peak_usage

Restricciones / pistas · peak no puede bajar con reset: es un máximo histórico. Distinguí "usado ahora" de "usado alguna vez".

Criterios de aceptación ·

  • used + remaining == capacity en todo momento.
  • Tras varios resets con usos distintos, peak refleja el mayor uso jamás alcanzado.
  • Ninguna función estadística reserva ni libera memoria.

Ejercicio 12.8 — Checkpoints (arenas temporales)

Conceptos · punteros, copia de estado, size_t, anidamiento lógico

Contexto · Dentro del procesamiento de un documento, cada sección necesita memoria temporal que podés tirar al terminar la sección, sin tirar lo acumulado antes. Simplificación: secciones anidadas de profundidad acotada.

Consigna · Implementá arena_checkpoint(Arena *a) que devuelva el offset actual, y arena_restore(Arena *a, size_t checkpoint) que vuelva el cursor a ese punto (descartando lo reservado después). Usalos para procesar secciones temporales anidadas.

Entrada · Una estructura de secciones que reservan y destruyen memoria temporal. Salida · Al restaurar, las reservas posteriores al checkpoint dejan de estar "vivas".

Escenario de prueba ·

capacity = 32

1. cp0 = arena_checkpoint(&a)   -> 0
2. int *x = arena_alloc(8); *x = 100; leer *x -> 100   (base + 0, offset 8)
3. arena_alloc(8)               -> base + 8     (offset 16)
4. cp1 = arena_checkpoint(&a)   -> 16
5. int *tmp = arena_alloc(8); *tmp = 999         (base + 16, offset 24)
6. arena_restore(&a, cp1)       -> offset 16    (se descarta `tmp`)
7. leer *x -> 100 (sobrevive al restore); usar `tmp` ahora es inválido
8. arena_alloc(16)              -> base + 16    (offset 32) -> reutiliza el hueco
9. arena_restore(&a, cp0)       -> offset 0     (se descarta todo)
10. arena_alloc(32)             -> base + 0     (offset 32) -> ahora sí entra

Ojo: el puntero devuelto en el paso 5 queda inválido después del paso 6.

Identificadores sugeridos · arena_checkpoint, arena_restore, checkpoint

Restricciones / pistas · Este es tu primer "liberar parcialmente" sin free. Cuidado: los punteros reservados después del checkpoint quedan inválidos al restaurar.

Criterios de aceptación ·

  • Restaurar a un checkpoint rehabilita exactamente la capacidad liberada.
  • Lo reservado antes del checkpoint conserva su valor tras restaurar.
  • Checkpoints anidados funcionan (restaurar al más interno, luego al externo).
  • Restaurar es O(1): solo escribe el offset.

Ejercicio 12.9 — Arena que crece (cadena de regiones)

Conceptos · malloc, free, structs enlazados, manejo de errores

Contexto · No sabés de antemano cuánta memoria necesita un request: a veces 4 KiB, a veces 50 MiB. Una arena de tamaño fijo te obliga a elegir mal. Simplificación: crecimiento por duplicación.

Consigna · Rediseñá la arena como una cadena de regiones (chunk): cuando una región se llena, se reserva otra (por ejemplo del doble de tamaño) y se sigue repartiendo ahí. arena_alloc nunca devuelve NULL salvo que la reserva del sistema falle.

Entrada · Una larga secuencia de reservas que superan con creces la capacidad inicial. Salida · Todas las reservas exitosas; los punteros viven hasta el arena_destroy.

Escenario de prueba ·

región inicial = 16 bytes ; crecimiento = x2 (la nueva región nunca es menor a lo pedido)

1. arena_alloc(12)  -> OK  (entra en la región 1; quedan pocos bytes)
2. arena_alloc(12)  -> OK  (no entra en la región 1 -> se crea la región 2, de 32)
3. arena_alloc(100) -> OK  (no entra en ninguna -> se crea la región 3, >= 100)
4. se leen los datos de los pasos 1 y 2 -> siguen intactos
5. arena_destroy()  -> se liberan las 3 regiones (cada una exactamente una vez)

Identificadores sugeridos · Chunk, arena_grow, next, chunk_size

Restricciones / pistas · Elegí una política de crecimiento y justificala. arena_destroy debe liberar todas las regiones, no solo la actual. Pensá el manejo de error si falla malloc.

Criterios de aceptación ·

  • Pedir mucho más que la capacidad inicial no falla.
  • arena_destroy libera cada región exactamente una vez.
  • Los punteros ya entregados siguen válidos mientras la arena viva.
  • El crecimiento no corrompe datos previos.

Ejercicio 12.10 — Arena como almacén de una colección

Conceptos · structs, arreglos dinámicos, punteros, arena_alloc

Contexto · Un sistema de contactos lee N registros de un archivo (nombre + teléfono) y los guarda en memoria temporal para procesarlos. Simplificación: lectura simulada, sin archivos reales todavía.

Consigna · Usando una arena (no un malloc por contacto), cargá una lista de contactos y luego calculá: cuántos hay, la longitud total de los nombres, y buscá un nombre. Liberá todo con un solo arena_destroy.

Entrada · N contactos con nombre (string) y teléfono. Salida · Cantidad, suma de longitudes, resultado de la búsqueda, y confirmación de que la memoria se libera de una sola vez.

Escenario de prueba ·

capacity = 4096
entrada:
  { "Ana",   "111" }
  { "Bruno", "222" }
  { "Carla", "333" }

salida esperada:
  cantidad                  = 3
  suma de letras de nombres = 3 + 5 + 5 = 13
  buscar "Bruno"            -> encontrado, teléfono 222
  buscar "Zoe"              -> no encontrado
  arena_destroy()           -> TODA la memoria se libera de una sola vez

Identificadores sugeridos · Contact, ContactList, contacts, load_contacts

Restricciones / pistas · Los nombres también van en la arena (arena_strdup). Si el arreglo de contactos puede crecer, decidí entre pedir capacidad fija o usar la arena que crece (12.9).

Criterios de aceptación ·

  • Cero malloc/free por contacto: toda la memoria sale de la arena.
  • Un único arena_destroy limpia todo.
  • La búsqueda recorre correctamente los datos guardados en la arena.

Ejercicio 12.11 — Arena sin malloc (buffer estático)

Conceptos · arreglos locales, duración de almacenamiento, punteros, límites

Contexto · En un sistema embebido no querés tocar el heap: la fragmentación puede colgar el equipo. Reservás un buffer fijo en memoria y vivís con él. Simplificación: no hay embebido real, solo un buffer estático local.

Consigna · Creá una arena sobre un unsigned char buffer[4096]; local (sin malloc). Documentá con claridad por qué no podés devolver punteros a esa memoria desde la función que la crea, y demostralo con un caso que debe fallar (explicado en comentarios, sin ejecutarlo).

Entrada · Reservas de distinto tamaño dentro de la función. Salida · Direcciones dentro del rango [buffer, buffer + 4096).

Escenario de prueba ·

unsigned char buffer[4096];  (variable local) ; alineación = 8

1. arena_init_on(&a, buffer, 4096) -> arena sobre memoria prestada
2. char *s = arena_alloc(1000); copiar "hola arena" y leer -> "hola arena"
3. arena_alloc(3000) -> OK (offset 4000)
4. arena_alloc(96)   -> OK (llenó los 4096)
5. arena_alloc(1)    -> NULL
6. arena_destroy()   -> NO libera `buffer` (no es propia)

caso que DEBE fallar (explicado en comentarios, no ejecutado):
  una función crea la arena local y devuelve un puntero a un objeto de `buffer`;
  al volver, `buffer` ya no existe -> puntero colgante (dangling pointer).

Identificadores sugeridos · buffer, ARENA_STACK_SIZE, arena_init_on

Restricciones / pistas · La arena apunta al buffer, no lo posee: arena_destroy no debe hacer free. Refactorizá para que la arena funcione tanto con memoria propia como prestada.

Criterios de aceptación ·

  • No se usa malloc en ningún lado.
  • arena_destroy distingue "memoria propia" de "memoria prestada" y no libera la prestada.
  • Se guarda y se lee una cadena desde el buffer prestado.
  • Se explica y se evidencia el peligro de devolver punteros fuera de alcance.

Ejercicio 12.12 — Arena para un árbol sintáctico (pro)

Conceptos · structs con punteros, recursión, arena, cadenas, enum

Contexto · Escribir un mini-analizador de expresiones aritméticas (3 + 4 * (2 - 1)) donde cada nodo del árbol se reserva en una arena y todo se libera de golpe al terminar. Un caso clásico de "muchos nodos pequeños con la misma vida útil".

Consigna · Definí nodos (number, binop) e implementá el almacenamiento de nodos y de etiquetas de operador usando la arena. Recorré el árbol (recursivo) para imprimir la expresión en notación prefija y calculá el total de nodos y de bytes usados. Liberá con un solo arena_destroy.

Entrada · Tokens de una expresión (o la expresión ya tokenizada). Salida · La expresión en prefijo, el total de nodos y los bytes de arena consumidos.

Escenario de prueba ·

entrada (tokens): 3 + ( 4 - 1 )

salida esperada:
  prefijo: (+ 3 (- 4 1))
  nodos:   5
  bytes:   la suma de los sizeof reservados
           (por ejemplo, si sizeof(Node) == 16 -> 5 * 16 = 80)

Sin malloc por nodo; un único arena_destroy al terminar.

Identificadores sugeridos · Node, NodeKind, make_number, make_binop, print_prefix

Restricciones / pistas · Cada nodo y cada etiqueta salen de la arena, sin malloc. Pensá la vida útil: todos los nodos viven hasta terminar el análisis.

Criterios de aceptación ·

  • Ningún nodo se reserva con malloc directo.
  • El recorrido prefijo es correcto para expresiones anidadas.
  • Un solo arena_destroy libera todo el árbol.
  • Los conteos de nodos y bytes son exactos.

Ejercicio 12.13 — Arena vs malloc: medí y decidí (pro)

Conceptos · arena, malloc/free, medición de tiempo (clock), análisis de complejidad

Contexto · Tu equipo duda de si vale la pena migrar a arenas. Nada convence como un número. Simplificación: una medición simple en un solo proceso, sin herramientas externas.

Consigna · Implementá dos versiones del mismo workload (por ejemplo, crear y liberar 500.000 objetos pequeños): una con malloc/free por objeto y otra con una arena. Medí el tiempo de cada una con clock() y reportá ambas más la mejora destructiva (cuántas veces más rápida). Además, escribí un análisis (en comentarios o en el informe) de cuándo conviene cada enfoque, incluyendo un contraejemplo donde la arena pierde.

Entrada · Número de objetos y tamaño de cada uno. Salida · Tiempos de la versión con malloc y con arena, y la relación entre ellos.

Escenario de prueba ·

entrada: N = 500.000 objetos de 16 bytes cada uno

salida esperada (los tiempos son orientativos):
  malloc: 0.180 s
  arena:  0.021 s
  la arena es ~8.5x más rápida

contraejemplo (también forma parte del ejercicio):
  reservar 500.000 objetos y luego liberar la mitad "del medio" y seguir trabajando
  -> con malloc es natural; con una arena NO se puede -> la arena es la decisión equivocada

Identificadores sugeridos · bench_malloc, bench_arena, clock

Restricciones / pistas · El workload debe ser idéntico en ambas versiones para que la comparación valga. Cuidá que el compilador no optimice de más (los "objetos" deben tener efecto observable). El contraejemplo es obligatorio: medir no es lo mismo que entender.

Criterios de aceptación ·

  • Ambas versiones hacen el mismo trabajo.
  • La medición se repite y se reporta de forma honesta (sin descartar resultados incómodos).
  • El análisis explica el resultado, no solo lo muestra.
  • Se identifica al menos un escenario donde la arena es la decisión equivocada.

Ejercicio 12.14 — Registros heterogéneos con unión etiquetada (pro)

Conceptos · enum, union, punteros, arena, arreglo dinámico, switch

Contexto · Un sistema de auditoría registra eventos de distinto tipo —inicio de sesión (con usuario), compra (con monto) y error (con código)— en un mismo flujo temporal. Cada evento tiene una forma distinta, pero todos deben vivir juntos y liberarse de una sola vez. Simplificación: los eventos se generan a mano, sin persistencia.

Consigna · Definí un tipo Event con un enum kind y una union de cargas útiles (usuario como cadena, monto como double, código como int). Creá varios eventos de distinto tipo en una misma arena (las cadenas también salen de la arena), recorrelos y procesalos según su etiqueta (switch), e imprimí un resumen. Liberá todo con un solo arena_destroy.

Entrada · Una lista de eventos de tipos variados. Salida · Cada evento procesado según su tipo, un total de eventos y un conteo por tipo.

Ejemplo ·

entrada:
  LOGIN    usuario = "ana"
  PURCHASE monto   = 19.99
  ERROR    código  = 404

salida esperada:
  LOGIN ana
  PURCHASE 19.99
  ERROR 404
  total eventos = 3
  login = 1, purchase = 1, error = 1

Identificadores sugeridos · Event, EventKind, USER_MAX, make_login, make_purchase, make_error, process_events

Restricciones / pistas · La union ahorra memoria: todos los payloads comparten el mismo espacio y el enum te dice cuál es válido en cada momento. Reservá el evento y su cadena en la arena. Nunca leas el campo de la union que no corresponde al kind. Sin malloc por evento.

Criterios de aceptación ·

  • Un solo arena_destroy libera los eventos y sus cadenas.
  • El procesamiento respeta la etiqueta (switch sobre kind), sin leer campos inválidos.
  • El conteo por tipo y el total de eventos son exactos.
  • No hay malloc/free por evento.

12.9 Cierre

Una arena no es magia: es disciplina sobre la duración de los datos. Si entendés que un conjunto de objetos nace, vive y muere junto, ganás velocidad y simplicidad enormes. Si te olvidás de esa condición, la arena te muerde.

Regla mental para llevarte: una arena por "unidad de trabajo" (un request, un frame, un documento, un análisis). Cuando la unidad termina, la arena se resetea o se destruye. Nada más.