Skip to main content

lzip_parallel/
deflate_scan.rs

1//! DEFLATE full-flush boundary scanner.
2//!
3//! A Z_FULL_FLUSH point resets the LZ77 back-reference window to empty,
4//! making all bytes that follow independently decompressible.  In the
5//! compressed byte stream this always produces the 5-byte sequence:
6//!
7//!   00          BFINAL=0, BTYPE=00 (stored), 5 padding zero-bits
8//!   00 00       LEN  = 0x0000 (zero-length stored block)
9//!   FF FF       NLEN = 0xFFFF (one's complement)
10//!
11//! We scan for the 4-byte LEN+NLEN pattern `00 00 FF FF`.  The next
12//! independently-decompressible segment starts at `match_pos + 4`.
13//!
14//! Mirrors lbzip2-rs block_scan.rs: parallel candidate-offset forward-scan,
15//! one task per physical core, no look-behind verification (Q1 resolved).
16
17use rayon::prelude::*;
18
19/// A full-flush boundary.
20///
21/// `start` is the byte offset where the next independently-decompressible
22/// segment begins (the byte immediately after the `FF FF` NLEN field).
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub struct FlushBoundary {
25    pub start: usize,
26}
27
28/// Scan forward from `from` for the next `00 00 FF FF` pattern.
29///
30/// Returns a `FlushBoundary` whose `start` is the first byte of the
31/// segment that follows the flush marker, or `None` if not found.
32pub fn find_next_flush(buf: &[u8], from: usize) -> Option<FlushBoundary> {
33    if buf.len() < 4 || from + 4 > buf.len() {
34        return None;
35    }
36
37    #[cfg(target_arch = "x86_64")]
38    {
39        if std::arch::is_x86_feature_detected!("avx512bw") {
40            return unsafe { find_next_flush_avx512(buf, from) };
41        }
42        if std::arch::is_x86_feature_detected!("avx2") {
43            return unsafe { find_next_flush_avx2(buf, from) };
44        }
45    }
46
47    find_next_flush_scalar(buf, from)
48}
49
50/// Scalar fallback for flush scan.
51fn find_next_flush_scalar(buf: &[u8], from: usize) -> Option<FlushBoundary> {
52    if buf.len() < 4 {
53        return None;
54    }
55    let end = buf.len() - 3;
56    let mut i = from;
57    while i < end {
58        // Fast skip: the pattern requires buf[i+2]==0xFF.
59        if buf[i + 2] != 0xFF {
60            i += 1;
61            continue;
62        }
63        if buf[i] == 0x00 && buf[i + 1] == 0x00 && buf[i + 3] == 0xFF {
64            return Some(FlushBoundary { start: i + 4 });
65        }
66        i += 1;
67    }
68    None
69}
70
71/// AVX-512BW flush scan: searches 64 bytes at a time.
72///
73/// Same algorithm as AVX2 but with 64-byte vectors (zmm registers).
74/// On Zen 4+ and Intel Skylake-X+, processes 2× the data per iteration.
75///
76/// # Safety
77/// Requires AVX-512BW. Caller ensures `buf.len() >= 4`.
78#[cfg(target_arch = "x86_64")]
79#[target_feature(enable = "avx512bw")]
80unsafe fn find_next_flush_avx512(buf: &[u8], from: usize) -> Option<FlushBoundary> {
81    use core::arch::x86_64::*;
82
83    unsafe {
84        let zero_vec = _mm512_setzero_si512();
85        let ff_vec = _mm512_set1_epi8(-1i8);
86
87        let ptr = buf.as_ptr();
88        let len = buf.len();
89        let mut i = from;
90
91        // Process 64 bytes at a time; need 3 extra for pattern overlap
92        while i + 67 <= len {
93            let chunk = _mm512_loadu_si512(ptr.add(i) as *const __m512i);
94
95            // Compare each byte to 0x00 and 0xFF — returns 64-bit masks
96            let zero_mask = _mm512_cmpeq_epi8_mask(chunk, zero_vec);
97            let ff_mask = _mm512_cmpeq_epi8_mask(chunk, ff_vec);
98
99            // Pattern 00 00 FF FF: shifted AND of masks
100            let candidates = (zero_mask & (zero_mask >> 1)) & ((ff_mask >> 2) & (ff_mask >> 3));
101
102            if candidates != 0 {
103                let bit_pos = candidates.trailing_zeros() as usize;
104                let pos = i + bit_pos;
105                if pos + 4 <= len
106                    && buf[pos] == 0x00
107                    && buf[pos + 1] == 0x00
108                    && buf[pos + 2] == 0xFF
109                    && buf[pos + 3] == 0xFF
110                {
111                    return Some(FlushBoundary { start: pos + 4 });
112                }
113                i += bit_pos + 1;
114                continue;
115            }
116
117            i += 64;
118        }
119
120        // Fall back to AVX2/scalar for the tail
121        find_next_flush_scalar(buf, i)
122    }
123}
124
125/// AVX2 flush scan: searches 32 bytes at a time using SIMD comparison.
126///
127/// Strategy: compare all bytes against 0x00, then against 0xFF.
128/// A valid pattern `00 00 FF FF` means: zero[i] && zero[i+1] && ff[i+2] && ff[i+3].
129/// We use shifted AND of the bitmasks to find candidates.
130#[cfg(target_arch = "x86_64")]
131#[target_feature(enable = "avx2")]
132unsafe fn find_next_flush_avx2(buf: &[u8], from: usize) -> Option<FlushBoundary> {
133    use core::arch::x86_64::*;
134
135    unsafe {
136        let zero_vec = _mm256_setzero_si256();
137        let ff_vec = _mm256_set1_epi8(-1); // 0xFF as i8
138
139        let ptr = buf.as_ptr();
140        let len = buf.len();
141
142        let mut i = from;
143
144        // Process 32 bytes at a time
145        // We need to check positions i..i+35 (32 bytes + 3 for pattern overlap)
146        while i + 35 <= len {
147            let chunk = _mm256_loadu_si256(ptr.add(i) as *const __m256i);
148
149            // Compare each byte to 0x00 and 0xFF
150            let eq_zero = _mm256_cmpeq_epi8(chunk, zero_vec);
151            let eq_ff = _mm256_cmpeq_epi8(chunk, ff_vec);
152
153            let zero_mask = _mm256_movemask_epi8(eq_zero) as u32;
154            let ff_mask = _mm256_movemask_epi8(eq_ff) as u32;
155
156            // Pattern 00 00 FF FF at position j means:
157            //   zero_mask bit j set, zero_mask bit j+1 set,
158            //   ff_mask bit j+2 set, ff_mask bit j+3 set.
159            let candidates = (zero_mask & (zero_mask >> 1)) & ((ff_mask >> 2) & (ff_mask >> 3));
160
161            if candidates != 0 {
162                let bit_pos = candidates.trailing_zeros() as usize;
163                let pos = i + bit_pos;
164                if pos + 4 <= len
165                    && buf[pos] == 0x00
166                    && buf[pos + 1] == 0x00
167                    && buf[pos + 2] == 0xFF
168                    && buf[pos + 3] == 0xFF
169                {
170                    return Some(FlushBoundary { start: pos + 4 });
171                }
172                i += bit_pos + 1;
173                continue;
174            }
175
176            i += 32;
177        }
178
179        // Scalar tail for remaining bytes
180        find_next_flush_scalar(buf, i)
181    }
182}
183
184/// Find all full-flush boundaries in `buf`.
185pub fn find_all_flushes(buf: &[u8]) -> Vec<FlushBoundary> {
186    let mut result = Vec::new();
187    let mut pos = 0;
188    while let Some(b) = find_next_flush(buf, pos) {
189        result.push(b);
190        pos = b.start;
191    }
192    result
193}
194
195/// Parallel split-point search: N−1 rayon tasks each find the nearest flush
196/// boundary at or after their nominal byte offset.
197///
198/// `n_splits` is typically the number of physical cores.
199/// Returns up to `n_splits − 1` boundaries, sorted and deduplicated.
200pub fn split_boundaries_parallel(buf: &[u8], n_splits: usize) -> Vec<FlushBoundary> {
201    if n_splits <= 1 || buf.len() < 8 {
202        return Vec::new();
203    }
204
205    let mut found: Vec<Option<FlushBoundary>> = crate::thread_pool().install(|| {
206        (1..n_splits)
207            .into_par_iter()
208            .map(|i| {
209                let nominal = buf.len() * i / n_splits;
210                find_next_flush(buf, nominal)
211            })
212            .collect()
213    });
214
215    let mut result: Vec<FlushBoundary> = found.drain(..).flatten().collect();
216    result.sort_by_key(|b| b.start);
217    result.dedup_by_key(|b| b.start);
218    result
219}
220
221#[cfg(test)]
222mod tests {
223    use super::*;
224
225    fn make_flush_marker() -> Vec<u8> {
226        vec![0x00, 0x00, 0x00, 0xFF, 0xFF]
227    }
228
229    #[test]
230    fn find_flush_at_start() {
231        let mut buf = make_flush_marker();
232        buf.extend_from_slice(&[0xAA, 0xBB]);
233        // Pattern 00 00 FF FF starts at offset 1 (the LEN bytes)
234        let b = find_next_flush(&buf, 0).unwrap();
235        assert_eq!(b.start, 5); // after the 5-byte sequence
236    }
237
238    #[test]
239    fn find_flush_mid_buffer() {
240        let mut buf = vec![0xDE, 0xAD, 0xBE, 0xEF];
241        buf.extend_from_slice(&make_flush_marker());
242        buf.extend_from_slice(&[0x01, 0x02]);
243        let b = find_next_flush(&buf, 0).unwrap();
244        assert_eq!(b.start, 9);
245    }
246
247    #[test]
248    fn no_flush_in_random_data() {
249        // High-byte data is unlikely to match
250        let buf: Vec<u8> = (0..64).map(|i| (i * 37 + 1) as u8).collect();
251        // Just check it doesn't panic
252        let _ = find_next_flush(&buf, 0);
253    }
254
255    #[test]
256    fn find_all_flushes_two_markers() {
257        let mut buf = Vec::new();
258        buf.extend_from_slice(&make_flush_marker()); // boundary at 5
259        buf.extend_from_slice(&[0xAA; 10]);
260        buf.extend_from_slice(&make_flush_marker()); // boundary at 20
261        let all = find_all_flushes(&buf);
262        assert_eq!(all.len(), 2);
263        assert_eq!(all[0].start, 5);
264        assert_eq!(all[1].start, 20);
265    }
266}