55//! parse + accumulate logic. The workspace's SIMD iron rule is "all SIMD from
66//! `ndarray::simd`" (`simd-savant` agent,
77//! `.claude/knowledge/ndarray-vertical-simd-alien-magic.md`) — this module
8- //! uses `ndarray::simd::U8x32::cmpeq_mask` exclusively, never a raw
8+ //! uses `ndarray::simd::{ U8x32, U8x64} ::cmpeq_mask` exclusively, never a raw
99//! `core::arch` intrinsic, `pulp`, `wide`, or `memchr`.
1010//!
11- //! `U8x32` is the AVX2-native byte width (one `__m256i` = 32 bytes; see
12- //! `ndarray/src/simd_avx2.rs` module doc). The corpus is scanned in 32-byte
13- //! strides: for each block, `cmpeq_mask(U8x32::splat(b'\n'))` and
14- //! `cmpeq_mask(U8x32::splat(b';'))` produce 32-bit masks with bit `i` set
15- //! iff `block[i]` matches. The set bits of the combined mask (newline and
16- //! semicolon bytes never coincide, so `nl_mask | semi_mask` has no lost
17- //! information) are walked in ascending order via the classic
18- //! `mask & (mask - 1)` "clear lowest set bit" trick, recovering the ordered
19- //! sequence of delimiter events for the block.
11+ //! **Width is compile-time-dispatched** (`SimdByte`): under AVX-512
12+ //! (`target-cpu=x86-64-v4` / `native`) the scan strides 64-byte `zmm`
13+ //! blocks via `U8x64` (`cmpeq_mask -> u64`); under the AVX2 default
14+ //! (`x86-64-v3`, the CI baseline) it strides 32-byte `ymm` blocks via
15+ //! `U8x32` (`cmpeq_mask -> u32`; one `__m256i`, see
16+ //! `ndarray/src/simd_avx2.rs`). For each block,
17+ //! `cmpeq_mask(SimdByte::splat(b'\n'))` and `cmpeq_mask(SimdByte::splat(b';'))`
18+ //! produce a `SimdByte::LANES`-bit mask with bit `i` set iff `block[i]`
19+ //! matches. The set bits of the combined mask (newline and semicolon bytes
20+ //! never coincide, so `nl_mask | semi_mask` has no lost information) are
21+ //! walked in ascending order via the classic `mask & (mask - 1)` "clear
22+ //! lowest set bit" trick, recovering the ordered sequence of delimiter
23+ //! events for the block. The walk is generic over the `u32`/`u64` mask
24+ //! width, so the same body serves both dispatched widths.
2025//!
2126//! **Parse remains scalar** (SWAR/branchless parse deliberately deferred —
2227//! see `README.md` §1 "NOT reimplemented here"): `parse_temp_tenths` is the
2732//! ## Cross-block record state
2833//!
2934//! A record's `;` and its `\n` are not guaranteed to land in the same
30- //! 32-byte block (short station names put the `;` near a block's end and
31- //! the `\n` in the next block, or vice versa). Two scalars carry state
32- //! across block boundaries:
35+ //! block (short station names put the `;` near a block's end and the `\n`
36+ //! in the next block, or vice versa). Two scalars carry state across block
37+ //! boundaries:
3338//!
3439//! - `line_start: usize` — the byte offset where the current (in-progress)
3540//! station name begins.
3641//! - `pending_semi: Option<usize>` — `Some(offset)` once this record's `;`
3742//! has been seen but its `\n` has not yet arrived; `None` while still
3843//! scanning for the `;`.
3944//!
40- //! The tail (fewer than 32 bytes remaining after the last full block) is
41- //! finished with a plain byte-wise scalar loop — the same station-name /
42- //! temp-field extraction shape as `lane_a_scalar`, continuing from whatever
43- //! `line_start` / `pending_semi` state the SIMD pass left behind.
45+ //! The tail (fewer than one full `SimdByte`-wide block remaining after the
46+ //! last full block) is finished with a plain byte-wise scalar loop — the
47+ //! same station-name / temp-field extraction shape as `lane_a_scalar`,
48+ //! continuing from whatever `line_start` / `pending_semi` state the SIMD
49+ //! pass left behind.
4450
4551use crate :: { parse_temp_tenths, Stats } ;
46- use ndarray:: simd:: { array_chunks, U8x32 } ;
52+ use ndarray:: simd:: array_chunks;
4753use std:: collections:: BTreeMap ;
4854
49- /// Lane B — SIMD delimiter scan. One pass over `data` in 32-byte strides
50- /// using `ndarray::simd::U8x32::cmpeq_mask` to locate `;` and `\n` bytes;
55+ // Compile-time SIMD byte-width dispatch — both widths are `ndarray::simd`
56+ // types (the iron rule; never a raw `core::arch` intrinsic). AVX-512
57+ // targets (`target-cpu=x86-64-v4` / `native`) scan in 64-byte `zmm`
58+ // strides via `U8x64`; the AVX2 default (`x86-64-v3`, the CI baseline)
59+ // scans in 32-byte `ymm` strides via `U8x32`. The stride, the needle
60+ // width, and the `array_chunks` const-generic all key off
61+ // `SimdByte::LANES`, so the same body strides the widest lane the target
62+ // actually provides. `cmpeq_mask` returns a `u64` (avx512) / `u32`
63+ // (avx2) mask; the ascending set-bit walk below is generic over both.
64+ #[ cfg( not( target_feature = "avx512f" ) ) ]
65+ use ndarray:: simd:: U8x32 as SimdByte ;
66+ #[ cfg( target_feature = "avx512f" ) ]
67+ use ndarray:: simd:: U8x64 as SimdByte ;
68+
69+ /// The SIMD block width lane B actually strides, in bytes — the dispatched
70+ /// `SimdByte::LANES` (64 under avx512, 32 under avx2). Exposed so tests can
71+ /// assert against the real block boundary instead of a hardcoded width.
72+ #[ cfg( test) ]
73+ pub ( crate ) const SIMD_LANES : usize = SimdByte :: LANES ;
74+
75+ /// Lane B — SIMD delimiter scan. One pass over `data` in `SimdByte::LANES`-wide
76+ /// strides (64-byte `zmm` under avx512, 32-byte `ymm` under avx2) using
77+ /// `ndarray::simd::{U8x64, U8x32}::cmpeq_mask` to locate `;` and `\n` bytes;
5178/// scalar integer temp parse (see module doc); `BTreeMap<String, Stats>`
5279/// accumulation identical in shape to `lane_a_scalar`.
5380pub fn lane_b_simd ( data : & [ u8 ] ) -> BTreeMap < String , Stats > {
@@ -60,18 +87,22 @@ pub fn lane_b_simd(data: &[u8]) -> BTreeMap<String, Stats> {
6087 let mut line_start = 0usize ;
6188 let mut pending_semi: Option < usize > = None ;
6289
63- let nl_needle = U8x32 :: splat ( b'\n' ) ;
64- let semi_needle = U8x32 :: splat ( b';' ) ;
90+ let nl_needle = SimdByte :: splat ( b'\n' ) ;
91+ let semi_needle = SimdByte :: splat ( b';' ) ;
6592
66- // The non-overlapping 32-byte stride walk routes through
93+ // The non-overlapping `SimdByte::LANES`-wide stride walk routes through
6794 // `ndarray::simd::array_chunks` (simd_ops.rs) — the W1a batch-walk
6895 // primitive; `array_windows` is its OVERLAPPING sibling (GEMM-style
6996 // row windows) and is deliberately NOT used here: delimiter scanning
70- // never re-reads bytes.
71- let aligned_end = ( len / U8x32 :: LANES ) * U8x32 :: LANES ;
72- for ( chunk_idx, chunk) in array_chunks :: < u8 , 32 > ( & data[ ..aligned_end] ) . enumerate ( ) {
73- let pos = chunk_idx * U8x32 :: LANES ;
74- let block = U8x32 :: from_slice ( chunk) ;
97+ // never re-reads bytes. The const-generic is `{ SimdByte::LANES }`, so
98+ // the chunk width tracks the dispatched lane width (64 under avx512,
99+ // 32 under avx2) with no hardcoded stride.
100+ let aligned_end = ( len / SimdByte :: LANES ) * SimdByte :: LANES ;
101+ for ( chunk_idx, chunk) in
102+ array_chunks :: < u8 , { SimdByte :: LANES } > ( & data[ ..aligned_end] ) . enumerate ( )
103+ {
104+ let pos = chunk_idx * SimdByte :: LANES ;
105+ let block = SimdByte :: from_slice ( chunk) ;
75106 let nl_mask = block. cmpeq_mask ( nl_needle) ;
76107 let semi_mask = block. cmpeq_mask ( semi_needle) ;
77108 // `;` and `\n` never occupy the same byte, so OR-ing loses no
@@ -105,9 +136,10 @@ pub fn lane_b_simd(data: &[u8]) -> BTreeMap<String, Stats> {
105136 }
106137 }
107138
108- // Tail — fewer than 32 bytes remain. Finish with a plain scalar scan,
109- // continuing from whatever `line_start` / `pending_semi` state the SIMD
110- // pass left behind (mirrors `lane_a_scalar`'s per-record shape).
139+ // Tail — fewer than `SimdByte::LANES` bytes remain. Finish with a plain
140+ // scalar scan, continuing from whatever `line_start` / `pending_semi`
141+ // state the SIMD pass left behind (mirrors `lane_a_scalar`'s per-record
142+ // shape).
111143 let mut i = aligned_end;
112144 while i < len {
113145 match pending_semi {
0 commit comments