Ist I/O kein Flaschenhals mehr?
(stoppels.ch)- Eine experimentelle Überprüfung der zuletzt diskutierten Schieflage zwischen I/O-Leistung und CPU-Verarbeitungsgeschwindigkeit zeigt, dass in der Praxis weiterhin die CPU die zentrale Begrenzung ist
- Die sequenzielle Lesegeschwindigkeit erreicht 1,6 GB/s mit kaltem Cache und 12,8 GB/s mit warmem Cache, während die Wortfrequenzberechnung in einem einzelnen Thread bei nur 278 MB/s liegt
- Die Branch-Struktur des Codes behindert die Vektorisierung; selbst eine einfache Optimierung der Kleinschreibungs-Konvertierung verbessert die Leistung nur auf etwa 330 MB/s
- Selbst der Befehl
wc -werreicht nur 245 MB/s, was bestätigt, dass CPU-Berechnung und Branch-Verarbeitung statt der Festplatte den Flaschenhals bilden - Durch manuelle Vektorisierung auf Basis von AVX2 wurde die Leistung auf 1,45 GB/s gesteigert, doch das entspricht immer noch nur etwa 11 % der sequenziellen Lesegeschwindigkeit und belegt, dass nicht I/O, sondern die CPU der Flaschenhals ist
Vergleich von I/O-Geschwindigkeit und CPU-Leistung
- Auf Grundlage der Behauptung von Ben Hoyt wurde untersucht, ob die jüngsten Steigerungen der sequenziellen Lesegeschwindigkeit die stagnierende CPU-Geschwindigkeit inzwischen überholt haben
- Bei Messung auf dieselbe Weise wurden 1,6 GB/s mit kaltem Cache und 12,8 GB/s mit warmem Cache erreicht
- Wird jedoch in einem einzelnen Thread eine Wortfrequenzberechnung durchgeführt, sind nur 278 MB/s möglich
- Selbst bei warmem Cache entspricht das nur etwa einem Fünftel der Lesegeschwindigkeit
Experiment zur Wortfrequenzberechnung in C
optimized.cwurde mit GCC 12 und den Optionen-O3 -march=nativekompiliert und anschließend mit einer 425-MB-Eingabedatei ausgeführt- Ergebnis: 1,525 Sekunden Laufzeit, 278 MB/s Durchsatz
- Mehrere Branches und frühe Abbrüche im Code behindern die Vektorisierungsoptimierung des Compilers
- Nachdem die Logik zur Kleinschreibungs-Konvertierung aus der Schleife heraus verlagert wurde, stieg die Leistung auf 330 MB/s
- Mit Clang gelingt die Vektorisierung besser
Vergleich mit einfachem Wortzählen (wc -w)
- Statt einer Frequenzberechnung wurde der Befehl
wc -wzum einfachen Zählen von Wörtern ausgeführt- Ergebnis: 245,2 MB/s, langsamer als erwartet
wcverarbeitet verschiedene Leerraumzeichen und Locale-Zeichen wie' ','\n','\t'- Dadurch ist der Rechenaufwand höher als bei Code, der nur anhand einfacher Leerzeichen trennt
Versuch der Vektorisierung mit AVX2
- Unter Nutzung moderner CPU-Funktionen wurde eine Vektorisierung mit dem AVX2-Befehlssatz implementiert
- Verwendung von 256-Bit-Registern, Daten auf 32 Bit ausgerichtet
- Für den Vergleich von Leerraumzeichen wurde der Befehl
VPCMPEQBverwendet
- Zur Erkennung von Wortgrenzen wurden Bitmasken (PMOVMSKB) und der Befehl Find First Set(ffs) eingesetzt
- Inspiriert von der
strlen-Implementierung in Cosmopolitan libc
- Inspiriert von der
Leistungsergebnisse und Fazit
- Der manuell vektorisierte Code (
wc-avx2) erreichte einen Durchsatz von 1,45 GB/s- Verifiziert wurde dasselbe Ergebnis wie bei
wc -w(82.113.300 Wörter)
- Verifiziert wurde dasselbe Ergebnis wie bei
- Selbst bei kaltem Cache dominiert weiterhin die Rechenzeit im User Mode
- Damit wird bestätigt, dass CPU-Berechnung und nicht Festplatten-I/O den Flaschenhals bilden
- Insgesamt ist die Festplattengeschwindigkeit ausreichend hoch, doch CPU-Operationen wie Branch-Verarbeitung und Hash-Berechnung bleiben der begrenzende Faktor
- Code und Versuchsergebnisse sind auf GitHub (
haampie/wc-avx2) veröffentlicht
1 Kommentare
Hacker-News-Kommentare
Ich denke, die Leistungsgrenze moderner CPUs wird durch die Datenmenge bestimmt, die ein einzelner Kern verarbeiten kann, also durch die Geschwindigkeit von
memcpy()Die meisten x86-Kerne liegen bei etwa 6 GB/s, Apples M-Serie bei ungefähr 20 GB/s
Zahlen wie „200 GB/s“ aus der Werbung sind nur die aufsummierte Bandbreite aller Kerne; ein einzelner Kern bleibt weiterhin in der Nähe von 6 GB/s
Deshalb kann man diese Grenze auch mit einem perfekten Parser nicht überschreiten
Mit einem Zero-Copy-Format kann die CPU jedoch unnötige Daten überspringen und damit theoretisch die 6 GB/s „übertreffen“
Das von mir entwickelte Lite³-Format nutzt dieses Prinzip und ist dadurch bis zu 120-mal schneller als simdjson
Zen 1 erreicht zum Beispiel 25 GB/s auf einem einzelnen Kern (Referenzlink)
Laut meinen Microbenchmark-Ergebnissen kommt Zen 2 ohne AVX auf 17 GB/s und mit non-temporal AVX auf bis zu 35 GB/s
Auf dem Apple M3 Max wurden mit non-temporal NEON sogar bis zu 125 GB/s gemessen
Die Angaben von 6 GB/s für x86 und 20 GB/s für Apple liegen daher weit unter der Realität
Denn die iGPU kann auf den Unified Memory zugreifen
Deshalb ist es technisch vorteilhaft, die iGPU als Blitter für große Speicherkopien, paralleles Parsing oder Kompression/Dekompression zu nutzen
Das im Zero-Copy-Format gemeinte „Überspringen“ erfolgt allerdings auf Cacheline-Ebene
Es sieht so aus, als hätte der Autor des Originalbeitrags die Ausgabe des Befehls
timefalsch interpretiertDie
system-Zeit ist die CPU-Zeit, die der Kernel im Namen des Prozesses verbraucht hatWenn im Beispiel
real0,395 s,user0,196 s undsys0,117 s stehen, dann hat die CPU insgesamt nur 313 ms gearbeitet, und die übrigen 82 ms war sie untätigDas bedeutet: Das System war zwar schneller als das Plattensubsystem, aber der Unterschied ist nicht groß
Außerdem ist der I/O-Pfad CPU-bound — selbst wenn Platte und Code unendlich schnell wären, würde allein der Kernel-I/O-Code 117 ms benötigen
Ich bin der Verfasser des Beitrags. Es gibt einen zweiten Teil: I/O is no longer the bottleneck, part 2
Interessant ist dieser Analysebeitrag, der verschiedene von den Teilnehmern verwendete Optimierungstechniken behandelt
Je nach Komplexität des Problems oder der Anzahl der als Leerraum klassifizierten Zeichen unterschieden sich die Ansätze
Ein Performance-Engpass ist nicht immer nur ein einzelner Faktor wie „CPU oder I/O“, sondern die Ressource, die in der realen Workload zuerst sättigt
Das kann die CPU, Speicherbandbreite, der Cache, die Festplatte, das Netzwerk, Locks oder Latenz sein
Deshalb muss man messen, per Profiling belegen und nach jeder Änderung erneut messen
Das Problem ist nicht CPU oder I/O, sondern das Gleichgewicht zwischen Latenz und Durchsatz
Die meiste Software ist langsam, weil sie Latenz ignoriert
Wenn man Daten linear im Speicher anordnet oder Batch-Verarbeitung und Parallelisierung anwendet, wird sie deutlich schneller
Ich stelle mir eine Architektur vor, die nur aus CPU ↔ Cache ↔ nichtflüchtigem Speicher besteht
Wenn
mmap()dieselben Performance-Eigenschaften wiemalloc()hätte, könnte man dem OS die Persistenz überlassen, indem man den Programmspeicher einfach per Dateinamen angibtViele Software-Designs sind noch immer an die Beschränkungen der Festplattenära gebunden
fsync()ist immer noch langsamFür echte Persistenz braucht es unabhängig von rotierenden Festplatten einen anderen Ansatz
Tatsächlich laufen die meisten Speicheranforderungen über
mmap()Allerdings kann das langsamer als
read/writesein, weil der Kernel das Zugriffsmuster schwer vorhersagen kannIn Cloud-Umgebungen wird Performance teils auch als Mittel zur Tarifsteuerung eingesetzt
Die Hardwareleistung hat sich erstaunlich stark verbessert, aber manche Software — besonders Windows oder Messenger-Apps — fühlt sich trotzdem langsamer an
Als Remote-Workstation für Entwickler ist das ineffizient
Telegram oder FB Messenger sind schnell, Teams oder Skype dagegen nicht
Manche LCDs haben 500 ms Latenz
Als NVMe-SSDs neu waren, machte man den Witz: „Jetzt haben wir praktisch 2 TB RAM“
Heute haben GPU-Server tatsächlich 2 TB RAM — beeindruckende Ingenieursleistung
Im Nachhinein bedaure ich, ihn nicht gekauft zu haben
Aus meiner Erfahrung bei der Optimierung von OLAP-Datenbanken in Umgebungen mit sehr hoher Parallelität war der Engpass meist die Speichergeschwindigkeit
Der I/O-Flaschenhals bezog sich ursprünglich nicht auf sequentielles Lesen, sondern auf die Suchzeit (Seek Time)
Ich verstehe die Aussage des Beitrags, wollte diesen Punkt aber anmerken
Die Geschwindigkeit beim sequentiellen Lesen lässt sich nicht per Code verbessern; entscheidend war daher die Optimierung nicht-sequenzieller Zugriffe