2 Punkte von GN⁺ 2026-01-08 | 1 Kommentare | Auf WhatsApp teilen
  • 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 -w erreicht 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.c wurde mit GCC 12 und den Optionen -O3 -march=native kompiliert 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 -w zum einfachen Zählen von Wörtern ausgeführt
    • Ergebnis: 245,2 MB/s, langsamer als erwartet
  • wc verarbeitet 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 VPCMPEQB verwendet
  • Zur Erkennung von Wortgrenzen wurden Bitmasken (PMOVMSKB) und der Befehl Find First Set(ffs) eingesetzt
    • Inspiriert von der strlen-Implementierung in Cosmopolitan libc

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)
  • 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

 
GN⁺ 2026-01-08
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

    • Ich halte die genannten Werte pro Einzelkern für deutlich zu niedrig
      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
    • Ich frage mich, woher diese Grenze kommt — ob sie an der Bus-Architektur zwischen Kern und Cache oder am Speichercontroller liegt
    • Ich frage mich, warum Apples M-Serie pro Kern die dreifache Bandbreite von x86 haben soll
    • Bei aktuellen Chips ist es schwer, die Speicherbandbreite allein mit der CPU auszulasten; dafür braucht man eher die iGPU
      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
    • Samsungs NVMe-SSDs werben mit 14 GB/s Lesegeschwindigkeit; wenn ein einzelner CPU-Kern bei 6 GB/s liegt, ist das Verhältnis dieser Werte interessant
  • Es sieht so aus, als hätte der Autor des Originalbeitrags die Ausgabe des Befehls time falsch interpretiert
    Die system-Zeit ist die CPU-Zeit, die der Kernel im Namen des Prozesses verbraucht hat
    Wenn im Beispiel real 0,395 s, user 0,196 s und sys 0,117 s stehen, dann hat die CPU insgesamt nur 313 ms gearbeitet, und die übrigen 82 ms war sie untätig
    Das 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

    • Ich habe früher einmal an einem Wettbewerb zum Zählen von Worthäufigkeiten teilgenommen
      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
    • Wenn dieser Test auf nur einem Kern durchgeführt wurde, dann ist die oben behauptete „6-GB/s-Grenze“ experimentell widerlegt
  • 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 wie malloc() hätte, könnte man dem OS die Persistenz überlassen, indem man den Programmspeicher einfach per Dateinamen angibt
    Viele Software-Designs sind noch immer an die Beschränkungen der Festplattenära gebunden

    • Aber fsync() ist immer noch langsam
      Für echte Persistenz braucht es unabhängig von rotierenden Festplatten einen anderen Ansatz
    • So etwas lässt sich auch unter Linux umsetzen
      Tatsächlich laufen die meisten Speicheranforderungen über mmap()
      Allerdings kann das langsamer als read/write sein, weil der Kernel das Zugriffsmuster schwer vorhersagen kann
  • In 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

    • Tatsächlich ist die Leistung von Cloud-Instanzen fünfmal langsamer als ein M1 MacBook und gleichzeitig viel teurer
      Als Remote-Workstation für Entwickler ist das ineffizient
    • Dass die meisten GUI-Apps langsam sind, liegt weiterhin an I/O-Wartezeiten
      Telegram oder FB Messenger sind schnell, Teams oder Skype dagegen nicht
    • CRT-Monitore haben Daten schneller angezeigt
      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

    • Ich habe einmal einen gebrauchten Epyc-Server mit 2 TB DDR4-RAM für 5.000 Dollar gesehen
      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

    • Dank moderner Technologien wie CXL/PCIe kann man inzwischen auch RAM und Speichercontroller als eine Art I/O-Gerät betrachten
    • In früheren Datenbankvorlesungen wurde I/O-Performance über die Suchzeit von Festplatten gemessen
      Die Geschwindigkeit beim sequentiellen Lesen lässt sich nicht per Code verbessern; entscheidend war daher die Optimierung nicht-sequenzieller Zugriffe