Zurück zur Startseite

SIMD CSV-Parsing: Nibble-Klassifikation

Der Artikel beschreibt einen SIMD CSV-Parsing-Algorithmus: Nibble-Klassifikation, Bitmasken für Klassen, Prefix XOR für zitierte Felder. Branchloser vektorisierter NEON-Code verarbeitet 16 Bytes pro Schleife. Ansatz aus simdjson angepasst für RFC4180.

Vektorisierter CSV: SIMD ohne Ifs und Schleifen
Advertisement 728x90

Vektorisierte CSV-Verarbeitung mit SIMD und Bitmasken

Das Parsen von CSV-Dateien gemäß RFC4180 erfordert die Identifizierung struktureller Zeichen: Kommas für Spalten, \r\n für Zeilen und Anführungszeichen für Escaping. SIMD ermöglicht die Klassifizierung von 16 Bytes pro Zyklus ohne Verzweigungen, wobei Lookup-Tabellen für Nibbles verwendet werden.

Der Algorithmus funktioniert mit ARM NEON (ähnlich zu x86 AVX2). Jedes Byte wird in hohes und niedriges Nibble aufgeteilt. Zwei 16-Elemente-Tabellen liefern Klassenbits: KOMMA=1, ANFÜHRUNGSZEICHEN=2, ZEILENUMBRUCH=3.

Für ein Komma (0x2C): hohes Nibble 0x2 → KOMMA|ANFÜHRUNGSZEICHEN, niedriges Nibble 0xC → KOMMA. UND lässt KOMMA übrig.

Google AdInline article slot

Falsch-Positiv-Prüfung: Die Anzahl der Bytes in einer Klasse muss dem Produkt der eindeutigen Nibbles entsprechen.

Implementierung des vektorisierten Klassifizierers

Die skalare Schleife wird durch die Verarbeitung von 16-Byte-Blöcken ersetzt:

use std::arch::aarch64::*;

const LO_LOOKUP: [u8; 16] = {
    let mut t = [0u8; 16];
    t[0x2] = QUOTE;
    t[0xA] = NEWLINE;
    t[0xC] = COMMA;
    t[0xD] = NEWLINE;
    t
};

const HI_LOOKUP: [u8; 16] = {
    let mut t = [0u8; 16];
    t[0x0] = NEWLINE;
    t[0x2] = COMMA | QUOTE;
    t
};

unsafe {
    let lo_lut = vld1q_u8(LO_LOOKUP.as_ptr());
    let hi_lut = vld1q_u8(HI_LOOKUP.as_ptr());
    let mask = vdupq_n_u8(0x0F);

    for chunk in bytes.chunks_exact(16) {
        let input = vld1q_u8(chunk.as_ptr());
        let lo_nibbles = vandq_u8(input, mask);
        let hi_nibbles = vandq_u8(vshrq_n_u8::<4>(input), mask);
        
        let lo_result = vqtbl1q_u8(lo_lut, lo_nibbles);
        let hi_result = vqtbl1q_u8(hi_lut, hi_nibbles);
        let classified = vandq_u8(lo_result, hi_result);
        
        let mut out = [0u8; 16];
        vst1q_u8(out.as_mut_ptr(), classified);
        classified_bytes.extend_from_slice(&out);
    }
}

Intrinsics vqtbl1q_u8 führen die Lookup-Operation durch, vandq_u8 — UND. Der Rest wird mit Padding behandelt.

Google AdInline article slot

Komprimierung in Bitmasken

Klassifizierte Bytes werden in drei Vec<u64> umgewandelt, wobei jedes Bit in der Maske einer Zeichenklassenposition entspricht. u64 beschreibt 64 Bytes des Datenstroms.

Beispiel für alice,30,Irvine\n:

  • KOMMA-Maske: Positionen 5, 8
  • ANFÜHRUNGSZEICHEN-Maske: leer
  • ZEILENUMBRUCH-Maske: Position 15

Filtern von Anführungszeichen-Feldern

Der Klassifizierer markiert alle Zeichen, einschließlich interner. Zum Filtern wird ein Präfix-XOR der ANFÜHRUNGSZEICHEN-Maske verwendet.

Google AdInline article slot

Die Parität der Anführungszeichen bestimmt den Zustand:

  • Ungerade Anzahl nach Position → innerhalb des Feldes
  • Gerade Anzahl → außerhalb des Feldes

Escaped "" gleicht sich selbst aus. Im Beispiel "where she said, ""hi"\nto me":

  • Position 0 ("): 1 Anführungszeichen → innerhalb
  • Position 17 ("): 2 → außerhalb
  • Position 29 ("): 6 → außerhalb

Interne Kommas und \n werden ignoriert.

Präfix-XOR für Zustände

Für 16-Byte-Blöcke wird ein kumulatives XOR der ANFÜHRUNGSZEICHEN-Maske berechnet. Bit i entspricht der Parität der Anführungszeichen von Position i bis zum Ende.

// Pseudocode
let prefix_xor = 0;
for (i, &bit) in quote_mask.iter().enumerate() {
    prefix_xor ^= bit;
    if prefix_xor == 0 {
        // außerhalb des Feldes
    }
}

Die SIMD-Version verwendet horizontales XOR über Blöcke mit einer Maske für Popcount oder Bitscan.

Sammeln von Trennzeichenpositionen

Gefilterte Masken werden gescannt, um Positionen echter Trennzeichen zu extrahieren:

  • Kommas außerhalb → Spaltengrenzen
  • \n außerhalb → Zeilengrenzen

Positionen werden in Offset-Arrays gespeichert. Dies ermöglicht das Aufteilen des Datenstroms ohne Datenkopie.

Vorteile des Ansatzes:

  • Keine Verzweigungen im Kern
  • 16-fache Parallelität pro Block
  • Kompakte Bitdarstellungen
  • Leicht erweiterbar auf 32/64 Bytes (AVX512)

Wichtige Punkte

  • Zwei 16-Byte-Nibble-Tabellen ersetzen eine 256-Byte-LUT für SIMD.
  • UND von zwei Lookups eliminiert falsche Klassen ohne Bedingungen.
  • Präfix-XOR bestimmt Grenzen von Anführungszeichen-Feldern in O(n).
  • Bitmasken komprimieren Daten auf 3/8 der ursprünglichen Größe.
  • Der Algorithmus ist portabel: NEON → SSE4/AVX2 mit pshufb/_mm_shuffle_epi8.

— Editorial Team

Advertisement 728x90

Weiterlesen