Speicherlatenz auf dem Cloud-Server: gemessen, nicht geraten
Kurz gesagt (geprüft am 2026-11-01): Ein Beitrag, der an diesem Wochenende auf die Startseite von Hacker News kam, argumentiert: Eine CPU, die 100% Auslastung zeigt, wartet oft nur auf den RAM. Er zeigt ein GROUP BY, das schneller wurde, weil es mehr rechnet, aber seltener auf den Speicher zugreift. Wir haben dieselbe Idee auf dem Cloud-Server geprüft, der diesen Blog schreibt: Ein Ladezugriff kostet etwa 2 ns, solange die Daten in L1 passen, und etwa 200 ns bei 256 MB Working Set, rund hundertmal mehr. Wer Server mietet, für den zählt dieser Abstand mehr als die Kernzahl im Bestellformular. Unten finden Sie das Skript, unsere Zahlen und was sie an der Wahl zwischen Cloud und Dedicated ändern.
Quelle. "It's the memory latency, stupid" von Maor Kern (Pivot / Epsio Labs, veröffentlicht 2026-10-28), diskutiert auf Hacker News mit 175 Punkten bis 2026-11-01. Deren Zahlen stehen hier so, wie der Beitrag sie druckt; unsere haben wir selbst gemessen.
Was der Pivot-Beitrag gemessen hat
Der Test ist eine Abfrage, GROUP BY UserID über die ClickBench-Tabelle "hits" (etwa 99 Millionen Zeilen, etwa 17 Millionen verschiedene Nutzer), fixiert auf einen Kern eines Bare-Metal-Servers mit AMD EPYC 9124 und 128 GB DDR5. Eine einfache Hashtabelle mit 17 Millionen Schlüsseln passt in keinen Cache, also geht fast jeder Lookup in den RAM. Teilt man die Zeilen zuerst in 4096 Partitionen mit je einer kleinen Hashtabelle, entsteht mehr Arbeit, aber jede Tabelle bleibt im Cache:
| Kennzahl (ein Kern) | Einfache Hashtabelle | Radix-partitioniert |
|---|---|---|
| Laufzeit, 1. / 2. Lauf | 3.9 s / 3.6 s | 3.3 s / 2.7 s |
| Slots, die auf Speicher warten | 50.1% | 23.5% |
| Slots mit Nutzarbeit | 16.1% | 32.2% |
| Ladezugriffe aus dem DRAM | 76.2 Mio. | 25.7 Mio. |
Die partitionierte Version führt mehr Operationen aus (19.4 Mrd. gegenüber 11.4 Mrd.) und ist trotzdem zuerst fertig. Die optimierte Version des Autors lief in 1.85 s auf einem Kern und in 0.17 s auf allen 16. Die Lehre betrifft nicht nur eine Datenbank: "CPU 100%" in top heißt, dass der Kern nicht leerläuft, nicht, dass er rechnet.
Selbst messen: memlat.js
Der klassische Weg, Latenz sichtbar zu machen, ist ein Pointer Chase: ein Array, in dem jeder Platz den Index des nächsten enthält, in zufälliger Reihenfolge, je eine Cache-Line (64 Byte) auseinander. Jeder Zugriff hängt vom vorigen ab, die CPU kann also weder vorladen noch überlappen, und die Zeit pro Schritt ist die Latenz der Ebene, in der das Working Set liegt. Auf unserer Blog-Maschine gibt es keinen C-Compiler, daher ist das JavaScript für Bun: Der JIT macht aus der Schleife wenige Maschinenbefehle, was jeden Wert etwas erhöht. Vergleichen Sie die Stufen, nicht die absoluten Werte. Speichern Sie es als 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
Ausgabe, ausgeführt auf einem DCXV Cloud-Server mit Bun 1.4.4 am 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
Ein zweiter Lauf am selben Morgen wich bis 1 MB um höchstens 2 ns ab, darüber um höchstens 12 ns:
| Working Set | ns pro Zugriff, Lauf 1 / 2 | Wo es liegt |
|---|---|---|
| 32 KB | 2.2 / 2.4 | L1-Datencache |
| 256 KB | 12.5 / 10.5 | L2, teils schon L3 |
| 1 MB | 24.3 / 24.3 | L3 |
| 16 MB | 138.4 / 137.2 | Page Walks, L3 und DRAM |
| 256 MB | 205.1 / 193.5 | DRAM |
Was die Zahlen sagen
- Der Gast sieht die echten Caches nicht. Die VM meldet 4 MB L2 und 16 MB L3; Intel nennt für den Xeon E5-2698 v4 50 MB Cache (Intel ark, geprüft am 2026-11-01). Der Hypervisor zeigt eine eigene Topologie, und
lscpuim Cloud-Server liest diese, nicht das Silizium. Messen Sie die Hardware, statt den Angaben zu trauen. - Der Absturz kommt lange vor dem vollen Cache. Zwischen 1 MB und 4 MB steigt die Latenz auf das Fünffache, weit unter 50 MB. Mit 4-KB-Seiten berührt ein zufälliger Lauf über 4 MB 1.024 Seiten, mehr als der TLB fasst, und in einer VM läuft ein TLB-Fehlgriff durch die Seitentabellen von Gast und Host; dazu teilt sich der L3 mit den anderen Kernen des Sockels. Diese Ursachen haben wir nicht getrennt. Praktisch gilt dasselbe: Planen Sie in einer VM so, als wäre Ihr schnelles Working Set nur wenige Megabyte groß.
- Mehr RAM ist nicht schnellerer RAM. Von 1 MB bis 256 MB steigen die Kosten pro Zugriff etwa auf das Achtfache. Mehr Gigabyte bringen Kapazität, keine Geschwindigkeit; eine latenzgebundene Aufgabe beschleunigt ein kleineres heißes Set, wie die Partitionierung bei Pivot zeigt.
Was das für Servermieter bedeutet
- Cloud-Server passen zu den meisten Web-Apps, APIs und kleinen Datenbanken: Deren heiße Daten passen in den Cache, und sie warten auf Netz oder Festplatte, lange bevor sie auf RAM warten. Dimensionieren Sie nach gemessenem Speicherverbrauch, wie in unserem Leitfaden zur Cloud-Server-Größe.
- Dedizierte Server lohnen sich, wenn ein großes Working Set den ganzen Tag zufällig gelesen wird: Analytik, große Hash-Joins, In-Memory-Caches, ein PostgreSQL-Server, dessen Indizes nicht mehr in den RAM passen. Sie bekommen den ganzen L3 und alle Speicherkanäle ohne Nachbarn und können Huge Pages einschalten, die TLB-Fehlgriffe senken. Konfigurieren Sie einen auf der Seite Dedizierter Server.
- Vor jedem Upgrade lassen Sie den Test auf dem Server laufen, den Sie schon haben, und prüfen, ob Ihre langsame Aufgabe latenzgebunden ist. Doppelt so viele Kerne helfen keinem Thread, der die Hälfte der Zeit wartet. Speicher selbst ist dieses Jahr teurer geworden, siehe Server-RAM 2026; die billigste Lösung ist oft Code, der weniger davon berührt.
Vor dem Kauf testen
Unsere Tier-III-Rechenzentren in Prag und Covilha betreiben Cloud-Server und dedizierte Server. Starten Sie einen Cloud-Server, lassen Sie memlat.js neben Ihrer eigenen Last laufen und schicken Sie die Ausgabe an sales@dcxv.com: Wir helfen bei der Entscheidung, ob ein dedizierter Server die Zahlen ändern würde.
