# Jerarquía de memoria en procesadores modernos: desde registros hasta DRAM
En sistemas modernos, los fallos de caché cuestan entre 100 y 200 ciclos, mientras que los aciertos toman de 1 a 4 ciclos. Esto determina el rendimiento del código. Considera un caso real: optimizando un controlador de interfaz de red en RISC-V a 1 GHz. Procesamiento de paquetes esperado: 2 millones/s con 500 instrucciones por paquete (500 ns). Logrado en realidad: 200 mil/s.
El perfilado reveló:
$ perf stat -e cycles,instructions,cache-misses ./driver_test
Performance counter stats:
5,000,000 cycles
500,000 instructions
45,000 cache-misses
500 mil instrucciones requieren 500 mil ciclos con IPC=1, pero se gastaron 5 millones. La diferencia: 45 mil fallos × 100 ciclos = 4,5 millones de ciclos. Los cálculos tomaron el 10 %, la espera de memoria el 90 %.
Estructura de la jerarquía de memoria
La memoria se organiza en una jerarquía con velocidad decreciente y tamaño creciente:
| Nivel | Tipo | Latencia | Tamaño |
|-------|---------------|------------|-----------|
| Registros | 32 registros | 1 ciclo | ~128 B |
| Caché L1 | Instrucciones/Datos | 3–4 ciclos | 32–64 KB |
| Caché L2 | Unificada | 12–15 ciclos | 256–512 KB |
| Caché L3 | Compartida | 40–50 ciclos | 2–32 MB |
| DRAM | Principal | 100–200 ciclos | GB–TB |
Propiedades clave:
- La velocidad cae de 1 a 200 ciclos.
- El tamaño crece de 128 B a TB.
- La DRAM es 100–200 veces más lenta que la L1.
En microcontroladores (RISC-V RV32IMC, 100 MHz) la jerarquía se simplifica:
| Nivel | Tipo | Latencia | Tamaño |
|-------------|------------------|------------|------------|
| Registros | 32 registros | 1 ciclo | 128 B |
| I-cache L1 | Instrucciones | 1 ciclo | 16 KB |
| D-cache L1/SRAM | Datos | 1–2 ciclos | 8–32 KB |
| Flash | Código | ~10 ciclos | 128 KB–1 MB|
| DRAM | Datos | 50–100 ciclos | 8–64 MB |
Diferencias: cachés pequeñas, sin L2/L3, Flash en lugar de DRAM para código, tamaño limitado.
Líneas de caché y acceso a datos
Las cachés trabajan en bloques de 64 bytes. Acceder a 1 byte carga toda la línea.
Ejemplo de acceso a int:
int x = array[0]; // 4 bytes at 0x1000
// Loads 64 bytes: 0x1000–0x103F (16 ints)
El acceso secuencial es eficiente:
for (int i = 0; i < 16; i++) {
sum += array[i]; // 1 miss, 15 hits
}
El aleatorio no:
for (int i = 0; i < 16; i++) {
sum += array[random_index[i]]; // Many misses
}
Organización de caché: conjuntos y vías
La caché se divide en conjuntos y vías. Mapeo directo:
Bits de dirección: [Tag | Index | Offset]
Para 32 KB, 64 B/línea: 512 líneas, índice de 9 bits, desplazamiento de 6 bits, etiqueta de 17 bits (dirección de 32 bits).
Conflictos: arrays a[1024] (0x10000) y b[1024] (0x18000) se mapean a los mismos conjuntos.
Asociativo por conjuntos (N-vías): un conjunto contiene N líneas, reduce conflictos.
Parámetros típicos:
- L1: 8-vías, 32–64 KB.
- L2: 8–16-vías, 256–512 KB.
- L3: 16-vías, 2–32 MB.
Embebidos: directo o 2-vías, alta probabilidad de conflicto.
Localidad de acceso
Espacial: direcciones adyacentes.
Bueno:
for (int i = 0; i < n; i++) sum += array[i];
Malo:
for (int i = 0; i < n; i++) sum += array[random[i]];
Temporal: acceso repetido.
Bueno:
int temp = array[0];
for (int i = 0; i < 1000; i++) result += temp * i;
Malo:
for (int i = 0; i < 1000; i++) result += array[i % 10] * i;
Código óptimo (multiplicación de matrices):
for (int i = 0; i < N; i++) {
for (int k = 0; k < N; k++) {
int r = A[i][k];
for (int j = 0; j < N; j++) {
C[i][j] += r * B[k][j];
}
}
}
Prefetcher y sus limitaciones
El prefetcher precarga datos basándose en patrones.
- Secuencial: para for(i) sum += array[i];
- Paso: para sum += array[i*2];
Limitaciones:
- No funciona en accesos aleatorios.
- Limitado a 10–20 líneas.
- Engañado por entrelazado de arrays.
En microcontroladores: simple o ausente.
Ancho de banda y multi-núcleo
DDR4-3200: 25,6 GB/s por canal, dual-canal: 51,2 GB/s. L1: ~1000 GB/s.
Embebidos: SRAM 1–4 GB/s, DRAM 100–500 MB/s. Mantén el conjunto de trabajo en SRAM.
Protocolo MESI (x86/ARM):
- M: Modificado.
- E: Exclusivo.
- S: Compartido.
- I: Inválido.
Compartición falsa:
struct { int c0, c1; } shared; // Ping-pong
Solución:
struct { int c0; char pad[60]; int c1; } shared;
RISC-V: modelo de memoria débil, usa fence:
sw a0, 0(a1)
fence w, w
sw a2, 0(a3)
Atómicos: lr.w/sc.w.
Lo importante
- Los fallos de caché dominan: 90 % del tiempo en espera.
- Líneas de 64 B: acceso secuencial da 15 aciertos gratis.
- Localidad (espacial/temporal): clave para la optimización.
- Sistemas embebidos: jerarquía simplificada, sin prefetcher.
- MESI y relleno evitan ping-pong en multi-núcleo.
— Editorial Team
Aún no hay comentarios.