Skip to main content

fhp_simd/
sse42.rs

1//! SSE4.2 accelerated operations (128-bit, x86_64).
2//!
3//! Each function is marked `#[target_feature(enable = "sse4.2")]` so the
4//! compiler generates SSE4.2 instructions without requiring the entire
5//! crate to be compiled with `-C target-feature=+sse4.2`.
6
7#[cfg(target_arch = "x86_64")]
8use core::arch::x86_64::*;
9
10use crate::{DelimiterResult, classify_byte};
11
12/// Scan `haystack` for the first HTML delimiter using SSE4.2 (128-bit).
13///
14/// Processes 16 bytes at a time. Falls back to scalar for the tail.
15///
16/// # Safety
17///
18/// Caller must ensure the CPU supports SSE4.2 (`is_x86_feature_detected!("sse4.2")`).
19#[target_feature(enable = "sse4.2")]
20#[cfg(target_arch = "x86_64")]
21pub unsafe fn find_delimiters(haystack: &[u8]) -> DelimiterResult {
22    let len = haystack.len();
23    let ptr = haystack.as_ptr();
24    let mut offset = 0;
25
26    // SAFETY: all intrinsics below require SSE4.2, guaranteed by #[target_feature].
27    unsafe {
28        // Splat each of the 7 delimiter bytes into its own register. We use
29        // explicit-length equality compares (`_mm_cmpeq_epi8`) rather than the
30        // implicit-length string intrinsic `_mm_cmpistri`: the latter treats a
31        // NUL byte as a string terminator and would miss any delimiter at or
32        // after a `\0`, diverging from the scalar and NEON backends (a `&str`
33        // may legally contain NUL bytes).
34        let lt = _mm_set1_epi8(b'<' as i8);
35        let gt = _mm_set1_epi8(b'>' as i8);
36        let amp = _mm_set1_epi8(b'&' as i8);
37        let quot = _mm_set1_epi8(b'"' as i8);
38        let apos = _mm_set1_epi8(b'\'' as i8);
39        let eq = _mm_set1_epi8(b'=' as i8);
40        let slash = _mm_set1_epi8(b'/' as i8);
41
42        while offset + 16 <= len {
43            // Load 16 bytes from haystack (unaligned).
44            let chunk = _mm_loadu_si128(ptr.add(offset) as *const __m128i);
45
46            // OR the per-delimiter equality masks together: 0xFF in any lane
47            // that matches one of the seven delimiters.
48            let m = _mm_or_si128(
49                _mm_or_si128(
50                    _mm_or_si128(_mm_cmpeq_epi8(chunk, lt), _mm_cmpeq_epi8(chunk, gt)),
51                    _mm_or_si128(_mm_cmpeq_epi8(chunk, amp), _mm_cmpeq_epi8(chunk, quot)),
52                ),
53                _mm_or_si128(
54                    _mm_or_si128(_mm_cmpeq_epi8(chunk, apos), _mm_cmpeq_epi8(chunk, eq)),
55                    _mm_cmpeq_epi8(chunk, slash),
56                ),
57            );
58            let mask = _mm_movemask_epi8(m) as u16;
59            if mask != 0 {
60                let pos = offset + mask.trailing_zeros() as usize;
61                return DelimiterResult::Found {
62                    pos,
63                    byte: *ptr.add(pos),
64                };
65            }
66            offset += 16;
67        }
68    }
69
70    // Scalar tail for remaining < 16 bytes.
71    crate::scalar::find_delimiters_safe(&haystack[offset..]).offset_by(offset)
72}
73
74/// Classify each byte using SSE4.2 SIMD — 16 bytes at a time.
75///
76/// # Safety
77///
78/// Caller must ensure SSE4.2 support.
79#[target_feature(enable = "sse4.2")]
80#[cfg(target_arch = "x86_64")]
81pub unsafe fn classify_bytes(input: &[u8]) -> Vec<u8> {
82    let len = input.len();
83    let mut result = Vec::with_capacity(len);
84    let ptr = input.as_ptr();
85    let out_ptr: *mut u8 = result.as_mut_ptr();
86    let mut offset = 0;
87
88    // SAFETY: all intrinsics below require SSE4.2, guaranteed by #[target_feature].
89    // Pointer arithmetic is valid because offset < len and result has capacity >= len.
90    unsafe {
91        while offset + 16 <= len {
92            let chunk = _mm_loadu_si128(ptr.add(offset) as *const __m128i);
93
94            // Whitespace check: compare against space, tab, newline, CR.
95            let space = _mm_set1_epi8(b' ' as i8);
96            let tab = _mm_set1_epi8(b'\t' as i8);
97            let nl = _mm_set1_epi8(b'\n' as i8);
98            let cr = _mm_set1_epi8(b'\r' as i8);
99
100            // _mm_cmpeq_epi8: 0xFF where equal, 0x00 where not.
101            let ws_mask = _mm_or_si128(
102                _mm_or_si128(_mm_cmpeq_epi8(chunk, space), _mm_cmpeq_epi8(chunk, tab)),
103                _mm_or_si128(_mm_cmpeq_epi8(chunk, nl), _mm_cmpeq_epi8(chunk, cr)),
104            );
105
106            // Alpha check: (b | 0x20) - 'a' <= 25 (unsigned)
107            let or_mask = _mm_set1_epi8(0x20);
108            let lower = _mm_or_si128(chunk, or_mask); // force lowercase
109            let a_val = _mm_set1_epi8(b'a' as i8);
110            let sub = _mm_sub_epi8(lower, a_val); // lower - 'a'
111            let bound = _mm_set1_epi8(25); // 'z' - 'a'
112            // Unsigned compare: alpha if sub == min(sub, 25)
113            let clamped = _mm_min_epu8(sub, bound);
114            let alpha_mask = _mm_cmpeq_epi8(sub, clamped);
115
116            // Digit check: b - '0' <= 9 (unsigned)
117            let zero = _mm_set1_epi8(b'0' as i8);
118            let sub_d = _mm_sub_epi8(chunk, zero);
119            let dbound = _mm_set1_epi8(9);
120            let dclamped = _mm_min_epu8(sub_d, dbound);
121            let digit_mask = _mm_cmpeq_epi8(sub_d, dclamped);
122
123            // Delimiter check: compare against each of the 7 delimiters.
124            let lt = _mm_set1_epi8(b'<' as i8);
125            let gt = _mm_set1_epi8(b'>' as i8);
126            let amp = _mm_set1_epi8(b'&' as i8);
127            let quot = _mm_set1_epi8(b'"' as i8);
128            let apos = _mm_set1_epi8(b'\'' as i8);
129            let eq = _mm_set1_epi8(b'=' as i8);
130            let slash = _mm_set1_epi8(b'/' as i8);
131
132            let delim_mask = _mm_or_si128(
133                _mm_or_si128(
134                    _mm_or_si128(_mm_cmpeq_epi8(chunk, lt), _mm_cmpeq_epi8(chunk, gt)),
135                    _mm_or_si128(_mm_cmpeq_epi8(chunk, amp), _mm_cmpeq_epi8(chunk, quot)),
136                ),
137                _mm_or_si128(
138                    _mm_or_si128(_mm_cmpeq_epi8(chunk, apos), _mm_cmpeq_epi8(chunk, eq)),
139                    _mm_cmpeq_epi8(chunk, slash),
140                ),
141            );
142
143            // Map to class constants and combine.
144            let ws_class = _mm_and_si128(ws_mask, _mm_set1_epi8(crate::class::WHITESPACE as i8));
145            let al_class = _mm_and_si128(alpha_mask, _mm_set1_epi8(crate::class::ALPHA as i8));
146            let di_class = _mm_and_si128(digit_mask, _mm_set1_epi8(crate::class::DIGIT as i8));
147            let de_class = _mm_and_si128(delim_mask, _mm_set1_epi8(crate::class::DELIMITER as i8));
148
149            let combined = _mm_or_si128(
150                _mm_or_si128(ws_class, al_class),
151                _mm_or_si128(di_class, de_class),
152            );
153
154            // Store 16 bytes of classification results.
155            _mm_storeu_si128(out_ptr.add(offset) as *mut __m128i, combined);
156            offset += 16;
157        }
158
159        // Scalar tail.
160        while offset < len {
161            *out_ptr.add(offset) = classify_byte(*ptr.add(offset));
162            offset += 1;
163        }
164
165        result.set_len(len);
166    }
167
168    result
169}
170
171/// Skip leading whitespace using SSE4.2 — 16 bytes at a time.
172///
173/// # Safety
174///
175/// Caller must ensure SSE4.2 support.
176#[target_feature(enable = "sse4.2")]
177#[cfg(target_arch = "x86_64")]
178pub unsafe fn skip_whitespace(input: &[u8]) -> usize {
179    let len = input.len();
180    let ptr = input.as_ptr();
181    let mut offset = 0;
182
183    // SAFETY: all intrinsics below require SSE4.2, guaranteed by #[target_feature].
184    unsafe {
185        // Whitespace needle: space, tab, newline, CR. As in `find_delimiters`,
186        // we use explicit-length equality compares instead of `_mm_cmpistri`,
187        // whose NUL-termination semantics would treat a `\0` (a non-whitespace
188        // byte) as still inside the run and over-skip past it.
189        let space = _mm_set1_epi8(b' ' as i8);
190        let tab = _mm_set1_epi8(b'\t' as i8);
191        let nl = _mm_set1_epi8(b'\n' as i8);
192        let cr = _mm_set1_epi8(b'\r' as i8);
193
194        while offset + 16 <= len {
195            let chunk = _mm_loadu_si128(ptr.add(offset) as *const __m128i);
196
197            // 0xFF in lanes that ARE whitespace.
198            let ws = _mm_or_si128(
199                _mm_or_si128(_mm_cmpeq_epi8(chunk, space), _mm_cmpeq_epi8(chunk, tab)),
200                _mm_or_si128(_mm_cmpeq_epi8(chunk, nl), _mm_cmpeq_epi8(chunk, cr)),
201            );
202            let mask = _mm_movemask_epi8(ws) as u16;
203            // First non-whitespace byte = first zero bit in the mask.
204            if mask != 0xFFFF {
205                return offset + (!mask).trailing_zeros() as usize;
206            }
207            offset += 16;
208        }
209    }
210
211    // Scalar tail.
212    offset + crate::scalar::skip_whitespace_safe(&input[offset..])
213}
214
215/// Produce a bitmask where bit `i` is set if `block[i] == byte`.
216///
217/// Processes 16 bytes at a time using `_mm_cmpeq_epi8` + `_mm_movemask_epi8`.
218/// Handles blocks up to 64 bytes.
219///
220/// # Safety
221///
222/// Caller must ensure the CPU supports SSE4.2.
223#[target_feature(enable = "sse4.2")]
224#[cfg(target_arch = "x86_64")]
225pub unsafe fn compute_byte_mask(block: &[u8], byte: u8) -> u64 {
226    // Mask is 64-bit; only the first 64 bytes can be represented.
227    let len = block.len().min(64);
228    let ptr = block.as_ptr();
229    let mut result: u64 = 0;
230    let mut offset = 0;
231
232    // SAFETY: all intrinsics below require SSE4.2, guaranteed by #[target_feature].
233    unsafe {
234        let target = _mm_set1_epi8(byte as i8);
235
236        while offset + 16 <= len {
237            let chunk = _mm_loadu_si128(ptr.add(offset) as *const __m128i);
238            let cmp = _mm_cmpeq_epi8(chunk, target);
239            let mask = _mm_movemask_epi8(cmp) as u16;
240            result |= (mask as u64) << offset;
241            offset += 16;
242        }
243    }
244
245    // Scalar tail.
246    while offset < len {
247        // SAFETY: offset < len, so ptr.add(offset) is valid.
248        if unsafe { *ptr.add(offset) } == byte {
249            result |= 1u64 << offset;
250        }
251        offset += 1;
252    }
253
254    result
255}
256
257#[cfg(all(test, target_arch = "x86_64"))]
258mod tests {
259    use super::*;
260    use crate::class;
261
262    fn has_sse42() -> bool {
263        is_x86_feature_detected!("sse4.2")
264    }
265
266    #[test]
267    fn find_delimiters_basic() {
268        if !has_sse42() {
269            return;
270        }
271        let input = b"hello world <div>";
272        let result = unsafe { find_delimiters(input) };
273        assert_eq!(
274            result,
275            DelimiterResult::Found {
276                pos: 12,
277                byte: b'<'
278            }
279        );
280    }
281
282    #[test]
283    fn find_delimiters_not_found() {
284        if !has_sse42() {
285            return;
286        }
287        let input = b"hello world no delimiters here";
288        let result = unsafe { find_delimiters(input) };
289        assert_eq!(result, DelimiterResult::NotFound);
290    }
291
292    #[test]
293    fn classify_bytes_basic() {
294        if !has_sse42() {
295            return;
296        }
297        let input = b"a1 <b2\t>Zz09&\"'/=\nhello world...";
298        let result = unsafe { classify_bytes(input) };
299        assert_eq!(result[0], class::ALPHA);
300        assert_eq!(result[1], class::DIGIT);
301        assert_eq!(result[2], class::WHITESPACE);
302        assert_eq!(result[3], class::DELIMITER);
303    }
304
305    #[test]
306    fn skip_whitespace_basic() {
307        if !has_sse42() {
308            return;
309        }
310        let result = unsafe { skip_whitespace(b"   \t\nhello") };
311        assert_eq!(result, 5);
312    }
313
314    #[test]
315    fn skip_whitespace_all_ws() {
316        if !has_sse42() {
317            return;
318        }
319        let result = unsafe { skip_whitespace(b"                    ") };
320        assert_eq!(result, 20);
321    }
322
323    #[test]
324    fn find_delimiters_after_nul_in_simd_block() {
325        // A NUL byte before a delimiter, both within the first 16-byte SIMD
326        // block. The implicit-length string intrinsic would treat the NUL as a
327        // terminator and miss the `<`; equality compares do not.
328        if !has_sse42() {
329            return;
330        }
331        let input = b"abc\0def<ghijklmn"; // 16 bytes, '<' at index 7
332        let result = unsafe { find_delimiters(input) };
333        assert_eq!(result, DelimiterResult::Found { pos: 7, byte: b'<' });
334    }
335
336    #[test]
337    fn skip_whitespace_stops_at_nul() {
338        // A NUL is not whitespace, so it must stop the skip, not be skipped.
339        if !has_sse42() {
340            return;
341        }
342        let input = b"   \0Xhelloworld!"; // 16 bytes, first non-ws (NUL) at 3
343        let result = unsafe { skip_whitespace(input) };
344        assert_eq!(result, 3);
345    }
346}