← Gestión de Memoria Bloque 9 · Intercambio y mapeo de memoria · Tema 44 de 52

Tema 44

Páginas grandes y NUMA

Un programa puede tener sus datos en RAM y aun así perder rendimiento por traducir demasiadas páginas o acceder a memoria conectada a otra CPU. Las páginas grandes aumentan la cantidad de memoria cubierta por una traducción; NUMA obliga a considerar dónde se encuentran las páginas y quién las utiliza. En este tema calcularás la cobertura de la TLB, distinguirás HugeTLB de Transparent Huge Pages y experimentarás con el primer toque, la afinidad y las políticas de ubicación mediante ejemplos en C y un simulador.

1. Dos costos que permanecen con los datos en RAM

En los temas de memoria virtual estudiamos el costo de traer páginas que no están presentes. Ahora supondremos que el conjunto de trabajo ya está en RAM. Todavía quedan, entre otros, dos problemas:

Una página grande puede ayudar al primer problema sin resolver el segundo. Fijar un hilo a una CPU puede estabilizar su ejecución sin acercar las páginas que otro hilo ya había inicializado. Son decisiones relacionadas, pero distintas.

Usaremos KiB, MiB y GiB con base 1024: 4 KiB son 4096 bytes y 2 MiB son 2 097 152 bytes. Los tamaños que aparecen en los ejemplos son habituales en x86-64; la arquitectura y la configuración del núcleo determinan cuáles están disponibles.

2. Páginas grandes y alcance de la TLB

Página grande es una página de mayor tamaño que la página base del sistema. Cuando el hardware permite traducirla con una sola entrada, esa traducción cubre un intervalo virtual mucho mayor.

Con páginas base de 4 KiB, una región de 2 MiB necesita 512 páginas. Una traducción de 2 MiB puede sustituir esas 512 traducciones base para ese intervalo. En x86-64 pueden existir páginas de 4 KiB, 2 MiB y, si el hardware lo admite, 1 GiB. Una entrada de un nivel superior de la tabla puede actuar como entrada final para una página grande.

alcance teórico de una TLB de E entradas para páginas de P bytes
Cobertura = E × P
Páginas de una región = techo(tamaño / P)

64 entradas × 4 KiB = 256 KiB
64 entradas × 2 MiB = 128 MiB
64 entradas × 1 GiB = 64 GiB
Región de 1 GiB, alineadaPáginas necesariasTraducciones si cada página usa una entrada
Páginas de 4 KiB262 144262 144
Páginas de 2 MiB512512
Página de 1 GiB11

Estas cuentas muestran el potencial, no describen toda la TLB real. Los procesadores pueden tener varios niveles, conjuntos, asociatividad y capacidades distintas para cada tamaño. No se debe asumir que una máquina tiene exactamente 64 entradas útiles para cualquier página.

También conviene distinguir fallo de TLB de fallo de página. El primero puede resolverse recorriendo tablas sin entrar al núcleo. El segundo requiere que el sistema atienda una condición de la traducción o protección. Reducir fallos de TLB no implica reducir en la misma proporción todas las faltas de caché ni todos los accesos a RAM.

3. Beneficios y costos de aumentar la página

Una carga que recorre grandes arreglos puede beneficiarse de menos traducciones y de tablas de páginas más pequeñas cuando las páginas se mapean desde niveles superiores. En cambio, un programa que utiliza solo unos pocos bytes de cada región grande puede desperdiciar memoria.

AspectoPosible beneficioCosto o condición
TLBMás bytes cubiertos por traducciónCapacidad y comportamiento específicos del hardware
TablasMenos entradas finales con mapeos grandesNo toda asignación grande usa una única entrada grande
Primer accesoMenos eventos de asignación en ciertos casosPoner a cero o compactar bloques grandes puede aumentar la latencia
FragmentaciónMenos objetos de administración por región densaMayor desperdicio con uso escaso o tamaños pequeños
Ubicación NUMAMenos unidades que administrarUna unidad grande puede mezclar datos de consumidores distintos

