Latencia de memoria en servidor cloud: medida, no supuesta
Respuesta corta (comprobado el 2026-11-01): Un post que llegó a la portada de Hacker News este fin de semana sostiene que una CPU al 100% a menudo solo está esperando a la RAM, y muestra un GROUP BY que se aceleró haciendo más trabajo pero tocando menos la memoria. Probamos la misma idea en el servidor cloud que escribe este blog: una lectura cuesta unos 2 ns cuando los datos caben en L1 y unos 200 ns con un conjunto de trabajo de 256 MB, unas cien veces más. Para quien alquila servidores, esa diferencia pesa más que el número de núcleos del pedido. Abajo están el script, nuestras cifras y lo que cambian al elegir entre cloud y dedicado.
Fuente. "It's the memory latency, stupid", de Maor Kern (Pivot / Epsio Labs, publicado el 2026-10-28), comentado en Hacker News con 175 puntos a 2026-11-01. Sus cifras se citan tal como las publica el post; las nuestras las medimos nosotros.
Qué midió el post de Pivot
La prueba es una consulta, GROUP BY UserID sobre la tabla "hits" de ClickBench (unos 99 millones de filas, unos 17 millones de usuarios distintos), fijada a un núcleo de un servidor bare metal AMD EPYC 9124 con 128 GB DDR5. Una tabla hash simple con 17 millones de claves no cabe en ninguna caché, así que casi cada búsqueda va a la RAM. Dividir antes las filas en 4096 particiones, cada una con una tabla hash pequeña, añade trabajo pero mantiene cada tabla en caché:
| Métrica (un núcleo) | Tabla hash simple | Particionado radix |
|---|---|---|
| Tiempo, 1.ª / 2.ª ejecución | 3.9 s / 3.6 s | 3.3 s / 2.7 s |
| Slots esperando a memoria | 50.1% | 23.5% |
| Slots con trabajo útil | 16.1% | 32.2% |
| Lecturas desde DRAM | 76.2 millones | 25.7 millones |
La versión particionada ejecuta más operaciones (19.4 mil millones frente a 11.4 mil millones) y aun así termina antes. La versión optimizada del autor tardó 1.85 s en un núcleo y 0.17 s en los 16. La lección no es de una sola base de datos: "CPU 100%" en top dice que el núcleo no está ocioso, no que esté calculando.
Mídelo tú mismo: memlat.js
La forma clásica de ver la latencia es un pointer chase: un array en el que cada posición guarda el índice de la siguiente, en orden aleatorio, separadas por una línea de caché (64 bytes). Cada lectura depende de la anterior, así que la CPU no puede precargar ni solaparlas, y el tiempo por salto es la latencia del nivel donde vive el conjunto de trabajo. En nuestra máquina del blog no hay compilador de C, así que es JavaScript para Bun: el JIT convierte el bucle en unas pocas instrucciones de máquina, lo que suma un poco a cada cifra. Compara los escalones, no los valores absolutos. Guárdalo como memlat.js:
// memlat.js - pointer-chase memory latency by working-set size (bun memlat.js)
import { readFileSync, readdirSync } from 'node:fs'
const cpu = readFileSync('/proc/cpuinfo', 'utf8').match(/model name\s*:\s*(.+)/)?.[1] ?? '?'
const base = '/sys/devices/system/cpu/cpu0/cache'
const caches = readdirSync(base).filter(d => d.startsWith('index')).map(d => {
const r = f => readFileSync(`${base}/${d}/${f}`, 'utf8').trim()
return `L${r('level')}${r('type') === 'Data' ? 'd' : r('type') === 'Instruction' ? 'i' : ''} ${r('size')}`
})
console.log(`CPU: ${cpu}\nCaches: ${caches.join(', ')}`)
// one random cycle through n slots, 64 bytes (16 x int32) apart: one cache line per hop
function ring(bytes) {
const n = Math.max(2, Math.floor(bytes / 64))
const order = new Uint32Array(n)
for (let i = 0; i < n; i++) order[i] = i
for (let i = n - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1))
const t = order[i]; order[i] = order[j]; order[j] = t
}
const a = new Int32Array(n * 16)
for (let i = 0; i < n; i++) a[order[i] * 16] = order[(i + 1) % n] * 16
return a
}
function chase(a, hops) {
let p = 0
for (let i = 0; i < hops; i++) p = a[p]
return p
}
const hops = 20_000_000
console.log('working set ns/load')
for (const kb of [16, 32, 256, 1024, 4096, 16384, 65536, 262144]) {
const a = ring(kb * 1024)
chase(a, hops / 4) // warm up: JIT and caches
const best = []
for (let r = 0; r < 3; r++) {
const t = Bun.nanoseconds()
const sink = chase(a, hops)
best.push((Bun.nanoseconds() - t) / hops)
if (sink < 0) console.log(sink)
}
const label = kb >= 1024 ? `${kb / 1024} MB` : `${kb} KB`
console.log(`${label.padStart(10)} ${Math.min(...best).toFixed(1).padStart(7)}`)
}
bun memlat.js
Salida, ejecutado en un servidor DCXV Cloud con Bun 1.4.4 el 2026-11-01:
CPU: Intel(R) Xeon(R) CPU E5-2698 v4 @ 2.20GHz
Caches: L2 4096K, L1d 32K, L3 16384K, L1i 32K
working set ns/load
16 KB 2.0
32 KB 2.2
256 KB 12.5
1 MB 24.3
4 MB 122.2
16 MB 138.4
64 MB 164.7
256 MB 205.1
Una segunda ejecución esa misma mañana se desvió como mucho 2 ns hasta 1 MB y como mucho 12 ns por encima:
| Conjunto de trabajo | ns por lectura, ejecución 1 / 2 | Dónde vive |
|---|---|---|
| 32 KB | 2.2 / 2.4 | caché de datos L1 |
| 256 KB | 12.5 / 10.5 | L2, desbordando a L3 |
| 1 MB | 24.3 / 24.3 | L3 |
| 16 MB | 138.4 / 137.2 | recorridos de tablas de páginas, L3 y DRAM |
| 256 MB | 205.1 / 193.5 | DRAM |
Lo que dicen las cifras
- El invitado no ve las cachés reales. La VM declara 4 MB de L2 y 16 MB de L3; Intel indica 50 MB de caché para el Xeon E5-2698 v4 (Intel ark, comprobado el 2026-11-01). El hipervisor presenta su propia topología, y
lscpudentro de un servidor cloud lee esa, no el silicio. Mide el hardware en lugar de fiarte de las etiquetas. - El salto llega mucho antes de llenar la caché. Entre 1 MB y 4 MB la latencia se multiplica por cinco, muy por debajo de 50 MB. Con páginas de 4 KB, un recorrido aleatorio de 4 MB toca 1.024 páginas, más de las que caben en la TLB, y en una VM un fallo de TLB recorre las tablas de páginas del invitado y del host; además, la L3 se comparte con los demás núcleos del socket. No separamos estas causas. La conclusión práctica es la misma: en una VM, planifica como si tu conjunto de trabajo rápido fuera de unos pocos megabytes.
- Más RAM no es RAM más rápida. De 1 MB a 256 MB el coste por lectura crece unas ocho veces. Más gigabytes dan capacidad, no velocidad; lo que acelera una tarea limitada por latencia es un conjunto caliente más pequeño, como muestra el particionado de Pivot.
Qué significa si alquilas servidores
- Los servidores cloud sirven para la mayoría de apps web, APIs y bases de datos pequeñas: sus datos calientes caben en caché y esperan a la red o al disco mucho antes que a la RAM. Dimensiónalos según el uso de memoria medido, como en nuestra guía de dimensionamiento de servidores cloud.
- Los servidores dedicados compensan cuando un conjunto de trabajo grande se lee al azar todo el día: analítica, hash joins grandes, cachés en memoria, un servidor PostgreSQL cuyos índices ya no caben en RAM. Tienes toda la L3 y todos los canales de memoria sin vecinos, y puedes activar huge pages, que reducen los fallos de TLB. Configura uno en la página de servidor dedicado.
- Antes de ampliar cualquiera de los dos, ejecuta la prueba en el servidor que ya tienes y comprueba si tu tarea lenta está limitada por latencia. Duplicar núcleos no ayuda a un hilo que pasa la mitad del tiempo parado. La memoria se encareció este año, ver RAM de servidor en 2026, así que la solución más barata suele ser código que la toque menos.
Prueba antes de comprar
Nuestros centros de datos Tier III en Praga y Covilha ofrecen servidores cloud y servidores dedicados. Arranca un servidor cloud, ejecuta memlat.js junto a tu propia carga y envía el resultado a sales@dcxv.com: te ayudaremos a decidir si un servidor dedicado cambiaría las cifras.
