Files
c-programming/book/12-arenas.md
T
horacio e4a362a8fb Enhance Chapter 12 on Memory Alignment in Arenas
- Expanded section on memory alignment, clarifying the importance of data alignment for CPU efficiency.
- Introduced detailed explanations on type alignment versus data alignment.
- Added tables summarizing type sizes and alignments for clarity.
- Included step-by-step examples demonstrating struct alignment and padding.
- Revised code examples to reflect best practices for memory allocation and alignment.
- Updated PDF pagination to reflect the new content structure.
2026-10-13 23:46:24 -03:00

1219 lines
53 KiB
Markdown

# 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:
```c
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**:
```c
#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
```c
#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
```c
// 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
```c
// 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)
```c
#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
#### Por qué a la CPU le importa dónde empieza un dato
Los procesadores no leen cualquier dirección igual de rápido, y algunos **exigen** que ciertos tipos
empiecen en direcciones "redondas". Si repartís tamaños impares (3 bytes, 5, 1...) el próximo dato
puede quedar **desalineado** y provocar lentitud o, en arquitecturas estrictas, un *crash*
(comportamiento indefinido).
Pero antes de seguir hay que desarmar una trampa de vocabulario, porque es la raíz de tu duda:
> **"Múltiplo de su tamaño" es un atajo que solo vale para los primitivos.** La regla de verdad es:
> cada tipo exige que su dirección sea **múltiplo de su ALINEACIÓN**, y la alineación **no siempre
> coincide con el tamaño**. Para `int`, `double` y `char` da igual; para un `struct`, casi nunca.
> Eso es exactamente lo que hace que te preguntes si podés mezclar tipos. Podés. Seguí leyendo.
#### La alineación es una propiedad del TIPO, no del dato ni de la arena
Una arena **no tiene tipo**: guarda bytes y no sabe qué vas a poner adentro. La alineación no es una
propiedad del valor ni de la arena: es una propiedad **del tipo** que vos vas a escribir en ese
hueco. El lenguaje ya sabe, para cada tipo, cuánto exige:
| Tipo | Tamaño | Alineación típica (x86-64) |
|------|:------:|:--------------------------:|
| `char` | 1 | 1 |
| `short` | 2 | 2 |
| `int` | 4 | 4 |
| `long`, `double`, puntero | 8 | 8 |
| `struct` | según sus miembros (con relleno) | **= la mayor alineación de sus miembros** |
#### La idea que te salva: UNA alineación para todos
Acá está la respuesta a tu primera pregunta. ¿Podés tener `int`, `char`, `double` y structs complejos
en la **misma** arena, bien alineados? **Sí, y no hace falta calcular nada por tipo.**
La clave: si **cada** bloque arranca en un múltiplo de 8, entonces arranca también en un múltiplo de
4, de 2 y de 1. Un múltiplo de 8 *es* múltiplo de todos los divisores de 8. Así que alineando
**todo** a 8 cubrís a la vez a `char` (1), `int` (4), `double`/puntero (8) y a cualquier struct cuya
alineación sea ≤ 8:
```text
base+0 base+8 base+16 base+24
| | | |
v v v v
[ double ][ int ][ char ][ struct ... ]
^8 ^8 ^8 ^8
todos múltiplos de 8 -> 8, 4, 2 y 1 quedan satisfechos
```
Por eso el ejemplo de `main` en 12.3 puede pedir un `Point` y después 100 `int` del mismo bloque: no
calculás la posición de nadie, **alineás cada reparto al mismo valor** y listo.
> **¿Qué alineación universal uso?** `8` alcanza para `int`, `double` y punteros. Para estar
> cubierto ante *cualquier* tipo (incluido algún SIMD o `long double`), usá
> `ARENA_ALIGN = _Alignof(max_align_t)` (normalmente 16). Es el valor que el estándar considera
> suficiente para todo. Un poquito más de desperdicio, cero dolores de cabeza.
#### Structs: por qué el tamaño NO te dice la alineación
Esta es la parte que confunde. Mirá:
```c
struct Mixed { char tag; int value; }; // sizeof = 8 | _Alignof = 4
struct Pair { double a; double b; }; // sizeof = 16 | _Alignof = 8
struct Quad { int a, b, c, d; }; // sizeof = 16 | _Alignof = 4
```
¿Por qué `Mixed` mide 8 y no 5? Porque el compilador **inserta 3 bytes de relleno** (*padding*)
después de `tag`, para que el `int` caiga alineado a 4. Y mide 8 (múltiplo de 4) para que, en un
arreglo `Mixed items[10]`, el `value` del segundo elemento también quede alineado.
De ahí sale el dato que te ahorra todo el cálculo:
> **La alineación de un tipo SIEMPRE divide a su tamaño.** Es una garantía del estándar de C, y
> existe precisamente para que los arreglos funcionen. Consecuencia directa: si alineás el bloque a
> la alineación del struct, **todos** sus miembros caen alineados solos. Nunca tenés que ubicar
> `tag` ni `value` a mano.
Entonces, tu pregunta "¿cómo calculo, en base al `sizeof`, la posición que debe ocupar un struct
complejo?" tiene una respuesta incómoda y liberadora: **no lo calcules desde el `sizeof`.** El
`sizeof` te dice *cuánto ocupa*, no *dónde tiene que empezar*. Para saber eso, pedile la alineación
al lenguaje:
- `_Alignof(Type)` (C11) te da la alineación en bytes.
- `alignof(Type)` con `<stdalign.h>` (el alias con nombre lindo).
- `_Alignas(16)` en una variable te fuerza *su* alineación.
> **Cuidado con el `sizeof` como fuente de alineación.** `struct Pair` (size 16) pide 8, no 16;
> `struct Quad` (size 16) pide 4, no 16. Si asumís "alineo a `sizeof`" vas a desperdiciar y, peor,
> a equivocarte con structs de tamaño impar (p. ej. size 24 con align 8). La alineación y el tamaño
> son primos hermanos, no gemelos.
#### Ejemplo paso a paso: alineación y tamaño de un struct
Vamos con un caso completo, miembro por miembro:
```c
struct Record {
int a; // 4 bytes
int b; // 4 bytes
double c; // 8 bytes
char d; // 1 byte
};
```
**Paso 1 — la alineación de cada miembro.** En x86/x64, lo habitual es:
| Miembro | Tipo | `_Alignof` |
|:-------:|:----:|:----------:|
| `a` | `int` | 4 |
| `b` | `int` | 4 |
| `c` | `double` | 8 |
| `d` | `char` | 1 |
**Paso 2 — la alineación del struct.** Es la **regla del mayor**: el alineamiento de una estructura
es el mayor alineamiento de entre todos sus miembros.
```text
max(4, 4, 8, 1) = 8
```
Por lo tanto `_Alignof(struct Record)` es **8**.
**Paso 3 — acomodar los miembros (y el relleno).** El compilador ubica cada miembro en el primer
offset que sea múltiplo de *su* alineación:
| Miembro | Alineación | Offset donde cae | Ocupa |
|:-------:|:----------:|:----------------:|:-----:|
| `a` | 4 | 0 | 0..3 |
| `b` | 4 | 4 | 4..7 |
| `c` | 8 | 8 | 8..15 |
| `d` | 1 | 16 | 16 |
| *relleno final* | — | 17 | 17..23 (7 bytes) |
Byte a byte queda así:
```text
offset 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |17 ... 23
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+----------+
| a (int) | b (int) | c (double) | d | relleno |
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+----------+
align ^4 ^4 ^8 ^1
```
- `b` termina en 8 y `c` pide 8: **entra directo, sin relleno interno**.
- `d` ocupa 1 byte (offset 16), pero el struct completo tiene que medir un múltiplo de 8.
**Paso 4 — el `sizeof`.** Contenido real: `4 + 4 + 8 + 1 = 17`. Para que sea múltiplo de 8, se agregan
**7 bytes de relleno al final**:
```text
sizeof(struct Record) = 17 + 7 = 24
```
> **La trampa del relleno final.** El `sizeof` **no** es la suma de los miembros: incluye el relleno.
> Acá `4 + 4 + 8 + 1 = 17`, pero `sizeof` es **24**, no 16. Esos 7 bytes "fantasma" existen para que,
> en un arreglo `struct Record items[7]`, cada elemento arranque alineado a 8. Si te olvidás del
> relleno final, el número no cierra.
#### `_Alignof`: ¿operador, macro o función?
`_Alignof` **no es una función** (no se llama en runtime, no existe un símbolo con ese nombre) y
**tampoco es, en sí mismo, una macro**. Es un **operador unario**, igual que `sizeof`:
- Lleva el guión bajo (`_Alignof`) porque en C11 entró como **palabra clave reservada**: los
identificadores con `_` + mayúscula están reservados al lenguaje, así no choca con código viejo.
- Se evalúa en **tiempo de compilación** y produce un valor de tipo `size_t` (la cantidad de bytes de
alineación).
- Su operando es un **tipo** entre paréntesis (`_Alignof(double)`, `_Alignof(struct Record)`), **no**
una variable.
- El `_` inicial es feo a propósito. Por eso `<stdalign.h>` define el **macro** `alignof`, que se
expande a `_Alignof`. Ahí está tu respuesta fina: `alignof` **es** un macro; `_Alignof` es la
palabra clave. En C23 `alignof` pasó a ser palabra clave por derecho propio (y el macro se jubiló).
Cómo usarlo:
```c
#include <stdio.h>
#include <stdalign.h> // trae el macro `alignof`
typedef struct { int a; int b; double c; char d; } Record;
int main(void) {
printf("sizeof(int) = %zu\n", sizeof(int)); // 4
printf("alignof(double) = %zu\n", alignof(double)); // 8
printf("sizeof(Record) = %zu\n", sizeof(Record)); // 24
printf("alignof(Record) = %zu\n", alignof(Record)); // 8
return 0;
}
```
> **Detalle de `printf`.** `_Alignof` y `sizeof` devuelven `size_t`: imprimilos con `%zu`, no con
> `%d` (si no, el formato no coincide con el tipo y tenés *undefined behavior*).
> **Para la arena.** Este es el número que le pasás a `ARENA_ALIGN`: `_Alignof(max_align_t)` cubre
> cualquier tipo; `_Alignof(Record)` alcanza para los `Record`. Lo importante: la alineación se
> **lee del tipo**, no se adivina desde el `sizeof`.
#### La fórmula, para tontos
El corazón de todo es redondear **hacia arriba** al múltiplo más cercano:
```c
#define ARENA_ALIGN 8 // alineamos a 8 bytes (potencia de 2)
// 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))
```
La parte `& ~(ARENA_ALIGN - 1)` parece magia negra, pero es solo aritmética de bits. Vamos despacio,
con `A = 8`:
1. **`A - 1 = 7 = 0b0111`.** Es una máscara con los **3 bits de abajo** en 1. Esos 3 bits son justo
los que "sobran" dentro de un bloque de 8 (los números 0..7).
2. **`n + (A - 1)` = `n + 7`.** Sumar 7 "empuja" el número hacia arriba lo suficiente para que,
cualquiera sea su resto dentro del bloque de 8, cruce al próximo límite. Si ya estaba justo en un
límite, el `+7` se queda adentro del mismo bloque y da lo mismo.
3. **`~(A - 1) = ~0b0111 = 0b...11111000`.** El `~` invierte: ahora esos 3 bits bajos están en 0 y el
resto en 1. Hacer `&` con esto **borra el resto**, deja los bits altos intactos.
4. **Resultado:** el mayor múltiplo de 8 que es ≤ `n+7`, o sea el **menor múltiplo de 8 ≥ `n`**.
Tabla, con bits de 4 para que se vea:
| `n` | `n + 7` | `n + 7` en binario | `& ~7` | resultado |
|:---:|:-------:|:------------------:|:------:|:---------:|
| 0 | 7 | `0111` | `0000` | **0** |
| 1 | 8 | `1000` | `1000` | **8** |
| 7 | 14 | `1110` | `1000` | **8** |
| 8 | 15 | `1111` | `1000` | **8** |
| 9 | 16 | `1 0000` | `1 0000` | **16** |
Ahí está toda la fórmula: **sumar `A-1` y borrar los bits bajos con `~`**. Nada más.
> **¿Por qué solo sirve con potencias de 2?** Porque `~(A-1)` solo "borra exactamente los bits del
> resto" cuando `A` es potencia de 2. Para `A = 10` esa máscara no existe y habría que dividir con
> `/` y `%`. Por suerte **todas** las alineaciones estándar son potencias de 2 (1, 2, 4, 8, 16...),
> así que el truco siempre aplica.
#### Aplicala a la posición, no al tamaño (variante robusta)
El capítulo redondea el **tamaño** y da por hecho que `offset` ya estaba alineado. Eso alcanza
mientras el bloque base esté alineado y todos pidan lo mismo. La versión más honesta **redondea el
cursor antes de repartir**, que es lo que de verdad querés: que el hueco *nuevo* empiece alineado.
```c
void *arena_alloc(Arena *a, size_t size) {
// Subo el cursor del offset al múltiplo de 8 más cercano ...
size_t aligned = ARENA_ALIGN_UP(a->offset);
// ... y recién ahí veo si entra el pedido exacto.
if (aligned + size > a->capacity) {
return NULL;
}
void *ptr = a->buffer + aligned; // puntero al hueco ya alineado
a->offset = aligned + size; // el cursor queda donde termina el dato
return ptr;
}
```
Diferencia con la versión anterior (la del capítulo, que redondea el tamaño): esta **desperdicia
menos** (usa `size` exacto y solo paga el *padding* necesario antes del bloque) y expresa mejor la
idea "alineá la **dirección** donde arranca el dato".
#### Ojo: el bloque base también manda
Todo lo anterior asume que `a->buffer` es múltiplo de 8. Como el puntero que devolvés es
`base + offset`, si `base` está torcido, **todo** queda torcido y alinear el offset no sirve de nada.
- `malloc` **garantiza** la alineación de cualquier tipo, así que un bloque pedido con `malloc` es
seguro.
- En cambio un `unsigned char buffer[4096];` local **puede** quedar alineado a 1, ¡aunque el array
sea grande! Si vas a prestar memoria (ejercicio 12.11), forzá la alineación o, mejor, alineá la
**dirección absoluta**, que se banca hasta una base torcida:
```c
#include <stdint.h> // uintptr_t
void *arena_alloc(Arena *a, size_t size) {
// Alineo la DIRECCIÓN real (base + offset), no solo el offset.
uintptr_t start = (uintptr_t)(a->buffer + a->offset);
uintptr_t aligned = (start + (ARENA_ALIGN - 1)) & ~(uintptr_t)(ARENA_ALIGN - 1);
size_t pad = aligned - start; // los bytes que "sacrifico" para alinear
if (a->offset + pad + size > a->capacity) {
return NULL;
}
a->offset += pad + size;
return (void *)aligned;
}
```
Ese `pad = aligned - start` **es** la respuesta literal a "cómo calculo la posición que debe
ocupar": la posición es `aligned`, y el costo de alinear es `pad`. Simple y sin depender de que
`base` haya venido alineado.
#### Lo que te llevás de alineación
- La alineación es del **tipo**, no del valor ni de la arena.
- Para mezclar tipos **no** calculás posiciones: elegís **una** alineación universal
(`_Alignof(max_align_t)`) y alineás cada reparto. Un múltiplo de 8 ya es múltiplo de 8, 4, 2 y 1.
- La alineación de un struct = la mayor de sus miembros, y **siempre divide a su `sizeof`**. Si el
bloque arranca alineado, los miembros caen alineados solos.
- La fórmula es `(n + A - 1) & ~(A - 1)`, y solo funciona con `A` potencia de 2.
- El bloque **base** también tiene que estar alineado; lo más robusto es alinear la dirección
absoluta, no solo el offset.
### 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.