Con una asignación que deba consumir páginas completas de 2 MiB, 5 MiB útiles ocupan 3 páginas, es decir, 6 MiB: queda 1 MiB sin aprovechar. Un mapeo THP puede combinar páginas base y grandes o dividirse, por lo que no se debe aplicar mecánicamente esa cuenta a cualquier región de Linux.

Una página grande requiere un bloque físico contiguo del tamaño correspondiente en los mecanismos que usamos aquí. Que haya suficiente RAM libre en total no garantiza que exista un bloque adecuado. La compactación intenta reunir memoria libre, pero tiene costo y puede no lograrlo.

4. HugeTLB y Transparent Huge Pages

Linux ofrece mecanismos diferentes bajo el nombre informal de «huge pages». Conviene identificarlos antes de interpretar una medición:

PropiedadHugeTLBTransparent Huge Pages, THP
SolicitudExplícita: MAP_HUGETLB, hugetlbfs y otras interfacesEl núcleo decide sobre regiones elegibles; madvise permite expresar preferencia
RecursosPool de páginas grandes administrado por el sistemaAsignación dentro de la gestión ordinaria de memoria
Si no hay página grande adecuadaLa solicitud puede fallarNormalmente puede continuar con páginas base
IntercambioLas páginas HugeTLB no se envían a swapLa memoria respaldada mediante THP puede participar en la gestión de swap
Control y observaciónPool por tamaño y contadores HugePagesPolíticas THP y estadísticas del mapeo

Las THP tradicionales de tamaño PMD suelen ser de 2 MiB cuando la página base es de 4 KiB en x86-64. Los núcleos modernos también pueden ofrecer mTHP, tamaños intermedios como 16 o 64 KiB según la plataforma. Una asignación física de varias páginas no equivale necesariamente a una única entrada PMD: parte de estas variantes se mapea con entradas PTE.

Por eso «THP está activado» no demuestra que una región concreta use exclusivamente páginas de 2 MiB. Intervienen las políticas por tamaño, la elegibilidad de la región, la disponibilidad de memoria y el momento en que se consulta.

5. Solicitar THP desde C

madvise con MADV_HUGEPAGE marca una región como candidata. Su éxito no garantiza que se hayan asignado páginas grandes. Para comparar usaremos una ejecución nueva con MADV_NOHUGEPAGE, que evita THP en esa región.

El programa crea 64 MiB, aplica la preferencia antes de escribir y toca cada página base. Luego espera Enter para que puedas examinar el proceso desde otra terminal. Cada modo debe ejecutarse por separado.

observar_thp.c · base o preferencia por THP
#define _DEFAULT_SOURCE
#include <stdio.h>
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
    if (argc != 2 || (strcmp(argv[1], "base") != 0 &&
                      strcmp(argv[1], "thp") != 0)) {
        fprintf(stderr, "Uso: %s base|thp\n", argv[0]);
        return 1;
    }
    long n = sysconf(_SC_PAGESIZE);
    if (n <= 0) {
        fprintf(stderr, "No se pudo consultar la página base\n");
        return 1;
    }
    size_t tam = 64UL * 1024 * 1024;
    unsigned char *p = mmap(NULL, tam, PROT_READ | PROT_WRITE,
        MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    if (p == MAP_FAILED) { perror("mmap"); return 1; }
    int consejo = strcmp(argv[1], "thp") == 0 ? MADV_HUGEPAGE : MADV_NOHUGEPAGE;
    if (madvise(p, tam, consejo) == -1) {
        perror("madvise"); munmap(p, tam); return 1;
    }
    for (size_t i = 0; i < tam; i += (size_t) n)
        p[i] = 1;
    printf("PID: %ld; base: %p; tamaño: %zu; modo: %s\n",
           (long) getpid(), (void *) p, tam, argv[1]);
    puts("Consulta smaps y numa_maps. Pulsa Enter para terminar.");
    getchar();
    if (munmap(p, tam) == -1) { perror("munmap"); return 1; }
    return 0;
}
terminal · ejecutar una variante cada vez
$ gcc -std=c17 -Wall -Wextra -O2 observar_thp.c -o observar_thp
$ ./observar_thp base
PID: 4217; base: 0x7f...; tamaño: 67108864; modo: base
Consulta smaps y numa_maps. Pulsa Enter para terminar.
$ ./observar_thp thp
PID: 4220; base: 0x7f...; tamaño: 67108864; modo: thp
Consulta smaps y numa_maps. Pulsa Enter para terminar.

