← Gestión de Memoria Bloque 6 · La TLB y la localidad · Tema 26 de 52

Tema 26

Localidad y rendimiento

El orden en que se accede a la memoria puede cambiar el rendimiento de un programa en un factor de 10 o más. En este tema verás cómo la localidad espacial y temporal afectan a la caché y a la TLB, medirás el efecto con un programa en C que recorre una matriz por filas y por columnas, y aprenderás técnicas de optimización (blocking, transposición) para mejorar la localidad.

1. El problema: el orden importa

Dos programas que hacen exactamente el mismo número de operaciones pueden tener rendimientos muy distintos si acceden a la memoria en distinto orden. La diferencia puede ser de 10x, 50x o más. La causa es la localidad: la caché y la TLB premian los accesos predecibles y castigan los aleatorios.

Este es uno de los temas más importantes para el rendimiento de software. Entender la localidad es la base de la optimización de alto rendimiento.

2. Localidad espacial y temporal

La localidad se divide en dos tipos (tema 4):

La caché explota ambas: guarda líneas de 64 bytes (localidad espacial) y las retiene (localidad temporal). La TLB explota la localidad espacial: si accedes a una página, es probable que accedas a las páginas cercanas.

3. Recorrer una matriz: por filas vs por columnas

En C, las matrices se almacenan en row-major (por filas): los elementos de una fila están contiguos en memoria. Recorrer por filas es secuencial (buena localidad espacial); recorrer por columnas salta por memoria (mala localidad).

Recorrido por filas (secuencial) vs por columnas (saltos) Por filas (row-major) 0,0 0,1 0,2 0,3 1,0 1,1 1,2 1,3 secuencial → buena localidad Por columnas 0,0 0,1 0,2 0,3 1,0 1,1 1,2 1,3 saltos → mala localidad
Por filas, los accesos son contiguos en memoria. Por columnas, cada acceso salta N elementos (una fila completa).

4. El efecto de la caché y la TLB

El recorrido por filas aprovecha la localidad espacial: cada línea de caché de 64 bytes contiene 16 enteros, y se traen de una vez. El recorrido por columnas salta N enteros cada vez, cayendo en líneas distintas, provocando fallos de caché y de TLB.

Además, la TLB sufre: el recorrido por columnas accede a una página distinta en cada paso (si N × 4 bytes ≥ 4096), causando fallos de TLB.

El efecto se amplifica con el tamaño de la matriz: para matrices que no caben en la caché, la diferencia es enorme.

5. Ejemplo numérico

Matriz de 4096×4096 enteros (4 bytes cada uno) = 64 MiB. Caché L1 de 32 KiB (8192 enteros, 512 líneas de 64 B). Recorrido por filas:

Recorrido por columnas:

6. Experimento en C

El programa siguiente mide el tiempo de recorrer una matriz por filas y por columnas:

matriz.c
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

static double ahora(void) { struct timespec t; clock_gettime(CLOCK_MONOTONIC, &t); return t.tv_sec + t.tv_nsec/1e9; }

int main(void)
{
    const int N = 4096;
    int *m = calloc((size_t)N * N, sizeof *m);
    volatile long s = 0;
    double t;

    t = ahora();
    for (int i = 0; i < N; i++)
        for (int j = 0; j < N; j++) s += m[i*N + j];
    printf("por filas:    %.3f s\\n", ahora() - t);

    t = ahora();
    for (int j = 0; j < N; j++)
        for (int i = 0; i < N; i++) s += m[i*N + j];
    printf("por columnas: %.3f s\\n", ahora() - t);

    free(m);
    return 0;
}
terminal · el mismo trabajo, distinto orden
$ gcc -std=c17 -O2 -Wall -Wextra -o matriz matriz.c
$ ./matriz
por filas:    0,041 s
por columnas: 2,980 spor columnas: 2,980 s

El mismo número de sumas (16 millones) tarda 70 veces más por columnas, porque cada acceso salta N enteros (16 KiB) y casi nunca acierta en la caché. Recorrer por filas aprovecha la localidad espacial: cada línea de 64 bytes contiene 16 enteros consecutivos y se traen de una vez.

7. Optimizaciones: blocking y transposición

Cuando no se puede cambiar el orden de acceso (ej. multiplicación de matrices), se usan técnicas para mejorar la localidad:

Ejemplo de blocking para multiplicación de matrices:

blocking.c
for (int ii = 0; ii < N; ii += B)
  for (int jj = 0; jj < N; jj += B)
    for (int kk = 0; kk < N; kk += B)
      for (int i = ii; i < ii+B; i++)
        for (int j = jj; j < jj+B; j++)
          for (int k = kk; k < kk+B; k++)
            C[i*N+j] += A[i*N+k] * B[k*N+j];

El tamaño del bloque B se elige para que quepa en la caché L1 o L2. Esto puede acelerar la multiplicación en un factor de 5-10.

8. Probar con un simulador

Este simulador estima el tiempo de recorrer una matriz según el tamaño, el orden (filas/columnas) y el tamaño de la caché.

Simulador · localidad en matrices

MedidaValor
Elementos totales
Bytes totales
Líneas de caché por fila
¿Cabe en la caché?
Tiempo estimado
Factor vs óptimo

9. En Linux: perf

terminal · medir fallos de caché
$ perf stat -e cache-references,cache-misses,L1-dcache-load-misses ./matriz
$ perf stat -e dTLB-load-misses,dTLB-loads ./matriz

10. Resumen

11. Preguntas de repaso

Intenta responder cada una antes de abrir la respuesta.

1. ¿Qué es la localidad espacial?

La tendencia a acceder pronto a los datos que están cerca en memoria del dato actual.

2. ¿Qué es la localidad temporal?

La tendencia a reusar pronto un dato ya accedido.

3. ¿Por qué recorrer una matriz por filas en C es más rápido que por columnas?

Porque C almacena las matrices por filas (row-major), así que el recorrido por filas es secuencial y aprovecha la localidad espacial.

4. ¿Qué es el blocking (tiling)?

Dividir la matriz en bloques que caben en la caché y procesarlos bloque por bloque.

5. ¿Qué es la transposición?

Reordenar los datos para que el acceso sea secuencial.

6. ¿Qué herramienta mide los fallos de caché en Linux?

perf stat con eventos como cache-misses y L1-dcache-load-misses.

7. ¿Por qué el recorrido por columnas causa fallos de TLB?

Porque cada acceso cae en una página distinta, provocando fallos de TLB.

8. ¿Qué es el prefetching?

Traer datos por adelantado (por hardware o manualmente) para mejorar la localidad.

Para practicar

  1. Ejecuta matriz.c con N = 1024, 2048 y 4096. ¿Se mantiene la diferencia?
  2. Usa perf stat sobre ./matriz para contar los fallos de caché de cada recorrido.
  3. Implementa el blocking para la multiplicación de matrices y mide la mejora.
  4. Transpone la matriz antes de recorrer por columnas y mide la diferencia.
  5. Explica por qué el prefetching por hardware ayuda al recorrido por filas.
  6. Compara el rendimiento con -O0 y -O2. ¿Cambia la diferencia?
  7. Dibuja el patrón de acceso a memoria para una matriz 4×4 por filas y por columnas.