Por qué el alto uso de CPU suele indicar que su base de datos está esperando memoria
Un análisis profundo sobre cómo la latencia de memoria detiene las CPUs durante las agregaciones en bases de datos y cómo la partición radix mejora la utilización de caché para impulsar el rendimiento.
Traducido automáticamente del original en inglés.
Cuando un servidor de base de datos reporta un 100% de uso de CPU, no necesariamente está procesando cálculos a máxima velocidad. A menudo, el procesador está inactivo, esperando a que lleguen los datos desde la memoria principal. Un reciente análisis técnico del equipo Pivot revela que la latencia de memoria, y no la complejidad computacional, es el cuello de botella principal en muchas operaciones comunes de bases de datos.
Qué ocurrió
El equipo de ingeniería de Pivot investigó por qué su motor de base de datos parecía estar totalmente utilizado, pero tenía un rendimiento inferior al esperado durante consultas de agregación. Se centraron en una operación estándar GROUP BY utilizando el conjunto de datos ClickBench, que contiene 99 millones de filas. La consulta específica contaba las ocurrencias de UserIDs únicos, resultando en aproximadamente 17 millones de grupos distintos. Para aislar las características de rendimiento, ejecutaron la prueba en un servidor bare-metal con un procesador AMD EPYC 9124 Genoa, fijando el proceso a un solo núcleo para evitar interferencias de tareas paralelas.
La implementación inicial utilizó un enfoque ingenuo: una única HashTable creciente indexada por UserID. Para cada fila, el motor verificaba si la clave existía y actualizaba el conteo. Este método tardó 3,9 segundos en la primera ejecución y 3,6 segundos en ejecuciones posteriores donde los datos ya estaban almacenados en la caché de páginas del sistema operativo. Aunque estos tiempos podrían parecer aceptables, los contadores internos de rendimiento mostraban otra realidad. La CPU pasaba la mitad de su tiempo sin hacer nada más que esperar a que se completaran los accesos a memoria.
Para abordar esto, los ingenieros reemplazaron la única HashTable con un algoritmo de partición radix de dos fases. En la primera fase, hasheaban cada clave y distribuían las filas en 4.096 particiones separadas basándose en los bits superiores del hash. En la segunda fase, procesaban cada partición individualmente, creando pequeñas HashTables temporales para cada una. Este cambio redujo el tiempo de consulta a 2,7 segundos con datos en caché. Optimizaciones adicionales dentro del motor Pivot finalmente redujeron el tiempo de un solo núcleo a 1,8 segundos, demostrando que los cambios estructurales en los patrones de acceso a datos producen ganancias significativas.
Cómo funciona
Las CPUs modernas ejecutan instrucciones mucho más rápido de lo que pueden recuperar datos de la RAM. Un procesador podría completar entre 4 y 8 instrucciones por nanosegundo, mientras que un único acceso a DRAM tarda entre 50 y 100 nanosegundos. Durante ese tiempo de espera, la CPU podría haber ejecutado cientos de instrucciones. Para mitigar esto, las CPUs utilizan cachés jerárquicos (L1, L2, L3) que son mucho más rápidos pero más pequeños que la memoria principal. Si los datos necesarios para una operación no están en la caché, la CPU se detiene.
En la implementación ingenua de GROUP BY, la HashTable crece para acomodar millones de claves únicas. A medida que la tabla se expande, supera la capacidad de las cachés L1 y L2. Como consecuencia, casi todas las búsquedas resultan en un "fallo de caché" (cache miss), forzando a la CPU a obtener datos de la lenta DRAM. Las métricas de rendimiento confirmaron esto: el 50,1% de los ciclos de CPU se detenían debido a límites de memoria, y solo el 16,1% de los ciclos se usaban para trabajo real. El sistema extrajo 4,5 GB de datos de la DRAM para la consulta.
La partición radix resuelve este problema dividiendo el problema en fragmentos más pequeños que caben en la caché. Al dividir las 17 millones de claves en 4.096 particiones, la HashTable de cada partición individual permanece lo suficientemente pequeña como para permanecer en la rápida caché L1 o L2 durante el procesamiento. Aunque este enfoque requiere más instrucciones totales—hashear dos veces y gestionar múltiples vectores—reduce drásticamente el número de lentos accesos a DRAM. La ejecución optimizada mostró que las esperas de memoria caían al 23,5% y el trabajo útil aumentaba al 32,2%. El tráfico de DRAM cayó a 1,5 GB, probando que intercambiar instrucciones extra de CPU por mejor localidad de caché es una estrategia ganadora.
Detalles clave
- El acceso a memoria DRAM es 50-100 veces más lento que el acceso a la caché L1, causando importantes paradas de CPU.
- El enfoque ingenuo de HashTable resultó en que el 50,1% de los ciclos de CPU se desperdiciaran en esperas de memoria.
- La partición radix divide los datos en 4.096 fragmentos, permitiendo que los conjuntos de trabajo quepan en las cachés de la CPU.
- El enfoque particionado optimizado redujo la transferencia de datos de DRAM de 4,5 GB a 1,5 GB para la misma consulta.
- A pesar de realizar más instrucciones totales, el método amigable con la caché redujo el tiempo de consulta en más del 25%.
- El rendimiento final optimizado de un solo núcleo alcanzó 1,8 segundos, con la ejecución paralela de 16 núcleos llegando a 171 ms.
Por qué importa
Para ingenieros de software y desarrolladores de bases de datos, este análisis destaca que las métricas de utilización de CPU pueden ser engañosas. Un servidor que muestra un 100% de uso de CPU puede en realidad estar hambriento de datos en lugar de potencia de cómputo. Entender la diferencia entre cargas de trabajo limitadas por cómputo (compute-bound) y limitadas por memoria (memory-bound) es crucial para un ajuste efectivo del rendimiento. Optimizar la localidad de caché a menudo produce mejoras mayores que simplemente añadir más núcleos o actualizar las velocidades de reloj.
Esta perspectiva se aplica ampliamente a cualquier sistema que maneje grandes conjuntos de datos con búsquedas o agregaciones frecuentes. Los desarrolladores que construyen backends de alto rendimiento deben considerar el diseño de datos y los patrones de acceso desde las primeras etapas del proceso de diseño. Los algoritmos que parecen computacionalmente más pesados en papel pueden tener un mejor rendimiento en la práctica si respetan las restricciones físicas de la jerarquía de memoria. Ignorar estas realidades del hardware puede llevar a sistemas que escalan mal a pesar de contar con abundantes recursos computacionales.
Qué puede hacer
- Utilice herramientas como
perf statpara medir los ciclos de parada de memoria y las tasas de fallos de caché, no solo el tiempo de CPU. - Analice sus patrones de acceso a datos para identificar operaciones que tocan regiones de memoria grandes y aleatorias.
- Considere particionar grandes conjuntos de datos en fragmentos más pequeños que quepan dentro de los tamaños de caché L2 o L3.
- Evalúe algoritmos basándose en la localidad de caché, no solo en la complejidad temporal teórica.
- Fije procesos críticos a núcleos específicos de CPU durante las pruebas de referencia para reducir el ruido causado por el cambio de contexto.
- Monitoree el uso del ancho de banda de DRAM como un indicador clave de cuellos de botella limitados por memoria en su aplicación.