La base devuelta por mmap está alineada a página base, pero no se debe asumir que cualquier dirección y longitud cubren solo intervalos completos de página grande. Puede haber extremos con páginas base. El núcleo también puede promover regiones elegibles después de su creación.

Consulta la región por su dirección, no solo el total del proceso. El campo AnonHugePages informa sobre respaldo anónimo grande; THPeligible, cuando aparece, informa elegibilidad, y VmFlags puede reflejar las preferencias. No concluyas que una región carece de THP únicamente porque KernelPageSize o MMUPageSize muestre 4 kB: los campos y la representación de regiones mixtas necesitan interpretarse juntos.

6. Una reserva explícita con MAP_HUGETLB

Para una página HugeTLB de 2 MiB en Linux podemos combinar MAP_HUGETLB con MAP_HUGE_2MB. Esta variante necesita soporte, recursos y autorización adecuados. Si no están disponibles, el programa informa el error y termina; no configura un pool ni modifica la máquina.

hugetlb_2m.c · petición explícita para Linux
#define _GNU_SOURCE
#include <stdio.h>
#include <sys/mman.h>

int main(void)
{
#if defined(MAP_HUGETLB) && defined(MAP_HUGE_2MB)
    size_t tam = 2UL * 1024 * 1024;
    unsigned char *p = mmap(NULL, tam, PROT_READ | PROT_WRITE,
        MAP_PRIVATE | MAP_ANONYMOUS | MAP_HUGETLB | MAP_HUGE_2MB, -1, 0);
    if (p == MAP_FAILED) {
        perror("mmap HugeTLB de 2 MiB");
        fprintf(stderr, "Comprueba soporte, pool disponible y permisos.\n");
        return 1;
    }
    p[0] = 7;
    p[tam - 1] = 9;
    printf("HugeTLB: %u y %u\n", (unsigned) p[0], (unsigned) p[tam - 1]);
    if (munmap(p, tam) == -1) { perror("munmap"); return 1; }
    return 0;
#else
    fprintf(stderr, "Las cabeceras no exponen esta variante HugeTLB.\n");
    return 1;
#endif
}
terminal · un error por falta de pool es un resultado posible
$ gcc -std=c17 -Wall -Wextra -O2 hugetlb_2m.c -o hugetlb_2m
$ ./hugetlb_2m
mmap HugeTLB de 2 MiB: Cannot allocate memory
Comprueba soporte, pool disponible y permisos.

Si tiene éxito, la salida será HugeTLB: 7 y 9. No se debe sustituir un error por una reserva base silenciosa y luego afirmar que se midieron páginas HugeTLB. La longitud y los desplazamientos deben respetar las exigencias del tamaño elegido; al desmapear HugeTLB también hay requisitos de alineación a ese tamaño.

El pool reduce la memoria disponible para otros usos. Reservarlo es una decisión de administración y capacidad, no un paso obligatorio para estudiar THP. En esta práctica basta con observar su estado y comprobar el manejo de errores.

7. Qué es una máquina NUMA

NUMA, Non-Uniform Memory Access, es una organización en la que el costo de acceder a memoria depende de la relación entre el procesador que ejecuta el acceso y el nodo donde reside esa memoria.

