Ventajas y desventajas de los hugepages

En una entrada anterior, he escrito sobre cómo comprobar y habilitar los hugepages transparentes en Linux de forma global.

Aunque esta entrada es importante si realmente tienes un caso de uso para los hugepages, he visto a varias personas engañadas por la perspectiva de que los hugepages aumentarán mágicamente el rendimiento. Sin embargo, los hugepages son un tema complejo y, si se utilizan de forma incorrecta, pueden reducir fácilmente el rendimiento general.

Esta entrada intenta explicar las ventajas, desventajas y advertencias del uso de hugepages, ya sea no usándolos en absoluto o de forma selectiva. Como una entrada con mucho contenido técnico o pedantemente exacta probablemente sea inaccesible para los usuarios que a menudo se dejan engañar por los hugepages, sacrificaré la precisión en favor de la simplicidad. Ten en cuenta que la mayoría de los temas son realmente complejos y, por lo tanto, están enormemente simplificados.

Ten en cuenta que estamos hablando de sistemas x86 de 64 bits ejecutando Linux aquí y que simplemente asumo que el sistema implementa hugepages transparentes (es decir, no es una desventaja que los hugepages no sean intercambiables) ya que este es el caso en casi cualquier entorno de Linux reciente.

Se proporcionarán descripciones más técnicas en los enlaces a continuación.

Memoria virtual

Si eres un programador de C++, sabes que los objetos en memoria tienen ciertas direcciones (es decir, el valor de un puntero).

Sin embargo, estas direcciones no necesariamente representan direcciones físicas (es decir, una dirección en RAM). Representan direcciones en memoria virtual. Tu CPU tiene un hardware MMU (unidad de gestión de memoria) que asiste al kernel en el mapeo de la memoria virtual a una ubicación física.

Este enfoque tiene numerosas ventajas, pero principalmente es útil para

  • Rendimiento (por diversas razones diferentes)
  • Aislar programas, es decir, ningún programa puede simplemente leer la memoria de otro programa

¿Qué son las páginas?

El espacio de memoria virtual está dividido en páginas.

Cada página individual apunta a cierta memoria física - puede apuntar a una sección de RAM física, pero también puede apuntar a una dirección asignada a un dispositivo físico como una tarjeta gráfica.

La mayoría de las páginas con las que tratas apuntan a la RAM o están intercambiadas (swapped out), es decir, almacenadas en un HDD o un SSD.

El kernel gestiona la ubicación física de cada página. Si se accede a una página que ha sido intercambiada, el kernel detiene el hilo que intenta acceder a la memoria, lee la página desde el HDD/SSD hacia la RAM y posteriormente continúa ejecutando el hilo.

Este proceso es transparente para el hilo, es decir, no tiene que leer explícitamente desde el HDD/SSD.

Las páginas normales tienen 4096 bytes de longitud. Los hugepages tienen un tamaño de 2 Megabytes.

El Translation Lookaside Buffer (TLB)

Cuando un programa accede a cierta página de memoria, la CPU necesita saber de qué página física leer los datos (es decir, un mapeo de dirección virtual a física).

El kernel contiene una estructura de datos (la tabla de páginas) que contiene toda la información sobre todas las páginas en uso. Usando esta estructura de datos, podríamos mapear la dirección virtual a una dirección física.

Sin embargo, la tabla de páginas es bastante compleja y lenta y simplemente no podemos analizar toda la estructura de datos cada vez que algún proceso accede a la memoria.

Afortunadamente, nuestra CPU contiene hardware - el TLB - que almacena en caché el mapeo de direcciones virtuales a físicas. Esto significa que, aunque tienes que analizar la tabla de páginas la primera vez que accedes a la página, todos los accesos posteriores a la página pueden ser manejados por el TLB, ¡lo cual es realmente rápido!

Pero como está implementado en hardware (lo que lo hace rápido en primer lugar), también tiene una capacidad limitada. Así que si accedes a un mayor número de páginas, el TLB no puede almacenar el mapeo para todas ellas. Esto hará que tu programa sea mucho más lento.

Hugepages al rescate

Entonces, ¿qué podemos hacer (asumiendo que el programa todavía necesita la misma cantidad de memoria) para evitar que el TLB se llene?

Aquí es donde entran los hugepages. En lugar de que 4096 bytes “consuman” una entrada del TLB, una entrada del TLB ahora puede apuntar a unos impresionantes 2 Megabytes.

Así que si asumimos que el TLB tiene 512 entradas, sin hugepages podemos mapear

$$4096\ \text{b} \cdot 512 = 2\ \text{MB}$$

pero con hugepages podemos mapear

$$2\ \text{MB} \cdot 512 = 1\ \text{GB}$$

Así que los hugepages son geniales - pueden llevar a un rendimiento muy aumentado casi sin esfuerzo. Pero no vienen sin advertencias.

Intercambio de hugepages

Tu kernel rastrea automáticamente con qué frecuencia se usa cada página de memoria. Si hay una cantidad insuficiente de memoria física (es decir, RAM) disponible, tu kernel moverá las páginas menos importantes (es decir, menos usadas) a tu disco duro para liberar algo de RAM para páginas más importantes.

