Powrót do strony głównej

SIMD parsowanie CSV: klasyfikacja nibblami

Artykuł opisuje algorytm SIMD parsowania CSV: klasyfikacja nibblami, maski bitowe dla klas, prefiksowy XOR dla pól w cudzysłowach. Wektoryzowany kod NEON bez rozgałęzień przetwarza 16 bajtów za cykl. Podejście z simdjson zaadaptowane dla RFC4180.

Wektoryzowany CSV: SIMD bez if i pętli
Advertisement 728x90

Wektorowy parsowanie CSV z wykorzystaniem SIMD i masek bitowych

Parsowanie CSV zgodnie z RFC4180 wymaga identyfikacji znaków strukturalnych: przecinki dla kolumn, \r\n dla wierszy, cudzysłowy dla ekranowania. SIMD umożliwia klasyfikację 16 bajtów na cykl bez rozgałęzień, wykorzystując tablice wyszukiwania dla półbajtów.

Algorytm działa z ARM NEON (odpowiedniki w x86 AVX2). Każdy bajt dzielony jest na starszy i młodszy półbajt. Dwie tablice po 16 elementów zwracają bity klas: PRZECINEK=1, CUDZYSŁÓW=2, NOWA_LINIA=3.

Dla przecinka (0x2C): starszy półbajt 0x2 → PRZECINEK|CUDZYSŁÓW, młodszy 0xC → PRZECINEK. AND pozostawia PRZECINEK.

Google AdInline article slot

Weryfikacja fałszywych trafień: liczba bajtów w klasie musi równać się iloczynowi unikalnych półbajtów.

Wektorowa implementacja klasyfikatora

Skalarna pętla zastępowana jest przetwarzaniem fragmentów po 16 bajtów:

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

const LO_LOOKUP: [u8; 16] = {
    let mut t = [0u8; 16];
    t[0x2] = CUDZYSŁÓW;
    t[0xA] = NOWA_LINIA;
    t[0xC] = PRZECINEK;
    t[0xD] = NOWA_LINIA;
    t
};

const HI_LOOKUP: [u8; 16] = {
    let mut t = [0u8; 16];
    t[0x0] = NOWA_LINIA;
    t[0x2] = PRZECINEK | CUDZYSŁÓW;
    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 wykonują wyszukiwanie, vandq_u8 — AND. Reszta przetwarzana jest z dopełnieniem.

Google AdInline article slot

Kompresja do masek bitowych

Sklasyfikowane bajty przekształcane są w trzy Vec<u64>, gdzie każdy bit maski odpowiada pozycji znaku klasy. u64 opisuje 64 bajty strumienia.

Przykład dla alice,30,Irvine\n:

  • Maska PRZECINEK: pozycje 5, 8
  • Maska CUDZYSŁÓW: pusta
  • Maska NOWA_LINIA: pozycja 15

Filtrowanie pól w cudzysłowach

Klasyfikator oznacza wszystkie znaki, w tym wewnętrzne. Do filtrowania używany jest prefiksowy XOR maski CUDZYSŁÓW.

Google AdInline article slot

Parzystość cudzysłowów określa stan:

  • Nieparzysta liczba po pozycji → wewnątrz pola
  • Parzysta → na zewnątrz

Ekranowane "" samobalansują się. W przykładzie "where she said, ""hi"\nto me":

  • Pozycja 0 ("): 1 cudzysłów → wewnątrz
  • Pozycja 17 ("): 2 → na zewnątrz
  • Pozycja 29 ("): 6 → na zewnątrz

Wewnętrzne przecinki i \n są ignorowane.

Prefiksowy XOR dla stanów

Dla fragmentów 16-bajtowych obliczany jest kumulacyjny XOR maski CUDZYSŁÓW. Bit i równa się parzystości cudzysłowów od pozycji i do końca.

// Pseudokod
let prefix_xor = 0;
for (i, &bit) in quote_mask.iter().enumerate() {
    prefix_xor ^= bit;
    if prefix_xor == 0 {
        // na zewnątrz pola
    }
}

Wersja SIMD wykorzystuje poziomy XOR po fragmentach z maską dla popcount lub bitscan.

Zbieranie pozycji separatorów

Przefiltrowane maski są skanowane w celu wyodrębnienia pozycji prawdziwych separatorów:

  • Przecinki na zewnątrz → granice kolumn
  • \n na zewnątrz → granice wierszy

Pozycje zapisywane są w tablicach offsets. Pozwala to podzielić strumień bez kopiowania danych.

Zalety podejścia:

  • Brak rozgałęzień w jądrze
  • 16x równoległość na fragment
  • Kompaktowe reprezentacje bitowe
  • Łatwo rozszerzalne na 32/64 bajty (AVX512)

Co jest ważne

  • Dwie 16-bajtowe tablice półbajtów zastępują 256-bajtową LUT dla SIMD.
  • AND dwóch wyszukiwań eliminuje fałszywe klasy bez warunków.
  • Prefiksowy XOR określa granice pól w cudzysłowach w O(n).
  • Maski bitowe kompresują dane do 3/8 oryginału.
  • Algorytm przenośny: NEON → SSE4/AVX2 z pshufb/_mm_shuffle_epi8.

— Editorial Team

Advertisement 728x90

Czytaj dalej