Skip to content

Commit 7ad18e0

Browse files
committed
Add pmull-based aarch64 SIMD version of crc32
Based on the apple zlib assembly implementation
1 parent b8dfae5 commit 7ad18e0

4 files changed

Lines changed: 244 additions & 3 deletions

File tree

zlib-rs/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ __internal-api = [] # export internal symbols for use in `libz-rs-sys`.
2727
ZLIB_DEBUG = []
2828
vpclmulqdq = [] # use avx512 to speed up crc32. Only stable from 1.89.0 onwards.
2929
avx512 = ["vpclmulqdq"] # use avx512 to speed up crc32 and adler32. Only stable from 1.89.0 onwards.
30+
pmull = [] # use pmull-based crc32 on aarch64 when the aes target feature is available.
3031

3132

3233
[dependencies]

zlib-rs/src/cpu_features.rs

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,14 +95,35 @@ pub fn is_enabled_neon() -> bool {
9595
#[inline(always)]
9696
pub fn is_enabled_crc() -> bool {
9797
#[cfg(target_arch = "aarch64")]
98-
#[cfg(feature = "std")]
99-
return std::arch::is_aarch64_feature_detected!("crc");
98+
{
99+
#[cfg(target_feature = "crc")]
100+
return true;
101+
102+
#[cfg(feature = "std")]
103+
return std::arch::is_aarch64_feature_detected!("crc");
104+
}
105+
106+
false
107+
}
108+
109+
#[inline(always)]
110+
pub fn is_enabled_aes() -> bool {
111+
#[cfg(target_arch = "aarch64")]
112+
{
113+
#[cfg(target_feature = "aes")]
114+
return true;
115+
116+
#[cfg(feature = "std")]
117+
return std::arch::is_aarch64_feature_detected!("aes");
118+
}
100119

101120
false
102121
}
103122

104123
#[inline(always)]
105124
pub fn is_enabled_simd128() -> bool {
125+
// There is no runtime detection on wasm.
126+
106127
#[cfg(target_arch = "wasm32")]
107128
return cfg!(target_feature = "simd128");
108129

zlib-rs/src/crc32.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ mod combine;
1010
mod loongarch;
1111
#[cfg(target_arch = "x86_64")]
1212
mod pclmulqdq;
13+
#[cfg(target_arch = "aarch64")]
14+
mod pmull;
1315
#[cfg(target_arch = "x86_64")]
1416
#[cfg(feature = "vpclmulqdq")]
1517
mod vpclmulqdq;
@@ -79,7 +81,13 @@ impl Crc32Fold {
7981
}
8082
}
8183
target_arch = "aarch64" => {
82-
if crate::cpu_features::is_enabled_crc() {
84+
if cfg!(arget_endian = "little")
85+
&& cfg!(feature = "pmull")
86+
&& crate::cpu_features::is_enabled_aes()
87+
{
88+
self.value = unsafe { self::pmull::crc32_pmull_aarch64(self.value, src) };
89+
return;
90+
} else if crate::cpu_features::is_enabled_crc() {
8391
self.value = unsafe { self::acle::crc32_acle_aarch64(self.value, src) };
8492
return;
8593
}

zlib-rs/src/crc32/pmull.rs

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
//! Folding polynomial constants for CRC32 (bit-reflected polynomial 0xEDB88320).
2+
//! Derived from the Apple zlib implementation in `crc32lt_arm64.s`.
3+
4+
use std::arch::aarch64::{
5+
poly64x2_t, uint8x16_t, vcombine_p64, vcreate_p64, vdupq_n_u32, vdupq_n_u64, veorq_u8,
6+
vgetq_lane_p64, vgetq_lane_u32, vgetq_lane_u64, vmull_high_p64, vmull_p64,
7+
vreinterpretq_p64_u8, vreinterpretq_u32_u8, vreinterpretq_u64_u8, vreinterpretq_u8_p128,
8+
vreinterpretq_u8_u32, vreinterpretq_u8_u64, vsetq_lane_u32, vsetq_lane_u64,
9+
};
10+
11+
/// pmull v4.1q, v0.1d, k.1d — polynomial multiply using the low 64-bit lanes.
12+
#[inline]
13+
#[target_feature(enable = "neon")]
14+
#[target_feature(enable = "aes")]
15+
unsafe fn pmull_lo(a: uint8x16_t, k: poly64x2_t) -> uint8x16_t {
16+
unsafe {
17+
let a = vreinterpretq_p64_u8(a);
18+
vreinterpretq_u8_p128(vmull_p64(vgetq_lane_p64::<0>(a), vgetq_lane_p64::<0>(k)))
19+
}
20+
}
21+
22+
/// Polynomial multiply using the high 64-bit lanes.
23+
#[inline]
24+
#[target_feature(enable = "neon")]
25+
#[target_feature(enable = "aes")]
26+
unsafe fn pmull_hi(a: uint8x16_t, k: poly64x2_t) -> uint8x16_t {
27+
unsafe { vreinterpretq_u8_p128(vmull_high_p64(vreinterpretq_p64_u8(a), k)) }
28+
}
29+
30+
/// Fold v0 into `next` using constant pair `k`.
31+
#[inline]
32+
#[target_feature(enable = "neon")]
33+
#[target_feature(enable = "aes")]
34+
unsafe fn fold1(v0: uint8x16_t, next: uint8x16_t, k: poly64x2_t) -> uint8x16_t {
35+
unsafe {
36+
let t = veorq_u8(pmull_lo(v0, k), next);
37+
veorq_u8(pmull_hi(v0, k), t)
38+
}
39+
}
40+
41+
/// CRC32 vectorized using PMULL.
42+
///
43+
/// Implements the PCLMULQDQ-equivalent technique on AArch64 as described in
44+
/// Intel's "Fast CRC Computation for Generic Polynomials Using PCLMULQDQ Instruction".
45+
///
46+
/// Derived from the Apple zlib implementation of crc32_little_aligned_vector.
47+
#[target_feature(enable = "neon")]
48+
#[target_feature(enable = "aes")]
49+
unsafe fn crc32_little_aligned_vector(crc: u32, mut buf: &[uint8x16_t]) -> u32 {
50+
// 4-way 512-bit folding: K1 = x^(512+64) mod P(x), K2 = x^512 mod P(x)
51+
let k12 = vcombine_p64(vcreate_p64(0x154442bd4), vcreate_p64(0x1c6e41596));
52+
// 1-way 128-bit folding: K3 = x^(128+64) mod P(x), K4 = x^128 mod P(x)
53+
let k34 = vcombine_p64(vcreate_p64(0x1751997d0), vcreate_p64(0x0ccaa009e));
54+
// 128-bit -> 64-bit reduction: K5 = x^96 mod P(x), K6 = x^64 mod P(x)
55+
let k56 = vcombine_p64(vcreate_p64(0x0ccaa009e), vcreate_p64(0x163cd6124));
56+
// Barrett reduction: ux = floor(x^64 / P(x)), Px = P(x)
57+
let upx = vcombine_p64(vcreate_p64(0x1F7011641), vcreate_p64(0x1DB710641));
58+
59+
let mut v0 = buf[0];
60+
buf = &buf[1..];
61+
62+
// Insert crc into lane 0 and XOR with the first vector.
63+
let zero_with_crc = unsafe { vreinterpretq_u8_u32(vsetq_lane_u32::<0>(crc, vdupq_n_u32(0))) };
64+
v0 = unsafe { veorq_u8(zero_with_crc, v0) };
65+
66+
if !buf.is_empty() {
67+
if let [v1, v2, v3, rest @ ..] = buf {
68+
buf = rest;
69+
70+
let mut v1 = *v1;
71+
let mut v2 = *v2;
72+
let mut v3 = *v3;
73+
74+
// Fold 4 independent streams using K12.
75+
while let [n0, n1, n2, n3, rest @ ..] = buf {
76+
buf = rest;
77+
78+
let t0 = unsafe { veorq_u8(pmull_lo(v0, k12), *n0) };
79+
let t1 = unsafe { veorq_u8(pmull_lo(v1, k12), *n1) };
80+
v0 = unsafe { veorq_u8(pmull_hi(v0, k12), t0) };
81+
v1 = unsafe { veorq_u8(pmull_hi(v1, k12), t1) };
82+
83+
let t2 = unsafe { veorq_u8(pmull_lo(v2, k12), *n2) };
84+
let t3 = unsafe { veorq_u8(pmull_lo(v3, k12), *n3) };
85+
v2 = unsafe { veorq_u8(pmull_hi(v2, k12), t2) };
86+
v3 = unsafe { veorq_u8(pmull_hi(v3, k12), t3) };
87+
}
88+
89+
// Merge the 4 streams into v0 using K34.
90+
v0 = unsafe { fold1(v0, v1, k34) };
91+
v0 = unsafe { fold1(v0, v2, k34) };
92+
v0 = unsafe { fold1(v0, v3, k34) };
93+
}
94+
95+
// Fold remaining single vectors using K34.
96+
for &next in buf {
97+
v0 = unsafe { fold1(v0, next, k34) };
98+
}
99+
}
100+
101+
// Reduce 128-bit v0 down to 32-bit CRC.
102+
103+
// Step 1: 128 -> 96 bits using K5 (i.e. x^96 mod P(x)).
104+
105+
// Fold low 64 bits forward by 96.
106+
let v1 = unsafe { pmull_lo(v0, k56) };
107+
// Shift high 64 bits to low position and XOR.
108+
let v2 = unsafe {
109+
let hi = vgetq_lane_u64::<1>(vreinterpretq_u64_u8(v0));
110+
vreinterpretq_u8_u64(vsetq_lane_u64::<0>(hi, vdupq_n_u64(0)))
111+
};
112+
v0 = unsafe { veorq_u8(v2, v1) };
113+
114+
// Extract the 96-bit result into the correct position for the K6 multiply.
115+
let v3 = unsafe {
116+
let u = vreinterpretq_u32_u8(v0);
117+
let s0 = vgetq_lane_u32::<0>(u);
118+
let s1 = vgetq_lane_u32::<1>(u);
119+
let s2 = vgetq_lane_u32::<2>(u);
120+
let v3 = vreinterpretq_u8_u32(vsetq_lane_u32::<2>(s0, vdupq_n_u32(0)));
121+
let u = vsetq_lane_u32::<0>(s1, u);
122+
let u = vsetq_lane_u32::<1>(s2, u);
123+
v0 = vreinterpretq_u8_u32(u);
124+
v3
125+
};
126+
127+
// Step 2: 96 -> 64 bits using K6 (i.e. x^64 mod P(x)).
128+
v0 = unsafe { veorq_u8(pmull_hi(v3, k56), v0) };
129+
130+
// Barrett reduction: 64 -> 32 bits.
131+
//
132+
// T1 = floor(v0 / x^32) * ux
133+
// T2 = floor(T1 / x^32) * Px
134+
// CRC = (v0 XOR T2) mod x^32
135+
136+
// Isolate low 32 bits of v0 for T1, clear the other lanes.
137+
let floor_v0 = unsafe {
138+
let u = vreinterpretq_u32_u8(v0);
139+
vreinterpretq_u8_u32(vsetq_lane_u32::<0>(vgetq_lane_u32::<0>(u), vdupq_n_u32(0)))
140+
};
141+
142+
let t1 = unsafe { pmull_lo(floor_v0, upx) };
143+
144+
// Place low 32 bits of T1 into high lane where pmull_hi expects it.
145+
let floor_t1 = unsafe {
146+
let u3 = vreinterpretq_u32_u8(v0);
147+
let t = vgetq_lane_u32::<0>(vreinterpretq_u32_u8(t1));
148+
vreinterpretq_u8_u32(vsetq_lane_u32::<2>(t, u3))
149+
};
150+
151+
let t2 = unsafe { pmull_hi(floor_t1, upx) };
152+
153+
// Apply correction and extract the output.
154+
let crc = unsafe { veorq_u8(v0, t2) };
155+
unsafe { (vgetq_lane_u64::<0>(vreinterpretq_u64_u8(crc)) >> 32) as u32 }
156+
}
157+
158+
/// CRC32 using Apple's pmull-based vectorized algorithm.
159+
///
160+
/// Requires the AES hardware feature (for 64-bit polynomial multiply).
161+
/// Falls back to the scalar table for unaligned prefix, short chunks,
162+
/// and trailing bytes.
163+
#[target_feature(enable = "aes")]
164+
pub unsafe fn crc32_pmull_aarch64(crc: u32, buf: &[u8]) -> u32 {
165+
let table = super::braid::get_crc_table();
166+
let mut crc = !crc;
167+
let mut buf = buf;
168+
169+
// SAFETY: [u8; 16] safely transmutes into uint8x16_t.
170+
let (before, mut middle, after) = unsafe { buf.align_to::<uint8x16_t>() };
171+
172+
// Align to 16 bytes using the CRC table
173+
for &byte in before {
174+
crc = (crc >> 8) ^ table[((crc ^ byte as u32) & 0xff) as usize];
175+
}
176+
177+
// Minimum 32 bytes needed; process in chunks up to 2^30 bytes.
178+
while let [_, _, ..] = middle {
179+
let k = Ord::min(middle.len(), 1 << 26);
180+
let (now, later) = middle.split_at(k);
181+
crc = unsafe { crc32_little_aligned_vector(crc, now) };
182+
middle = later;
183+
}
184+
185+
// Trailing bytes (< 32) processed with the CRC table
186+
for &byte in &buf[buf.len() - (middle.len() * 16 + after.len())..] {
187+
crc = (crc >> 8) ^ table[((crc ^ byte as u32) & 0xff) as usize];
188+
}
189+
190+
!crc
191+
}
192+
193+
#[cfg(test)]
194+
mod tests {
195+
use super::*;
196+
quickcheck::quickcheck! {
197+
fn crc32_pmull_aarch64_is_crc32fast(v: Vec<u8>, start: u32) -> bool {
198+
if !crate::cpu_features::is_enabled_aes() {
199+
return true;
200+
}
201+
202+
let mut h = crc32fast::Hasher::new_with_initial(start);
203+
h.update(&v);
204+
205+
let a = unsafe { crc32_pmull_aarch64(start, &v) };
206+
let b = h.finalize();
207+
208+
a == b
209+
}
210+
}
211+
}

0 commit comments

Comments
 (0)