Skip to main content

crc32fast/specialized/
pclmulqdq.rs

1//! Specialized checksum code for the x86 CPU architecture, based on the efficient algorithm described
2//! in the following whitepaper:
3//!
4//! Gopal, V., Ozturk, E., Guilford, J., Wolrich, G., Feghali, W., Dixon, M., & Karakoyunlu, D. (2009).
5//! _Fast CRC computation for generic polynomials using PCLMULQDQ instruction_. Intel.
6//! (Mirror link: <https://fossies.org/linux/zlib-ng/doc/crc-pclmulqdq.pdf>, accessed 2024-05-20)
7//!
8//! Throughout the code, this work is referred to as "the paper".
9//!
10//! On top of the 128-bit `PCLMULQDQ` implementation, two wider variants use `VPCLMULQDQ` to fold
11//! several independent 128-bit streams per instruction: an `AVX2` variant over 256-bit `YMM`
12//! registers (8 streams) and an `AVX-512` variant over 512-bit `ZMM` registers (16 streams). Both
13//! rely on `VPCLMULQDQ` intrinsics stabilized in Rust 1.89 and are only compiled when the
14//! `stable_vpclmulqdq` cfg is set by `build.rs`, leaving the crate MSRV unchanged otherwise. The
15//! best variant supported by the running CPU is chosen at runtime.
16//!
17//! Each `unsafe fn` here is unsafe only because it needs the CPU features in its
18//! `#[target_feature]` list (PCLMULQDQ for `reduce128`). None of them has a memory-safety
19//! precondition: every load is bounds-checked.
20
21#[cfg(target_arch = "x86")]
22use core::arch::x86 as arch;
23#[cfg(target_arch = "x86_64")]
24use core::arch::x86_64 as arch;
25
26/// Which SIMD implementation to use, chosen once at construction from the CPU's features.
27#[derive(Clone, Copy)]
28enum Kind {
29    /// 128-bit `PCLMULQDQ`, fold by 4.
30    Sse,
31    /// 256-bit `VPCLMULQDQ`, 8 streams.
32    #[cfg(stable_vpclmulqdq)]
33    Avx2,
34    /// 512-bit `VPCLMULQDQ`, 16 streams.
35    #[cfg(stable_vpclmulqdq)]
36    Avx512,
37}
38
39#[derive(Clone)]
40pub struct State {
41    kind: Kind,
42}
43
44impl State {
45    #[cfg(not(feature = "std"))]
46    #[inline]
47    fn detect() -> Option<Kind> {
48        if cfg!(target_feature = "pclmulqdq")
49            && cfg!(target_feature = "sse2")
50            && cfg!(target_feature = "sse4.1")
51            && cfg!(target_feature = "ssse3")
52        {
53            #[cfg(stable_vpclmulqdq)]
54            {
55                if cfg!(target_feature = "avx512f") && cfg!(target_feature = "vpclmulqdq") {
56                    return Some(Kind::Avx512);
57                }
58                if cfg!(target_feature = "avx2") && cfg!(target_feature = "vpclmulqdq") {
59                    return Some(Kind::Avx2);
60                }
61            }
62
63            return Some(Kind::Sse);
64        }
65
66        None
67    }
68
69    #[cfg(feature = "std")]
70    #[inline]
71    fn detect() -> Option<Kind> {
72        if is_x86_feature_detected!("pclmulqdq")
73            && is_x86_feature_detected!("sse2")
74            && is_x86_feature_detected!("sse4.1")
75            && is_x86_feature_detected!("ssse3")
76        {
77            #[cfg(stable_vpclmulqdq)]
78            {
79                if is_x86_feature_detected!("avx512f") && is_x86_feature_detected!("vpclmulqdq") {
80                    return Some(Kind::Avx512);
81                }
82                if is_x86_feature_detected!("avx2") && is_x86_feature_detected!("vpclmulqdq") {
83                    return Some(Kind::Avx2);
84                }
85            }
86
87            return Some(Kind::Sse);
88        }
89
90        None
91    }
92
93    #[inline]
94    pub fn new() -> Option<Self> {
95        // SAFETY: `detect` only returns a `Kind` whose instructions the CPU supports.
96        Self::detect().map(|kind| Self { kind })
97    }
98
99    #[inline]
100    pub fn update(&self, state: u32, buf: &[u8]) -> u32 {
101        // SAFETY: The kernels are only unsafe because the CPU might lack their target features.
102        // `detect` checked every feature listed on the kernel for `self.kind`, and `kind` can't be
103        // changed after `new`.
104        unsafe {
105            match self.kind {
106                Kind::Sse => calculate(state, buf),
107                #[cfg(stable_vpclmulqdq)]
108                Kind::Avx2 => calculate_avx2(state, buf),
109                #[cfg(stable_vpclmulqdq)]
110                Kind::Avx512 => calculate_avx512(state, buf),
111            }
112        }
113    }
114}
115
116const K1: i64 = 0x154442bd4;
117const K2: i64 = 0x1c6e41596;
118const K3: i64 = 0x1751997d0;
119const K4: i64 = 0x0ccaa009e;
120const K5: i64 = 0x163cd6124;
121
122const P_X: i64 = 0x1DB710641;
123const U_PRIME: i64 = 0x1F7011641;
124
125// The wider kernels have progressively more streams to initialize and collapse. Keep their
126// crossovers conservative so short inputs do not pay that fixed cost.
127const MIN_FOLD_BY_4_BYTES: usize = 128;
128#[cfg(stable_vpclmulqdq)]
129const MIN_AVX512_BYTES: usize = 2 * 1024;
130
131// Fold constants for the wider strides. For a fold by `D` bits the pair is
132// `(reflect(x^(D+32) mod P), reflect(x^(D-32) mod P))`, as for `K1`/`K2` (D = 512) and
133// `K3`/`K4` (D = 128). AVX2 uses 8 streams (D = 1024), AVX-512 uses 16 streams (D = 2048).
134#[cfg(stable_vpclmulqdq)]
135const K_1024_LOW: i64 = 0x1e88ef372;
136#[cfg(stable_vpclmulqdq)]
137const K_1024_HIGH: i64 = 0x14a7fe880;
138#[cfg(stable_vpclmulqdq)]
139const K_2048_LOW: i64 = 0x11542778a;
140#[cfg(stable_vpclmulqdq)]
141const K_2048_HIGH: i64 = 0x1322d1430;
142
143#[target_feature(
144    enable = "pclmulqdq",
145    enable = "sse2",
146    enable = "sse4.1",
147    enable = "ssse3"
148)]
149unsafe fn calculate(crc: u32, mut data: &[u8]) -> u32 {
150    // Below 16 bytes there isn't even a single full block to load, so use the scalar fallback.
151    if data.len() < 16 {
152        return crate::baseline::update_short(crc, data);
153    }
154
155    // For 16..127 bytes a single-accumulator fold-by-1 is enough; the fold-by-4 setup below only
156    // pays off once there are several 64-byte groups.
157    if data.len() < MIN_FOLD_BY_4_BYTES {
158        let mut x = get(&mut data);
159        x = arch::_mm_xor_si128(x, arch::_mm_cvtsi32_si128(!crc as i32));
160        return reduce_128_to_crc(x, data);
161    }
162
163    // Step 1: fold by 4 loop
164    let mut x3 = get(&mut data);
165    let mut x2 = get(&mut data);
166    let mut x1 = get(&mut data);
167    let mut x0 = get(&mut data);
168
169    // fold in our initial value, part of the incremental crc checksum
170    x3 = arch::_mm_xor_si128(x3, arch::_mm_cvtsi32_si128(!crc as i32));
171
172    let k1k2 = arch::_mm_set_epi64x(K2, K1);
173    while data.len() >= 64 {
174        x3 = reduce128(x3, get(&mut data), k1k2);
175        x2 = reduce128(x2, get(&mut data), k1k2);
176        x1 = reduce128(x1, get(&mut data), k1k2);
177        x0 = reduce128(x0, get(&mut data), k1k2);
178    }
179
180    let k3k4 = arch::_mm_set_epi64x(K4, K3);
181    let mut x = reduce128(x3, x2, k3k4);
182    x = reduce128(x, x1, k3k4);
183    x = reduce128(x, x0, k3k4);
184
185    reduce_128_to_crc(x, data)
186}
187
188/// 256-bit `VPCLMULQDQ` variant: 8 streams across four `YMM` registers (two lanes each), folding
189/// two streams per carry-less multiply.
190#[cfg(stable_vpclmulqdq)]
191#[allow(clippy::incompatible_msrv)] // intrinsics are gated to rustc >= 1.89 by build.rs
192#[target_feature(
193    enable = "pclmulqdq",
194    enable = "sse2",
195    enable = "sse4.1",
196    enable = "ssse3",
197    enable = "avx",
198    enable = "avx2",
199    enable = "vpclmulqdq"
200)]
201unsafe fn calculate_avx2(crc: u32, mut data: &[u8]) -> u32 {
202    // Too small for the wide loop; use the 128-bit path.
203    if data.len() < 256 {
204        return calculate(crc, data);
205    }
206
207    // First 128 bytes as 8 streams (four YMM registers, two lanes each).
208    let mut v0 = get256(&mut data);
209    let mut v1 = get256(&mut data);
210    let mut v2 = get256(&mut data);
211    let mut v3 = get256(&mut data);
212
213    // Fold the initial CRC into the lowest-offset stream.
214    v0 = arch::_mm256_xor_si256(
215        v0,
216        arch::_mm256_castsi128_si256(arch::_mm_cvtsi32_si128(!crc as i32)),
217    );
218
219    let k = arch::_mm256_set_epi64x(K_1024_HIGH, K_1024_LOW, K_1024_HIGH, K_1024_LOW);
220    while data.len() >= 128 {
221        v0 = reduce256(v0, get256(&mut data), k);
222        v1 = reduce256(v1, get256(&mut data), k);
223        v2 = reduce256(v2, get256(&mut data), k);
224        v3 = reduce256(v3, get256(&mut data), k);
225    }
226
227    // Collapse the 8 streams, in increasing byte offset, folding each into the next by 128 bits.
228    let k3k4 = arch::_mm_set_epi64x(K4, K3);
229    let mut x = arch::_mm256_castsi256_si128(v0);
230    x = reduce128(x, arch::_mm256_extracti128_si256(v0, 1), k3k4);
231    x = reduce128(x, arch::_mm256_castsi256_si128(v1), k3k4);
232    x = reduce128(x, arch::_mm256_extracti128_si256(v1, 1), k3k4);
233    x = reduce128(x, arch::_mm256_castsi256_si128(v2), k3k4);
234    x = reduce128(x, arch::_mm256_extracti128_si256(v2, 1), k3k4);
235    x = reduce128(x, arch::_mm256_castsi256_si128(v3), k3k4);
236    x = reduce128(x, arch::_mm256_extracti128_si256(v3, 1), k3k4);
237
238    reduce_128_to_crc(x, data)
239}
240
241/// 512-bit `VPCLMULQDQ` variant: 16 streams across four `ZMM` registers (four lanes each), folding
242/// four streams per carry-less multiply.
243#[cfg(stable_vpclmulqdq)]
244#[allow(clippy::incompatible_msrv)] // intrinsics are gated to rustc >= 1.89 by build.rs
245#[target_feature(
246    enable = "pclmulqdq",
247    enable = "sse2",
248    enable = "sse4.1",
249    enable = "ssse3",
250    enable = "avx",
251    enable = "avx2",
252    enable = "vpclmulqdq",
253    enable = "avx512f"
254)]
255unsafe fn calculate_avx512(crc: u32, mut data: &[u8]) -> u32 {
256    // Use a conservative crossover because the 16-stream setup and collapse are costly near 1 KiB.
257    if data.len() < MIN_AVX512_BYTES {
258        return calculate_avx2(crc, data);
259    }
260
261    // First 256 bytes as 16 streams (four ZMM registers, four lanes each).
262    let mut v0 = get512(&mut data);
263    let mut v1 = get512(&mut data);
264    let mut v2 = get512(&mut data);
265    let mut v3 = get512(&mut data);
266
267    // Fold the initial CRC into the lowest-offset stream.
268    v0 = arch::_mm512_xor_si512(
269        v0,
270        arch::_mm512_castsi128_si512(arch::_mm_cvtsi32_si128(!crc as i32)),
271    );
272
273    let k = arch::_mm512_set_epi64(
274        K_2048_HIGH,
275        K_2048_LOW,
276        K_2048_HIGH,
277        K_2048_LOW,
278        K_2048_HIGH,
279        K_2048_LOW,
280        K_2048_HIGH,
281        K_2048_LOW,
282    );
283    while data.len() >= 256 {
284        v0 = reduce512(v0, get512(&mut data), k);
285        v1 = reduce512(v1, get512(&mut data), k);
286        v2 = reduce512(v2, get512(&mut data), k);
287        v3 = reduce512(v3, get512(&mut data), k);
288    }
289
290    // Collapse the 16 streams, in increasing byte offset, folding each into the next by 128 bits.
291    let k3k4 = arch::_mm_set_epi64x(K4, K3);
292    let mut x = arch::_mm512_castsi512_si128(v0);
293    x = reduce128(x, arch::_mm512_extracti32x4_epi32(v0, 1), k3k4);
294    x = reduce128(x, arch::_mm512_extracti32x4_epi32(v0, 2), k3k4);
295    x = reduce128(x, arch::_mm512_extracti32x4_epi32(v0, 3), k3k4);
296    x = reduce128(x, arch::_mm512_castsi512_si128(v1), k3k4);
297    x = reduce128(x, arch::_mm512_extracti32x4_epi32(v1, 1), k3k4);
298    x = reduce128(x, arch::_mm512_extracti32x4_epi32(v1, 2), k3k4);
299    x = reduce128(x, arch::_mm512_extracti32x4_epi32(v1, 3), k3k4);
300    x = reduce128(x, arch::_mm512_castsi512_si128(v2), k3k4);
301    x = reduce128(x, arch::_mm512_extracti32x4_epi32(v2, 1), k3k4);
302    x = reduce128(x, arch::_mm512_extracti32x4_epi32(v2, 2), k3k4);
303    x = reduce128(x, arch::_mm512_extracti32x4_epi32(v2, 3), k3k4);
304    x = reduce128(x, arch::_mm512_castsi512_si128(v3), k3k4);
305    x = reduce128(x, arch::_mm512_extracti32x4_epi32(v3, 1), k3k4);
306    x = reduce128(x, arch::_mm512_extracti32x4_epi32(v3, 2), k3k4);
307    x = reduce128(x, arch::_mm512_extracti32x4_epi32(v3, 3), k3k4);
308
309    // A substantial remainder is faster through SSE's complete fold-by-4 kernel than through the
310    // serial fold-by-1 tail below. Finalizing and restarting is valid because CRC updates compose.
311    if data.len() >= MIN_FOLD_BY_4_BYTES {
312        let crc = reduce_128_to_crc(x, &[]);
313        return calculate(crc, data);
314    }
315
316    reduce_128_to_crc(x, data)
317}
318
319/// Folds any remaining 16-byte chunks into `x`, then folds a final partial (`< 16` byte) block
320/// with a byte-shift, reduces from 128 to 32 bits with a Barrett reduction, and returns the CRC.
321/// Shared by all of the fold implementations.
322#[target_feature(
323    enable = "pclmulqdq",
324    enable = "sse2",
325    enable = "sse4.1",
326    enable = "ssse3"
327)]
328unsafe fn reduce_128_to_crc(mut x: arch::__m128i, mut data: &[u8]) -> u32 {
329    let k3k4 = arch::_mm_set_epi64x(K4, K3);
330
331    // Fold by 1 over any remaining whole 16-byte blocks.
332    while data.len() >= 16 {
333        x = reduce128(x, get(&mut data), k3k4);
334    }
335
336    // Fold a final partial block of `n` (1..=15) bytes. The last `n` bytes of the accumulator are
337    // shifted out (`overflow`) and folded back by 128 bits, while `x` is shifted down to make room
338    // for the `n` new bytes, which are byte-aligned into the vacated high lanes. The shuffle masks
339    // are built at runtime from the byte length, avoiding a lookup table.
340    let n = data.len();
341    if n > 0 {
342        let seq = arch::_mm_setr_epi8(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15);
343        let shl = arch::_mm_add_epi8(seq, arch::_mm_set1_epi8(n as i8 - 16));
344        let shr = arch::_mm_xor_si128(shl, arch::_mm_set1_epi8(-128));
345
346        let overflow = arch::_mm_shuffle_epi8(x, shl);
347        x = arch::_mm_shuffle_epi8(x, shr);
348
349        let (lo, hi) = partial_words(data);
350        let part = arch::_mm_set_epi64x(hi as i64, lo as i64);
351        x = arch::_mm_xor_si128(x, arch::_mm_shuffle_epi8(part, shl));
352
353        x = reduce128(overflow, x, k3k4);
354    }
355
356    // Perform step 3, reduction from 128 bits to 64 bits. This is
357    // significantly different from the paper and basically doesn't follow it
358    // at all. It's not really clear why, but implementations of this algorithm
359    // in Chrome/Linux diverge in the same way. It is beyond me why this is
360    // different than the paper, maybe the paper has like errata or something?
361    // Unclear.
362    //
363    // It's also not clear to me what's actually happening here and/or why, but
364    // algebraically what's happening is:
365    //
366    // x = (x[0:63] • K4) ^ x[64:127]           // 96 bit result
367    // x = ((x[0:31] as u64) • K5) ^ x[32:95]   // 64 bit result
368    //
369    // It's... not clear to me what's going on here. The paper itself is pretty
370    // vague on this part but definitely uses different constants at least.
371    // It's not clear to me, reading the paper, where the xor operations are
372    // happening or why things are shifting around. This implementation...
373    // appears to work though!
374    let x = arch::_mm_xor_si128(
375        arch::_mm_clmulepi64_si128(x, k3k4, 0x10),
376        arch::_mm_srli_si128(x, 8),
377    );
378    let x = arch::_mm_xor_si128(
379        arch::_mm_clmulepi64_si128(
380            arch::_mm_and_si128(x, arch::_mm_set_epi32(0, 0, 0, !0)),
381            arch::_mm_set_epi64x(0, K5),
382            0x00,
383        ),
384        arch::_mm_srli_si128(x, 4),
385    );
386
387    // Perform a Barrett reduction from our now 64 bits to 32 bits. The
388    // algorithm for this is described at the end of the paper, and note that
389    // this also implements the "bit reflected input" variant.
390    let pu = arch::_mm_set_epi64x(U_PRIME, P_X);
391
392    // T1(x) = ⌊(R(x) % x^32)⌋ • μ
393    let t1 = arch::_mm_clmulepi64_si128(
394        arch::_mm_and_si128(x, arch::_mm_set_epi32(0, 0, 0, !0)),
395        pu,
396        0x10,
397    );
398    // T2(x) = ⌊(T1(x) % x^32)⌋ • P(x)
399    let t2 = arch::_mm_clmulepi64_si128(
400        arch::_mm_and_si128(t1, arch::_mm_set_epi32(0, 0, 0, !0)),
401        pu,
402        0x00,
403    );
404    // We're doing the bit-reflected variant, so get the upper 32-bits of the
405    // 64-bit result instead of the lower 32-bits.
406    //
407    // C(x) = R(x) ^ T2(x) / x^32
408    !(arch::_mm_extract_epi32(arch::_mm_xor_si128(x, t2), 1) as u32)
409}
410
411#[inline]
412unsafe fn reduce128(a: arch::__m128i, b: arch::__m128i, keys: arch::__m128i) -> arch::__m128i {
413    let t1 = arch::_mm_clmulepi64_si128(a, keys, 0x00);
414    let t2 = arch::_mm_clmulepi64_si128(a, keys, 0x11);
415    arch::_mm_xor_si128(arch::_mm_xor_si128(b, t1), t2)
416}
417
418#[cfg(stable_vpclmulqdq)]
419#[allow(clippy::incompatible_msrv)] // intrinsics are gated to rustc >= 1.89 by build.rs
420#[target_feature(enable = "avx2", enable = "vpclmulqdq")]
421#[inline]
422unsafe fn reduce256(a: arch::__m256i, b: arch::__m256i, keys: arch::__m256i) -> arch::__m256i {
423    let t1 = arch::_mm256_clmulepi64_epi128(a, keys, 0x00);
424    let t2 = arch::_mm256_clmulepi64_epi128(a, keys, 0x11);
425    arch::_mm256_xor_si256(arch::_mm256_xor_si256(b, t1), t2)
426}
427
428#[cfg(stable_vpclmulqdq)]
429#[allow(clippy::incompatible_msrv)] // intrinsics are gated to rustc >= 1.89 by build.rs
430#[target_feature(enable = "avx512f", enable = "vpclmulqdq")]
431#[inline]
432unsafe fn reduce512(a: arch::__m512i, b: arch::__m512i, keys: arch::__m512i) -> arch::__m512i {
433    let t1 = arch::_mm512_clmulepi64_epi128(a, keys, 0x00);
434    let t2 = arch::_mm512_clmulepi64_epi128(a, keys, 0x11);
435    arch::_mm512_xor_si512(arch::_mm512_xor_si512(b, t1), t2)
436}
437
438// The loads below index `a[..N]` before reading through its pointer, so a miscounted length panics
439// instead of reading out of bounds. The callers' loop conditions already imply the check, so it
440// compiles away: the kernels' machine code is unchanged.
441
442/// Loads the next 16 bytes of `a` as a vector and advances `a` past them.
443#[inline]
444fn get(a: &mut &[u8]) -> arch::__m128i {
445    // SAFETY: `a[..16]` is 16 readable bytes, `_mm_loadu_si128` doesn't need them aligned, and SSE2
446    // is enabled for the whole build (`specialized/mod.rs` only compiles this module when it is).
447    let r = unsafe { arch::_mm_loadu_si128(a[..16].as_ptr() as *const arch::__m128i) };
448    *a = &a[16..];
449    r
450}
451
452#[cfg(stable_vpclmulqdq)]
453#[target_feature(enable = "avx")]
454#[inline]
455unsafe fn get256(a: &mut &[u8]) -> arch::__m256i {
456    let r = arch::_mm256_loadu_si256(a[..32].as_ptr() as *const arch::__m256i);
457    *a = &a[32..];
458    r
459}
460
461#[cfg(stable_vpclmulqdq)]
462#[allow(clippy::incompatible_msrv)] // intrinsics are gated to rustc >= 1.89 by build.rs
463#[target_feature(enable = "avx512f")]
464#[inline]
465unsafe fn get512(a: &mut &[u8]) -> arch::__m512i {
466    let r = arch::_mm512_loadu_si512(a[..64].as_ptr() as *const _);
467    *a = &a[64..];
468    r
469}
470
471/// Packs a final partial block of 1..=15 bytes into two zero-padded little-endian words, using
472/// overlapping loads. Building it in registers avoids copying through a stack buffer, whose narrow
473/// stores the following 16-byte load cannot forward from.
474#[inline]
475fn partial_words(data: &[u8]) -> (u64, u64) {
476    let n = data.len();
477    debug_assert!(n > 0 && n < 16);
478    let v: u128 = if n >= 8 {
479        let lo = u64::from_le_bytes(data[..8].try_into().unwrap());
480        let hi = u64::from_le_bytes(data[n - 8..].try_into().unwrap());
481        lo as u128 | (hi as u128) << (8 * (n - 8))
482    } else if n >= 4 {
483        let lo = u32::from_le_bytes(data[..4].try_into().unwrap());
484        let hi = u32::from_le_bytes(data[n - 4..].try_into().unwrap());
485        lo as u128 | (hi as u128) << (8 * (n - 4))
486    } else {
487        data[0] as u128
488            | (data[n / 2] as u128) << (8 * (n / 2))
489            | (data[n - 1] as u128) << (8 * (n - 1))
490    };
491    (v as u64, (v >> 64) as u64)
492}
493
494#[cfg(test)]
495mod test {
496    quickcheck::quickcheck! {
497        fn check_against_baseline(init: u32, chunks: Vec<(Vec<u8>, usize)>) -> bool {
498            let mut baseline = init;
499            let mut pclmulqdq = init;
500            let spec = super::State::new().expect("not supported");
501
502            for (chunk, mut offset) in chunks {
503                // simulate random alignments by offsetting the slice by up to 15 bytes
504                offset &= 0xF;
505                if chunk.len() <= offset {
506                    baseline = crate::baseline::update_fast_16(baseline, &chunk);
507                    pclmulqdq = spec.update(pclmulqdq, &chunk);
508                } else {
509                    baseline = crate::baseline::update_fast_16(baseline, &chunk[offset..]);
510                    pclmulqdq = spec.update(pclmulqdq, &chunk[offset..]);
511                }
512            }
513            pclmulqdq == baseline
514        }
515    }
516
517    // Exercises the wide fold paths across the sizes and alignments where the strategy switches.
518    #[test]
519    fn check_large_inputs_against_baseline() {
520        let mut data = vec![0u8; 8200];
521        let mut s: u32 = 0x1234_5678;
522        for b in data.iter_mut() {
523            s = s.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
524            *b = (s >> 24) as u8;
525        }
526
527        for &len in &[
528            0usize, 1, 15, 16, 17, 63, 64, 127, 128, 129, 255, 256, 257, 511, 512, 513, 1023, 1024,
529            1025, 2047, 2048, 2049, 2175, 2176, 2303, 2304, 4096, 8192, 8199,
530        ] {
531            for &offset in &[0usize, 1, 3, 7, 8, 15] {
532                if offset + len > data.len() {
533                    continue;
534                }
535                let slice = &data[offset..offset + len];
536                let baseline = crate::baseline::update_fast_16(0, slice);
537                let spec = super::State::new().expect("not supported");
538                let specialized = spec.update(0, slice);
539
540                assert_eq!(
541                    specialized, baseline,
542                    "mismatch for len={len} offset={offset}",
543                );
544            }
545        }
546    }
547}