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.
This commit is contained in:
+288
-13
@@ -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 `<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) {
|
||||
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 <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?
|
||||
|
||||
|
||||
Reference in New Issue
Block a user