CSV-Parsing mit SIMD erreicht 21 GB/s auf AMD 9950X
(nietras.com)- Sep 0.10.0 erreicht durch Optimierungen für CPUs mit AVX-512-Unterstützung wie AMD 9950X (Zen 5) beim Low-Level-CSV-Parsing 21 GB/s und ist damit schneller als zuvor mit rund 18 GB/s
- Der Performance-Zuwachs stammt aus einer geänderten Parser-Struktur, die den Flaschenhals durch Roundtrips über Maskenregister in der AVX-512-Codegenerierung von .NET 9.0 reduziert
- Der neue AVX-512-to-256-Parser lädt
charmit 512 Bit und wandelt sie anschließend in 256-Bit-Byte-Vektoren um, wodurch Maskenverarbeitung und separate Permutationskosten vermieden werden - Die Low-Level-Parsing-Performance von Sep verbesserte sich von rund 7 GB/s mit 0.1.0 auf 5950X/.NET 7.0 im Jahr 2023 auf rund 21 GB/s mit 0.10.0 auf 9950X/.NET 9.0 – eine Verbesserung um etwa das Dreifache
- Auch in den High-Level-Benchmarks verarbeitete Sep auf dem 9950X mit mehreren Threads 1 Mio. Zeilen package assets in 72,213 ms mit rund 8,0 GB/s; bei Float-Daten wurden ebenfalls rund 8,1 GB/s erreicht
Ziel und Ergebnis von Sep 0.10.0
- Sep 0.10.0 wurde am 22. April 2025 veröffentlicht und enthält Optimierungen für CPUs mit AVX-512-Unterstützung wie AMD 9950X (Zen 5) sowie 9950X-Benchmarks
- Beim Low-Level-CSV-Parsing erreicht Sep auf dem 9950X 21 GB/s
- Vor 0.10.0 lag der Wert auf demselben 9950X bei rund 18 GB/s
- Der Analysebereich ist das Low-Level-
Rows-Parsing der package-assets-CSV-Daten; alle Werte beziehen sich auf einen einzelnen Thread - Benchmark-Werte können um einige Prozentpunkte schwanken, sodass in bestimmten Releases kleine Regressionen sichtbar sein können
Performance-Entwicklung von 0.1.0 bis 0.10.0
- Die Performance von Sep verbesserte sich schrittweise durch das Zusammenspiel von Codeänderungen, Wechseln der .NET-Version und neuen CPU-Generationen
- Der repräsentative Performance-Verlauf sieht wie folgt aus
- 0.1.0, 5950X, .NET 7.0: rund 7 GB/s
- 0.3.0, 5950X, .NET 8.0: rund 12 GB/s
- 0.6.0, 5950X, .NET 9.0: rund 13 GB/s
- 0.9.0, 9950X, .NET 9.0: rund 18 GB/s
- 0.10.0, 9950X, .NET 9.0: rund 21 GB/s
- Seit der Veröffentlichung von Sep im Juni 2023 wurde es in etwas weniger als zwei Jahren um etwa das Dreifache schneller
- Vergleicht man Sep 0.9.0 auf dem 5950X mit Sep 0.10.0 auf dem 9950X, ergibt sich eine Verbesserung um etwa das 1,6-Fache
- Der Boost-Takt des 9950X beträgt 5,7 GHz, der des 5950X 4,9 GHz
- Allein dieser Taktunterschied dürfte etwa den Faktor 1,2 erklären
Maskenregister-Flaschenhals in der .NET-AVX-512-Codegenerierung
- Sep unterstützt seit 0.2.3 AVX-512, doch .NET 8 unterstützte die AVX-512-Maskenregister
k1-k8damals nicht explizit - Bei der bisherigen AVX-512-Codegenerierung entstand ein Ablauf, bei dem Vergleichsergebnisse zunächst in Maskenregistern landen, dann in allgemeine Register verschoben werden und anschließend wieder in Maskenregister zurückwandern
- Nach dem Upgrade auf den 9950X erreichte Sep 0.9.0 beim Low-Level-CSV-Parsing rund 18 GB/s, etwa das 1,4-Fache des 5950X
- Beim Vergleich durch Wechsel des Parsers per Umgebungsvariable erreichte der AVX2-Parser auf dem 9950X rund 20 GB/s und war damit etwa 10 % schneller als der bisherige AVX-512-Parser
- Dieser Unterschied bestätigte, dass die Verarbeitung der AVX-512-Maskenregister weiterhin die Performance beeinflusst
Grundstruktur der Sep-Parsing-Schleife
- Alle Parser von Sep folgen derselben Grundstruktur und unterstützen über eine generische
Parse-Methode zwei Pfade mit unterschiedlicher Behandlung von AnführungszeichenParseColInfos: Wird bei Verarbeitung von Anführungszeichen genutzt und erfordert mehr ZustandsverfolgungParseColEnds: Wird verwendet, wenn keine Verarbeitung von Anführungszeichen nötig ist
- Das Parsing erfolgt in
char-Spans aus Arrays; das Beispiel nutzt eine Größe von 16K- Diese Größe ist klein genug, um in den CPU-Cache zu passen, und ist später auch für effizientes Multithreading vorteilhaft
- Die Schleife lädt 16-Bit-Zeichendaten in SIMD-Register, wandelt sie in Byte-SIMD-Register um und vergleicht anschließend CSV-Sonderzeichen
- Zu den Vergleichszielen gehören
\n,\r,",;und weitere
- Zu den Vergleichszielen gehören
- Das Vergleichsergebnis wird in eine Bitmaske umgewandelt, und nur die in der Maske gesetzten Bits werden sequenziell geparst
- Performance-Unterschiede hängen stark davon ab, in welchen Maschinencode dieser SIMD-C#-Code von .NET per JIT kompiliert wird
Bisheriger AVX-512-Parser und Anpassung in 0.10.0
SepParserAvx512PackCmpOrMoveMaskTzcntaus 0.9.0 lädt jeweils 32charin zwei 512-Bit-SIMD-Register, packt sie in einen 512-Bit-Byte-Vektor und verarbeitet damit 64 Zeichen pro Schleifendurchlauf- Die gepackten Daten sind in ihrer Reihenfolge vermischt und müssen mit
PermuteVar8x64wieder neu angeordnet werden - Im Assembly von .NET 9.0 führte jedes
Vec.Equalszu den beiden Instruktionenvpcmpeqbundvpmovm2b; dabei werden Bewegungen zwischen Maskenregistern wiek1und allgemeinenzmm-Vektorregistern wiederholt - In Sep 0.10.0 wurde der
MoveMask-Aufruf weiter nach vorne gezogen, um die Zahl der Roundtrips zwischen Maskenregistern und allgemeinen Registern zu reduzieren- In anderen Parsern wird
MoveMasknur bei Bedarf aufgerufen, um auf dem schnellen Pfad „ohne Sonderzeichen“ die Anzahl der Instruktionen zu verringern
- In anderen Parsern wird
- Auch nach der Anpassung bleibt die Bewegung vom Maskenregister ins allgemeine Register bestehen, doch die Gesamtzahl der Assembly-Instruktionen sinkt
AVX2 und neuer AVX-512-to-256-Parser
- Das Assembly des AVX2-basierten
SepParserAvx2PackCmpOrMoveMaskTzcnthat keine Maskenregister und daher eine geradlinigere Struktur - Dank dieser Struktur war der AVX2-Parser schneller als der bisherige AVX-512-Parser aus 0.9.0
- Der neue
SepParserAvx512To256CmpOrMoveMaskTzcntaus 0.10.0 lädtcharmit AVX-512-Instruktionen und erzeugt anschließend mitConvertToVector256ByteWithSaturationeinen 256-Bit-Byte-Vektor- Die tatsächliche Instruktion ist
vpmovuswb - Der Durchsatz pro Schleifendurchlauf beträgt „nur“ 32
char, aber die Struktur ist einfacher
- Die tatsächliche Instruktion ist
- Dieser Ansatz umgeht das Problem der 512-Bit-Maskenregister, und da die gepackten Daten bereits in der richtigen Reihenfolge in
ymm4liegen, ist auch keine separate Permutation nötig - Der neue Parser hebt die Sep-Parsing-Performance auf dem 9950X auf rund 21 GB/s
Low-Level-Benchmarks der Parser auf dem 9950X
- Beim Vergleich aller Parser auf AMD 9950X per Umgebungsvariable war der neue AVX-512-to-256-Parser am schnellsten
- Die wichtigsten Ergebnisse lauten wie folgt
SepParserAvx512To256CmpOrMoveMaskTzcnt: 21597,7 MB/s, 27,0 ns/row, 1,351 msSepParserVector256NrwCmpExtMsbTzcnt: 20608,5 MB/s, 28,3 ns/row, 1,416 msSepParserAvx2PackCmpOrMoveMaskTzcnt: 20599,3 MB/s, 28,3 ns/row, 1,417 msSepParserAvx512PackCmpOrMoveMaskTzcnt: 19944,3 MB/s, 29,3 ns/row, 1,463 ms
- Der plattformübergreifende Parser auf Basis von
Vector256erreicht nahezu das Niveau von AVX2 - Die plattformübergreifenden Parser auf Basis von
Vector128undVector512sind weiterhin schnell, aber 5 bis 10 % langsamer;Vector512war langsamer alsVector128 SepParserIndexOfAnyfällt mit 2787,0 MB/s deutlich zurück, undVector64wird auf dem 9950X nicht beschleunigt und bleibt bei 459,9 MB/s
High-Level-Benchmarks auf 5950X und 9950X
- Bei den package-assets-Daten mit 1 Mio. Zeilen war der 9950X deutlich schneller als der 5950X
- 5950X
Sep_MT: 119,430 ms, 4888,1 MB/s - 9950X
Sep_MT: 72,213 ms, 8084,1 MB/s
- 5950X
- Auf dem 9950X verarbeitet
Sepmit einem einzelnen Thread 1 Mio. package-assets-Zeilen in 291,979 ms mit 1999,4 MB/s - Im selben package-assets-Benchmark auf dem 9950X zeigen die Vergleichskandidaten folgende Performance
Sylvan: 413,265 ms, 1412,6 MB/sReadLine_: 377,033 ms, 1548,4 MB/s, Allokationen 1991,04 MBCsvHelper: 1005,323 ms, 580,7 MB/s
- Auch bei Float-Daten verarbeitet Sep auf dem 9950X mit mehreren Threads 25.000 Zeilen in 2,497 ms mit 8136,8 MB/s
- Die Verbesserung der High-Level-Benchmarks vom 5950X zum 9950X liegt ähnlich wie bei den Low-Level-Benchmarks bei etwa dem 1,5- bis 1,6-Fachen
Noch keine Kommentare.