En principio, lo mismo se aplica a los hugepages. Pero el kernel solo puede intercambiar páginas completas - no bytes individuales.

Asumamos que tenemos un programa como este:

hugepage_swap_example.cpp
char* mymemory = malloc(2*1024*1024); //Asumiremos que esto es una hugepage!
// Llenar mymemory con algunos datos
// Hacer muchas otras cosas,
// causando que la página mymemory sea swapada
// ...
// Acceder solo al primer byte
putchar(mymemory[0]);

En ese caso, el kernel necesitará intercambiar hacia adentro (es decir, leer) los 2 Megabytes completos desde el HDD/SSD solo para que leas un único byte. Con páginas normales, solo se necesitan leer 4096 bytes desde el HDD/SSD.

Así que si un hugepage es intercambiado, leerlo solo es más rápido si necesitas acceder a casi todo el hugepage. Eso significa que si accedes aleatoriamente a diferentes partes de la memoria y solo lees un par de kilobytes, deberías simplemente usar las viejas páginas normales y no preocuparte por ello.

Por otro lado, si necesitas acceder a una gran porción de memoria de forma secuencial, los hugepages pueden aumentar tu rendimiento. Aun así, necesitas medir esto usando tu programa (¡no un software de benchmark abstracto!) y ver si es más rápido con o sin hugepages habilitados.

Asignación de memoria

Como programador de C, sabes que puedes solicitar cantidades de memoria arbitrariamente pequeñas (o casi arbitrariamente grandes) del heap usando malloc().

Asumamos que estás solicitando 30 bytes de memoria:

malloc_small_example.c
char* mymemory = malloc(30);

Para el programador, puede parecer que estás “solicitando” 30 bytes de memoria al sistema operativo y obtienes un puntero a cierta memoria virtual.

Pero en realidad, malloc() es solo una función de C que internamente llama a las funciones brk y sbrk para solicitar o liberar memoria del sistema operativo.

Sin embargo, es ineficiente simplemente solicitar más y más memoria al SO para cada pequeña asignación - lo más probable es que un segmento de memoria haya sido liberado con free() y podamos reutilizar esa memoria. malloc() implementa algoritmos bastante complejos para reutilizar memoria liberada con free().

Pero todo esto es transparente para ti - ¿así que por qué debería importarte? Porque también significa que llamar a free() no significa que la memoria no sea necesariamente devuelta al sistema operativo inmediatamente.

Hay un tipo de problema llamado fragmentación de memoria. En casos extremos hay segmentos del heap donde solo se usan unos pocos bytes mientras que todo lo intermedio ha sido liberado con free().

En la mayoría de los casos, el kernel simplemente intercambiará la mayor parte de la memoria no utilizada porque

Si estás usando hugepages para toda la memoria de tu programa (es decir, no selectivamente como se muestra a continuación), esto podría

Ten en cuenta que la fragmentación de memoria es un problema increíblemente complejo e incluso pequeños cambios en un programa pueden afectar significativamente la fragmentación de memoria. En la mayoría de los casos los programas no causan fragmentación de memoria significativa, pero debes tener en cuenta que, si hay problemas de fragmentación de memoria en alguna área del heap, los hugepages pueden empeorar el problema.

Hugepaging selectivo

Usando la información de este artículo, has identificado algunas partes de tu programa que podrían beneficiarse de los hugepages, pero otras partes de tu programa no se beneficiarían de los hugepages. ¿Deberías habilitar los hugepages o no?

Afortunadamente puedes usar madvise() para habilitar el hugepaging solo para áreas de memoria donde te beneficies de ello.

Primero, comprueba que los hugepages están habilitados en modo madvise (ver esta entrada de blog).

Luego, usa madvise para decirle al kernel que se usarán hugepages para la

madvise_example.cpp
#include <sys/mman.h>

// Asignar una gran cantidad de memoria para la que tienes algún uso
size_t size = 256*1024*1024;
char* mymemory = malloc(size);
// O simplemente habilitar hugepages ...
madvise(mymemory, size, MADV_HUGEPAGE);
// ... o también decir al
madvise(mymemory, size, MADV_HUGEPAGE | MADV_SEQUENTIAL)

Ten en cuenta que esto solo recomienda algunos aspectos de gestión de memoria al kernel. No significa automáticamente que el kernel realmente use hugepages para la memoria dada.

Consulta la página del manual de madvise para más detalles - hay mucho que aprender sobre gestión de memoria y madvise, y el tema tiene una curva de aprendizaje increíblemente empinada. Así que si tienes intención de seguir esta ruta, prepárate para leer, probar y medir durante unas semanas antes de esperar cualquier resultado positivo.

¿Quieres leer más?

Artículo de IBM OpenStack sobre hugepages Hugepages transparentes vs hugepages no transparentes Artículo de Wikipedia sobre el TLBDocumentación del kernel de Linux sobre hugepages transparentesStackOverflow: Los hugepages son propensos a la fragmentación de memoriaMicrosoft sobre la gestión de memoria virtual


Echa un vistazo a artículos similares por categoría: C/C++ Rendimiento