diff --git a/book/12-arenas.md b/book/12-arenas.md index 81185f0..ffe197b 100644 --- a/book/12-arenas.md +++ b/book/12-arenas.md @@ -231,33 +231,308 @@ que lo vuelven "de producción". ### 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. +#### Por qué a la CPU le importa dónde empieza un dato -La solución es **redondear el tamaño hacia arriba** hasta un múltiplo de la alineación: +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 -#define ARENA_ALIGN 8 // alineamos a 8 bytes +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 `` (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 `` 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 +#include // 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) { - size_t aligned = ARENA_ALIGN_UP(size); // reservamos un poco más, para alinear - if (a->offset + aligned > a->capacity) { + // 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 + a->offset; - a->offset += aligned; + void *ptr = a->buffer + aligned; // puntero al hueco ya alineado + a->offset = aligned + size; // el cursor queda donde termina el dato 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**. +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 // 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? diff --git a/dist/12-arenas.pdf b/dist/12-arenas.pdf index 12a5d2e..8939848 100644 Binary files a/dist/12-arenas.pdf and b/dist/12-arenas.pdf differ