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