Un nodo puede agrupar CPU y memoria conectadas con mayor proximidad. Un acceso local se atiende desde la memoria de ese nodo; uno remoto atraviesa una interconexión. Los nodos no equivalen necesariamente uno a uno a sockets: un socket puede exponer varios nodos y también puede haber nodos sin CPU.

CPU que ejecutaPágina en nodo 0Página en nodo 1
CPU del nodo 0Acceso localAcceso remoto
CPU del nodo 1Acceso remotoAcceso local

No existe una penalización universal de «el doble». Influyen latencia, ancho de banda, interconexión, carga y cachés. Si el dato ya está en una caché cercana, no se realiza necesariamente una lectura de DRAM. Las distancias que muestra el sistema son indicadores relativos de topología, no una tabla de nanosegundos por acceso.

En una máquina de un solo nodo, una máquina virtual o un contenedor con visibilidad restringida, tal vez no haya dos nodos disponibles para el experimento. Eso no invalida las ideas, pero impide medir allí una diferencia local/remota real.

8. Primer toque y afinidad

En el caso común de memoria anónima con política local predeterminada, el nodo de asignación se decide al materializar la página, no al reservar el intervalo virtual. De ahí la regla práctica del primer toque: el hilo que provoca la asignación inicial influye en dónde se ubica la página.

Para experimentar, conviene usar una primera escritura. Una lectura inicial puede resolverse con una página de ceros compartida sin asignar todavía la página privada que después usará el programa. También hay excepciones a la asignación local por políticas explícitas, presión de memoria y restricciones de nodos.

  1. Un hilo del nodo 0 reserva un arreglo grande con mmap, sin tocarlo.
  2. Si ese hilo inicializa todo el arreglo, las páginas privadas tienden a asignarse cerca de ese hilo bajo la política local.
  3. Después, trabajadores del nodo 1 pueden realizar accesos remotos a esas páginas.
  4. Si cada trabajador inicializa la parte que luego utilizará, puede mejorar la localidad, siempre que las páginas y las particiones permitan esa distribución.

La afinidad de CPU restringe dónde se ejecuta un hilo. La política de memoria guía dónde se asignan sus páginas. Cambiar afinidad después de inicializar no migra automáticamente todos los datos al nuevo nodo.

Linux puede realizar balanceo NUMA automático y migración de páginas en determinados entornos. Tampoco son instantáneos ni gratuitos. Si se modifica una política, las páginas existentes no cambian necesariamente de ubicación: algunas interfaces permiten pedir migración explícita, con condiciones y límites propios.

9. Políticas de memoria con numactl

numactl permite inspeccionar nodos y ejecutar un programa con restricciones de CPU y políticas de memoria. Empieza por la topología y los recursos permitidos al proceso:

terminal · inventario, sin modificar políticas globales
$ numactl --hardware
$ numactl --show
$ lscpu -e=CPU,NODE,SOCKET,CORE
$ grep -E 'Cpus_allowed_list|Mems_allowed_list' /proc/self/status
OpciónSignificadoPrecaución al interpretar
--cpunodebind=0Ejecutar en CPU del nodo 0No coloca por sí misma toda página existente en ese nodo
--membind=0Restringir asignaciones a los nodos indicadosPuede fallar o agotar recursos sin recurrir a otros nodos
--preferred=0Preferir un nodo, permitiendo alternativasNo garantiza que todas las páginas terminen allí
--interleave=0,1Distribuir asignaciones entre nodosPuede repartir ancho de banda a costa de accesos remotos
terminal · solo si ambos nodos existen y están permitidos
$ numactl --cpunodebind=0 --membind=0 ./observar_thp base
$ numactl --cpunodebind=1 --membind=0 ./observar_thp base
$ numactl --cpunodebind=0 --interleave=0,1 ./observar_thp base

