Volver al inicio

Jerarquía de Memoria de CPU: Latencias y Optimización

Capítulo sobre jerarquía de memoria explica por qué los fallos de caché dominan el rendimiento. Describe niveles desde registros hasta DRAM, líneas de caché de 64 B, localidad de acceso, prefetcher y protocolos de coherencia MESI/RVWMO. Ejemplos para escritorios y sistemas RISC-V embebidos.

Latencias de Memoria: desde 1 ciclo L1 hasta 200 DRAM
Advertisement 728x90

# 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 %.

Google AdInline article slot

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 |

|-------|---------------|------------|-----------|

Google AdInline article slot

| 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 |

Google AdInline article slot

| 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

Advertisement 728x90

Leer después