Las restricciones de cpusets y contenedores pueden reducir los nodos y CPU accesibles o impedir llamadas de política. Un error no demuestra que la máquina carezca de NUMA. Verifica las condiciones del entorno antes de sacar conclusiones.

Para control desde C existen set_mempolicy, mbind, interfaces de migración y la biblioteca libnuma. Aquí usaremos afinidad de CPU para mostrar el primer toque sin añadir una dependencia de biblioteca. Si impones una política externa con numactl, esa política también influirá en el experimento.

10. Experimento de inicialización y recorrido

El programa recibe una CPU para inicializar y otra para leer. Desactiva THP en la región para reducir una variable de la comparación, fija el hilo antes de la primera escritura y después cambia su afinidad para el recorrido. Las dos CPU deben pertenecer al conjunto permitido al iniciar el programa.

Se escribe un byte cada 64 bytes y se realizan 16 recorridos sobre 64 MiB. La separación de 64 bytes es una elección del experimento, no una consulta del tamaño real de línea de caché. volatile obliga a realizar las lecturas del recorrido; no interviene como mecanismo de sincronización.

recorrido_numa.c · inicializar y consumir en CPU elegidas
#define _GNU_SOURCE
#include <errno.h>
#include <sched.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <time.h>
#include <unistd.h>

static int leer_cpu(const char *texto, const cpu_set_t *permitidas)
{
    char *fin;
    errno = 0;
    long n = strtol(texto, &fin, 10);
    if (errno || fin == texto || *fin != '\0' || n < 0 ||
        n >= CPU_SETSIZE || !CPU_ISSET((int) n, permitidas))
        return -1;
    return (int) n;
}

static int fijar_cpu(int cpu)
{
    cpu_set_t conjunto;
    CPU_ZERO(&conjunto);
    CPU_SET(cpu, &conjunto);
    return sched_setaffinity(0, sizeof conjunto, &conjunto);
}

int main(int argc, char *argv[])
{
    if ((argc != 3 && argc != 4) ||
        (argc == 4 && strcmp(argv[3], "pausa") != 0)) {
        fprintf(stderr, "Uso: %s cpu_inicializa cpu_lee [pausa]\n", argv[0]);
        return 1;
    }
    cpu_set_t permitidas;
    if (sched_getaffinity(0, sizeof permitidas, &permitidas) == -1) {
        perror("sched_getaffinity"); return 1;
    }
    int inicializa = leer_cpu(argv[1], &permitidas);
    int lee = leer_cpu(argv[2], &permitidas);
    if (inicializa < 0 || lee < 0) {
        fprintf(stderr, "CPU inválida o fuera del conjunto permitido\n");
        return 1;
    }
    size_t tam = 64UL * 1024 * 1024;
    unsigned char *p = mmap(NULL, tam, PROT_READ | PROT_WRITE,
        MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    if (p == MAP_FAILED) { perror("mmap"); return 1; }
    int codigo = 1;
    if (madvise(p, tam, MADV_NOHUGEPAGE) == -1) {
        perror("madvise"); goto liberar;
    }
    if (fijar_cpu(inicializa) == -1) {
        perror("afinidad de inicialización"); goto liberar;
    }
    for (size_t i = 0; i < tam; i += 64)
        p[i] = 1;
    if (fijar_cpu(lee) == -1) {
        perror("afinidad de lectura"); goto liberar;
    }
    struct timespec antes, despues;
    if (clock_gettime(CLOCK_MONOTONIC, &antes) == -1) {
        perror("clock_gettime"); goto liberar;
    }
    volatile const unsigned char *lectura = p;
    uint64_t suma = 0;
    for (int vuelta = 0; vuelta < 16; vuelta++)
        for (size_t i = 0; i < tam; i += 64)
            suma += lectura[i];
    if (clock_gettime(CLOCK_MONOTONIC, &despues) == -1) {
        perror("clock_gettime"); goto liberar;
    }
    double segundos = (despues.tv_sec - antes.tv_sec) +
        (despues.tv_nsec - antes.tv_nsec) / 1e9;
    printf("Inicializa CPU %d; lee CPU %d; tiempo %.6f s; suma %llu\n",
           inicializa, lee, segundos, (unsigned long long) suma);
    if (argc == 4) {
        printf("PID: %ld; base: %p. Pulsa Enter para terminar.\n",
               (long) getpid(), (void *) p);
        getchar();
    }
    codigo = suma == (uint64_t) (tam / 64) * 16 ? 0 : 1;
liberar:
    if (munmap(p, tam) == -1) { perror("munmap"); codigo = 1; }
    return codigo;
}
terminal · números de CPU ilustrativos, deben verificarse con lscpu
$ gcc -std=c17 -Wall -Wextra -O2 recorrido_numa.c -o recorrido_numa
$ ./recorrido_numa 0 0
$ ./recorrido_numa 0 8
$ ./recorrido_numa 8 8 pausa
Cada ejecución muestra sus CPU, tiempo medido y suma 16777216.

Para comparar nodos, selecciona CPU que la topología ubique en nodos distintos. Dos CPU diferentes del mismo nodo no constituyen una comparación local/remota. El programa usa un cpu_set_t de tamaño fijo; en sistemas que excedan su capacidad debe adaptarse a conjuntos de CPU dinámicos.

La medida es el tiempo de un recorrido con lecturas independientes, no la latencia pura de DRAM. Puede solapar accesos, aprovechar precarga y leer desde cachés; 64 MiB incluso pueden caber en la caché de último nivel de algunas máquinas. Tampoco controla todas las decisiones de migración. Para estudiar latencia de memoria se necesitan otros patrones, como cadenas de accesos dependientes, y un diseño de medición específico.

11. Simulador de traducción y ubicación

El modelo divide una región en dos mitades: un trabajador del nodo 0 utiliza la primera y otro del nodo 1 utiliza la segunda. Ambos realizan la misma cantidad de accesos por byte útil. Puedes cambiar la página, la cantidad de entradas de TLB y quién asigna inicialmente las páginas.

Simulador · tamaño de página y localidad NUMA

PáginasRespaldo redondeadoBytes sin uso al finalCobertura teórica TLB
TrabajadorParte útilAccesos localesAccesos remotos

Asignación de páginas, primeras 16 como máximo:

PáginaIntervalo útil, MiBNodo asignado

Con los valores iniciales hay 16 384 páginas de 4 KiB y una cobertura teórica de 256 KiB. Al pasar a 2 MiB quedan 32 páginas y la cobertura sube a 128 MiB. Con inicialización por mitades, ambas particiones conservan localidad porque el límite coincide con páginas completas.

Con una página de 1 GiB, las dos mitades de una región útil de 64 MiB caen en la misma página. En este modelo el trabajador 0 la toca primero, por lo que se coloca en nodo 0 y el trabajador 1 accede remotamente. Además, el respaldo redondeado a páginas completas es de 1 GiB, aunque solo se usen 64 MiB.

El simulador supone páginas uniformes, asignación sin fallos y ausencia de migración. Para una página que cruza el límite entre mitades, el trabajador 0 la materializa primero. No reproduce THP mixtas ni mTHP, y los porcentajes son accesos al respaldo según el patrón indicado: no predicen transacciones reales de DRAM, tiempos ni una tasa exacta de fallos de TLB.

12. Observar páginas y nodos en Linux

Primero consulta la configuración sin cambiarla. La opción entre corchetes en una línea de política indica la seleccionada en esa interfaz:

terminal · estado de THP y HugeTLB
$ cat /sys/kernel/mm/transparent_hugepage/enabled
always [madvise] never
$ cat /sys/kernel/mm/transparent_hugepage/defrag
$ ls /sys/kernel/mm/transparent_hugepage/hugepages-*/enabled
$ grep -E 'HugePages_|Hugepagesize|Hugetlb|AnonHugePages' /proc/meminfo
$ ls /sys/kernel/mm/hugepages

Los directorios por tamaño de THP dependen de la versión y configuración; su ausencia en un sistema no es un error del programa. HugePages_Total, HugePages_Free y HugePages_Rsvd describen el pool HugeTLB del tamaño predeterminado. AnonHugePages corresponde a otra categoría: no son nombres intercambiables.

Mientras observar_thp o recorrido_numa está en pausa, reemplaza el PID por el que imprime y localiza la región por su dirección inicial:

terminal · estadísticas por proceso y región
$ less /proc/4217/smaps
$ cat /proc/4217/numa_maps
... ejemplo de una región anónima, extracto ilustrativo ...
7f5000000000 default anon=16384 dirty=16384 N0=16384 kernelpagesize_kB=4
$ numastat -p 4217

numa_maps permite relacionar una región con su política efectiva y con las páginas presentes en cada nodo. En el extracto, N0=16384 indica páginas contabilizadas en nodo 0; interpreta sus unidades con el tamaño y el tipo de respaldo que informa el sistema. No deduzcas una tasa de accesos remotos solo a partir de la cantidad de páginas: también necesitas saber qué CPU y qué patrón las utilizan.

numastat -p resume la distribución de memoria del proceso. Para estudiar traducciones puede servir perf stat, si los permisos y el hardware exponen contadores apropiados:

terminal · nombres de eventos sujetos a disponibilidad
$ perf list
$ perf stat -e dTLB-loads,dTLB-load-misses,page-faults ./recorrido_numa 0 0

Los eventos genéricos no tienen idéntica implementación en todos los procesadores. El resultado puede incluir el arranque y la inicialización, mientras que el temporizador del programa cubre solo los recorridos. En una máquina virtual tal vez no haya contadores disponibles. Una reducción de un contador aislado no demuestra por sí sola una mejora del tiempo total.

13. Medir y decidir

Empieza por un caso correcto y repetible. Mantén el mismo trabajo útil y comprueba el resultado; luego cambia una variable por vez. Si comparas páginas base con THP, verifica primero qué respaldo recibió cada ejecución. Si comparas NUMA local con remoto, confirma la topología y la ubicación de las páginas.

  1. Separar fases. Mide reserva, inicialización y procesamiento de forma que sepas qué costo estás observando.
  2. Repetir y alternar. Ejecuta varias veces ambas variantes y compara su variación, no solo el mejor tiempo.
  3. Controlar el entorno. Considera afinidad, políticas, carga de otros procesos, cachés, frecuencia y migraciones.
  4. Observar varias métricas. Tiempo, uso de memoria, fallos, distribución por nodos y latencias de las operaciones relevantes.
  5. Evaluar el costo completo. Una mejora media puede convivir con pausas por compactación o con mayor consumo de memoria.

Para datos densos y muy grandes, las páginas grandes pueden resultar útiles. Para estructuras dispersas o aplicaciones sensibles a pausas, sus costos merecen especial atención. Si el problema principal es una inicialización centralizada que coloca todo en un nodo, ampliar páginas no sustituye repartir correctamente el trabajo.

En memoria compartida entre procesos, cada página también tiene una ubicación física en un momento dado. Todos los consumidores no pueden considerarla local a la vez si están en nodos distintos. Las opciones incluyen particionar datos, colocar consumidores cerca, intercalar para repartir ancho de banda o replicar datos de solo lectura cuando el costo lo justifica.

Documentación para contrastar las interfaces: HugeTLB en el núcleo Linux, Transparent Huge Pages, políticas NUMA, madvise, numactl y smaps.

14. Resumen

15. Preguntas de repaso

Intenta responder antes de abrir las explicaciones.

1. ¿Cuántas páginas de 4 KiB equivalen a 2 MiB?

512. La cuenta es 2 × 1024 × 1024 / 4096. Una página de 2 MiB mapeada como tal puede cubrir ese intervalo con una traducción grande.

2. ¿Qué cobertura teórica tienen 128 entradas para páginas de 2 MiB?

256 MiB. Es una cuenta ideal: no determina por sí sola la tasa real de fallos, la asociatividad ni la cantidad de entradas de la máquina.

3. ¿Una página grande elimina los fallos de caché de datos?

No. El tamaño de página afecta la traducción y la gestión de memoria. Las cachés de datos siguen teniendo sus propias líneas, capacidades y patrones de acceso.

4. ¿MADV_HUGEPAGE con resultado cero prueba que hay THP en toda la región?

No. Informa que se aceptó la indicación. Hay que observar la región y considerar elegibilidad, tamaños, políticas y disponibilidad.

5. ¿Qué diferencia hay entre HugePages_Free y AnonHugePages?

El primero es un contador del pool HugeTLB; el segundo informa memoria anónima respaldada por páginas grandes transparentes en el ámbito del archivo consultado. No miden la misma categoría.

6. ¿Por qué la primera escritura es importante para NUMA?

Porque puede materializar la página privada. Bajo política local, la CPU que provoca esa asignación influye en el nodo elegido. La lectura de ceros puede no crear todavía esa página privada.

7. ¿Cambiar la afinidad de un hilo migra su arreglo?

No por sí solo. Cambia dónde se ejecuta el hilo. La migración de páginas depende de otros mecanismos, políticas y condiciones.

8. ¿Intercalar páginas garantiza accesos locales?

No. Distribuye asignaciones entre nodos. Puede repartir capacidad o ancho de banda, pero un trabajador de un solo nodo tendrá parte de sus accesos al respaldo en otros nodos.

9. ¿Dos CPU distintas bastan para probar acceso remoto?

No. Pueden pertenecer al mismo nodo. Se debe consultar la topología y comprobar dónde residen las páginas que utilizan.

10. ¿Por qué una página muy grande puede empeorar el reparto por trabajadores?

Porque los datos de varios consumidores pueden caer en una misma unidad de ubicación. Si se asigna a un nodo, otros consumidores pueden necesitar acceder remotamente a su parte.

11. ¿El recorrido en C mide latencia pura de DRAM?

No. Incluye un patrón con accesos que pueden solaparse y aprovechar cachés y precarga. Es una medida del recorrido bajo esas condiciones.

Para practicar

  1. Calcula las páginas necesarias para 3 GiB con páginas de 4 KiB y de 2 MiB. Comprueba que obtienes 786 432 y 1536.
  2. Calcula la cobertura de 96 entradas con páginas de 4 KiB y de 2 MiB. Explica por qué no puedes convertir directamente esa cuenta en una tasa de aciertos.
  3. Ejecuta observar_thp en ambos modos y localiza la región en smaps. Registra los campos relevantes aunque el modo thp no produzca páginas grandes.
  4. Ejecuta hugetlb_2m y clasifica su resultado. Si falla, consulta el estado del pool sin modificarlo y distingue falta de recursos de falta de soporte o permisos.
  5. Consulta la topología y el conjunto de CPU permitido. Ejecuta recorrido_numa usando dos CPU permitidas y anota si pertenecen al mismo nodo.
  6. Si hay dos nodos accesibles, compara varias ejecuciones con inicialización y lectura en el mismo nodo frente a lectura desde otro. Registra también la distribución de páginas.
  7. En el simulador, compara 64 MiB con páginas de 4 KiB, 2 MiB y 1 GiB. Explica cobertura, desperdicio y localidad en cada caso.
  8. Selecciona inicialización completa en nodo 0 y después intercalado. Explica por qué repartir páginas no garantiza que todos los accesos sean locales.
  9. Diseña la inicialización de un arreglo dividido entre cuatro trabajadores en dos nodos. Considera los límites de página y qué sucede si una página grande cruza una partición.