const P1: u64 = 0x9E37_79B1_85EB_CA87;
const P2: u64 = 0xC2B2_AE3D_27D4_EB4F;
const P3: u64 = 0x1656_67B1_9E37_79F9;
const P4: u64 = 0x85EB_CA77_C2B2_AE63;
const P5: u64 = 0x27D4_EB2F_1656_67C5;
#[inline(always)]
fn round(acc: u64, input: u64) -> u64 {
acc.wrapping_add(input.wrapping_mul(P2))
.rotate_left(31)
.wrapping_mul(P1)
}
#[inline(always)]
fn merge(acc: u64, val: u64) -> u64 {
(acc ^ round(0, val)).wrapping_mul(P1).wrapping_add(P4)
}
#[inline(always)]
fn read_u64_at(s: &[u8], i: usize) -> u64 {
let mut buf = [0u8; 8];
buf.copy_from_slice(&s[i..i + 8]);
u64::from_le_bytes(buf)
}
#[inline(always)]
fn read_u32_at(s: &[u8], i: usize) -> u32 {
let mut buf = [0u8; 4];
buf.copy_from_slice(&s[i..i + 4]);
u32::from_le_bytes(buf)
}
#[inline(always)]
fn stripe(v: &mut [u64; 4], src: &[u8], off: usize) {
v[0] = round(v[0], read_u64_at(src, off));
v[1] = round(v[1], read_u64_at(src, off + 8));
v[2] = round(v[2], read_u64_at(src, off + 16));
v[3] = round(v[3], read_u64_at(src, off + 24));
}
const PRE_TILE: usize = 256;
#[cfg(target_arch = "x86_64")]
#[target_feature(enable = "avx2")]
#[allow(unsafe_code)]
unsafe fn premul_p2_avx2(
src: &[u8; PRE_TILE],
out: &mut [core::mem::MaybeUninit<u64>; PRE_TILE / 8],
) {
use core::arch::x86_64::*;
unsafe {
let p2lo = _mm256_set1_epi64x((P2 & 0xFFFF_FFFF) as i64);
let p2hi = _mm256_set1_epi64x((P2 >> 32) as i64);
let n = PRE_TILE / 32;
let sp = src.as_ptr();
let op = out.as_mut_ptr().cast::<u64>();
let mut i = 0usize;
while i < n {
let a = _mm256_loadu_si256(sp.add(i * 32) as *const __m256i);
let t0 = _mm256_mul_epu32(a, p2lo);
let t1 = _mm256_mul_epu32(a, p2hi);
let ah = _mm256_srli_epi64(a, 32);
let t2 = _mm256_mul_epu32(ah, p2lo);
let cross = _mm256_slli_epi64(_mm256_add_epi64(t1, t2), 32);
_mm256_storeu_si256(op.add(i * 4) as *mut __m256i, _mm256_add_epi64(t0, cross));
i += 1;
}
}
}
#[cfg(target_arch = "aarch64")]
#[allow(unsafe_code)]
unsafe fn premul_p2_neon(
src: &[u8; PRE_TILE],
out: &mut [core::mem::MaybeUninit<u64>; PRE_TILE / 8],
) {
use core::arch::aarch64::*;
unsafe {
let p2lo = vdup_n_u32((P2 & 0xFFFF_FFFF) as u32);
let p2hi = vdup_n_u32((P2 >> 32) as u32);
let sp = src.as_ptr();
let op = out.as_mut_ptr().cast::<u64>();
let n = PRE_TILE / 16;
let mut i = 0usize;
while i < n {
let a = vld1q_u64(sp.add(i * 16).cast::<u64>());
let alo = vmovn_u64(a); let ahi = vshrn_n_u64(a, 32); let t0 = vmull_u32(alo, p2lo);
let t1 = vmull_u32(alo, p2hi);
let t2 = vmull_u32(ahi, p2lo);
let cross = vshlq_n_u64(vaddq_u64(t1, t2), 32);
vst1q_u64(op.add(i * 2), vaddq_u64(t0, cross));
i += 1;
}
}
}
#[inline(always)]
fn round_pre(acc: u64, pre: u64) -> u64 {
acc.wrapping_add(pre).rotate_left(31).wrapping_mul(P1)
}
#[inline(always)]
fn stripes_pre<F>(input: &[u8], v: &mut [u64; 4], mut premul: F) -> usize
where
F: FnMut(&[u8; PRE_TILE], &mut [core::mem::MaybeUninit<u64>; PRE_TILE / 8]),
{
let n = (input.len() / PRE_TILE) * PRE_TILE;
if n == 0 {
return 0;
}
let (mut v1, mut v2, mut v3, mut v4) = (v[0], v[1], v[2], v[3]);
let mut pre = [const { core::mem::MaybeUninit::<u64>::uninit() }; PRE_TILE / 8];
for tile in input[..n].chunks_exact(PRE_TILE) {
let Ok(tile) = <&[u8; PRE_TILE]>::try_from(tile) else {
unreachable!()
};
premul(tile, &mut pre);
#[allow(unsafe_code)]
let pre = unsafe { &*pre.as_ptr().cast::<[u64; PRE_TILE / 8]>() };
let mut k = 0usize;
while k + 4 <= PRE_TILE / 8 {
v1 = round_pre(v1, pre[k]);
v2 = round_pre(v2, pre[k + 1]);
v3 = round_pre(v3, pre[k + 2]);
v4 = round_pre(v4, pre[k + 3]);
k += 4;
}
}
v[0] = v1;
v[1] = v2;
v[2] = v3;
v[3] = v4;
#[cfg(feature = "profile")]
{
census::HYBRID_BYTES.fetch_add(n as u64, core::sync::atomic::Ordering::Relaxed);
census::HYBRID_CALLS.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
}
n
}
#[inline]
fn stripes_hybrid(input: &[u8], v: &mut [u64; 4]) -> usize {
#[cfg(target_arch = "x86_64")]
{
if crate::simd::has_avx2() && vec_arm_enabled() {
return stripes_pre(input, v, |tile, out| {
#[allow(unsafe_code)]
unsafe {
premul_p2_avx2(tile, out)
}
});
}
}
#[cfg(target_arch = "aarch64")]
{
if vec_arm_enabled() {
return stripes_pre(input, v, |tile, out| {
#[allow(unsafe_code)]
unsafe {
premul_p2_neon(tile, out)
}
});
}
}
let _ = (input, v);
0
}
static XXH_AVX2_ARM: core::sync::atomic::AtomicU8 = core::sync::atomic::AtomicU8::new(0);
pub fn set_xxh_avx2_arm(on: bool) {
XXH_AVX2_ARM.store(u8::from(on) + 1, core::sync::atomic::Ordering::Relaxed);
}
#[inline(always)]
fn vec_arm_enabled() -> bool {
XXH_AVX2_ARM.load(core::sync::atomic::Ordering::Relaxed) != 1
}
#[inline(never)]
fn finish_tail(mut tail: &[u8], mut acc: u64, total: u64) -> u64 {
debug_assert!(tail.len() <= 31, "xxh64 tail {} exceeded 31", tail.len());
acc = acc.wrapping_add(total);
while tail.len() >= 8 {
let k1 = round(0, read_u64_at(tail, 0));
acc = (acc ^ k1).rotate_left(27).wrapping_mul(P1).wrapping_add(P4);
tail = &tail[8..];
}
if tail.len() >= 4 {
let k1 = u64::from(read_u32_at(tail, 0));
acc = (acc ^ k1.wrapping_mul(P1))
.rotate_left(23)
.wrapping_mul(P2)
.wrapping_add(P3);
tail = &tail[4..];
}
debug_assert!(tail.len() <= 3, "byte tail {} exceeded 3", tail.len());
for i in 0..3 {
let Some(&b) = tail.get(i) else { break };
acc = (acc ^ u64::from(b).wrapping_mul(P5))
.rotate_left(11)
.wrapping_mul(P1);
}
acc ^= acc >> 33;
acc = acc.wrapping_mul(P2);
acc ^= acc >> 29;
acc = acc.wrapping_mul(P3);
acc ^= acc >> 32;
acc
}
#[cfg(feature = "profile")]
pub mod census {
use core::sync::atomic::{AtomicU64, Ordering};
pub static HYBRID_BYTES: AtomicU64 = AtomicU64::new(0);
pub static SCALAR_BYTES: AtomicU64 = AtomicU64::new(0);
pub static HYBRID_CALLS: AtomicU64 = AtomicU64::new(0);
pub fn take() -> (u64, u64, u64) {
(
HYBRID_BYTES.swap(0, Ordering::Relaxed),
SCALAR_BYTES.swap(0, Ordering::Relaxed),
HYBRID_CALLS.swap(0, Ordering::Relaxed),
)
}
}
#[inline]
fn stripes_scalar(input: &[u8], v: &mut [u64; 4]) -> usize {
let len = input.len();
let n128 = (len / 128) * 128;
for chunk in input[..n128].chunks_exact(128) {
stripe(v, chunk, 0);
stripe(v, chunk, 32);
stripe(v, chunk, 64);
stripe(v, chunk, 96);
}
let n32 = (len / 32) * 32;
for chunk in input[n128..n32].chunks_exact(32) {
stripe(v, chunk, 0);
}
#[cfg(feature = "profile")]
census::SCALAR_BYTES.fetch_add(n32 as u64, core::sync::atomic::Ordering::Relaxed);
n32
}
#[inline]
fn stripes_all<'a>(input: &'a [u8], v: &mut [u64; 4]) -> &'a [u8] {
let done = stripes_hybrid(input, v);
let rest = &input[done..];
let done2 = stripes_scalar(rest, v);
&rest[done2..]
}
#[inline(never)]
fn combine(v1: u64, v2: u64, v3: u64, v4: u64) -> u64 {
let acc = v1
.rotate_left(1)
.wrapping_add(v2.rotate_left(7))
.wrapping_add(v3.rotate_left(12))
.wrapping_add(v4.rotate_left(18));
let acc = merge(acc, v1);
let acc = merge(acc, v2);
let acc = merge(acc, v3);
merge(acc, v4)
}
pub fn xxh64(input: &[u8]) -> u64 {
xxh64_seed(input, 0)
}
pub fn xxh64_seed(input: &[u8], seed: u64) -> u64 {
let len = input.len();
if len >= 32 {
let mut v = [
seed.wrapping_add(P1).wrapping_add(P2),
seed.wrapping_add(P2),
seed,
seed.wrapping_sub(P1),
];
let rest = stripes_all(input, &mut v);
let [v1, v2, v3, v4] = v;
debug_assert_eq!(len - rest.len(), (len / 32) * 32);
finish_tail(rest, combine(v1, v2, v3, v4), len as u64)
} else {
finish_tail(input, seed.wrapping_add(P5), len as u64)
}
}
pub struct Xxh64 {
total: u64,
v1: u64,
v2: u64,
v3: u64,
v4: u64,
buf: [u8; 32],
buf_len: usize,
}
impl Xxh64 {
pub fn new() -> Self {
Self {
total: 0,
v1: P1.wrapping_add(P2),
v2: P2,
v3: 0,
v4: 0u64.wrapping_sub(P1),
buf: [0; 32],
buf_len: 0,
}
}
pub fn update(&mut self, mut data: &[u8]) {
self.total = self.total.wrapping_add(data.len() as u64);
debug_assert!(self.buf_len < 32, "buf_len {} escaped 0..32", self.buf_len);
let buf_len = self.buf_len & 31;
if buf_len + data.len() < 32 {
self.buf[buf_len..buf_len + data.len()].copy_from_slice(data);
self.buf_len = buf_len + data.len();
return;
}
let mut v = [self.v1, self.v2, self.v3, self.v4];
if buf_len > 0 {
let take = 32 - buf_len;
let (head, rest) = data.split_at(take);
self.buf[buf_len..].copy_from_slice(head);
data = rest;
stripe(&mut v, &self.buf, 0);
self.buf_len = 0;
}
let rest = stripes_all(data, &mut v);
if !rest.is_empty() {
debug_assert!(rest.len() < 32, "xxh64 remainder {} reached 32", rest.len());
let n = rest.len() & 31;
self.buf[..n].copy_from_slice(&rest[..n]);
self.buf_len = n;
}
[self.v1, self.v2, self.v3, self.v4] = v;
}
pub fn digest(&self) -> u64 {
debug_assert!(self.buf_len < 32, "buf_len {} escaped 0..32", self.buf_len);
let rest = &self.buf[..self.buf_len & 31];
let acc = if self.total >= 32 {
combine(self.v1, self.v2, self.v3, self.v4)
} else {
P5
};
finish_tail(rest, acc, self.total)
}
}
impl Default for Xxh64 {
fn default() -> Self {
Self::new()
}
}
pub fn content_checksum(data: &[u8]) -> u32 {
xxh64(data) as u32
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn matches_spec_oracle_at_every_length() {
const RP1: u64 = 0x9E37_79B1_85EB_CA87;
const RP2: u64 = 0xC2B2_AE3D_27D4_EB4F;
const RP3: u64 = 0x1656_67B1_9E37_79F9;
const RP4: u64 = 0x85EB_CA77_C2B2_AE63;
const RP5: u64 = 0x27D4_EB2F_1656_67C5;
fn rnd(acc: u64, x: u64) -> u64 {
acc.wrapping_add(x.wrapping_mul(RP2))
.rotate_left(31)
.wrapping_mul(RP1)
}
fn w(d: &[u8], i: usize) -> u64 {
u64::from_le_bytes(d[i..i + 8].try_into().unwrap())
}
fn ref_xxh64(d: &[u8]) -> u64 {
let n = d.len();
let mut i = 0usize;
let mut acc;
if n >= 32 {
let (mut v1, mut v2, mut v3, mut v4) =
(RP1.wrapping_add(RP2), RP2, 0u64, 0u64.wrapping_sub(RP1));
while i + 32 <= n {
v1 = rnd(v1, w(d, i));
v2 = rnd(v2, w(d, i + 8));
v3 = rnd(v3, w(d, i + 16));
v4 = rnd(v4, w(d, i + 24));
i += 32;
}
acc = v1
.rotate_left(1)
.wrapping_add(v2.rotate_left(7))
.wrapping_add(v3.rotate_left(12))
.wrapping_add(v4.rotate_left(18));
for v in [v1, v2, v3, v4] {
acc = (acc ^ rnd(0, v)).wrapping_mul(RP1).wrapping_add(RP4);
}
} else {
acc = RP5;
}
acc = acc.wrapping_add(n as u64);
while i + 8 <= n {
acc = (acc ^ rnd(0, w(d, i)))
.rotate_left(27)
.wrapping_mul(RP1)
.wrapping_add(RP4);
i += 8;
}
if i + 4 <= n {
let k = u64::from(u32::from_le_bytes(d[i..i + 4].try_into().unwrap()));
acc = (acc ^ k.wrapping_mul(RP1))
.rotate_left(23)
.wrapping_mul(RP2)
.wrapping_add(RP3);
i += 4;
}
while i < n {
acc = (acc ^ u64::from(d[i]).wrapping_mul(RP5))
.rotate_left(11)
.wrapping_mul(RP1);
i += 1;
}
acc ^= acc >> 33;
acc = acc.wrapping_mul(RP2);
acc ^= acc >> 29;
acc = acc.wrapping_mul(RP3);
acc ^= acc >> 32;
acc
}
let mut data = alloc::vec![0u8; 1100];
for (i, b) in data.iter_mut().enumerate() {
*b = (i.wrapping_mul(167).wrapping_add(29) % 251) as u8;
}
let mut checked = 0usize;
for len in (0usize..=600).chain([768, 1023, 1024, 1025, 1088, 1099]) {
let d = &data[..len];
let want = ref_xxh64(d);
assert_eq!(xxh64(d), want, "one-shot len {len}");
for c in [1usize, 7, 31, 32, 33, 127, 128, 129, 255, 256, 257] {
let mut h = Xxh64::new();
for part in d.chunks(c) {
h.update(part);
}
assert_eq!(h.digest(), want, "stream len {len} chunk {c}");
}
checked += 1;
}
assert_eq!(checked, 607, "lengths actually exercised");
}
#[test]
fn premul_decomposition_matches_mul() {
fn decomposed(a: u64) -> u64 {
let (alo, ahi) = (a & 0xFFFF_FFFF, a >> 32);
let (p2lo, p2hi) = (P2 & 0xFFFF_FFFF, P2 >> 32);
let t0 = alo.wrapping_mul(p2lo);
let t1 = alo.wrapping_mul(p2hi);
let t2 = ahi.wrapping_mul(p2lo);
t0.wrapping_add(t1.wrapping_add(t2) << 32)
}
for &a in &[
0u64,
1,
u64::MAX,
1 << 31,
1 << 32,
(1 << 32) - 1,
u32::MAX as u64,
(u32::MAX as u64) << 32,
0xFFFF_FFFF_0000_0000,
0x0000_0000_FFFF_FFFF,
0x8000_0000_8000_0000,
] {
assert_eq!(decomposed(a), a.wrapping_mul(P2), "a = {a:#018x}");
}
let mut x = 0x243F_6A88_85A3_08D3u64;
for _ in 0..200_000 {
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
assert_eq!(decomposed(x), x.wrapping_mul(P2), "a = {x:#018x}");
}
for b in 0..64 {
let a = 1u64 << b;
assert_eq!(decomposed(a), a.wrapping_mul(P2), "bit {b}");
}
}
#[test]
fn empty_seed0() {
assert_eq!(xxh64(b""), 0xEF46_DB37_51D8_E999);
assert_eq!(content_checksum(b""), 0x51D8_E999);
}
#[test]
fn c_zstd_157_checksums() {
assert_eq!(content_checksum(b"a"), 0xA98C_6E5B);
assert_eq!(content_checksum(b"hello"), 0x889F_6DA3);
}
#[test]
fn thirty_two_zeros_stripe_path() {
assert_eq!(xxh64(&[0u8; 32]), 0xF6E9_BE5D_7063_2CF5);
}
#[test]
fn hasher_matches_oneshot() {
let samples: &[&[u8]] = &[
b"", b"a", b"hello", &[0u8; 31], &[0u8; 32], &[0u8; 33], &[0u8; 64],
];
for &s in samples {
let mut h = Xxh64::new();
h.update(s);
assert_eq!(h.digest(), xxh64(s), "len {}", s.len());
let mut h2 = Xxh64::new();
for chunk in s.chunks(3) {
h2.update(chunk);
}
assert_eq!(h2.digest(), xxh64(s), "chunked len {}", s.len());
}
let mut long = vec![0u8; 100_003];
for (i, b) in long.iter_mut().enumerate() {
*b = (i.wrapping_mul(251) % 251) as u8;
}
let mut h = Xxh64::new();
h.update(&long);
assert_eq!(h.digest(), xxh64(&long), "long oneshot");
let mut h2 = Xxh64::new();
for chunk in long.chunks(17) {
h2.update(chunk);
}
assert_eq!(h2.digest(), xxh64(&long), "long chunked");
}
}
#[cfg(test)]
mod locality_probe {
use super::*;
use std::time::Instant;
#[ignore]
#[test]
fn xxh64_throughput_by_working_set() {
for (label, sz) in [
("32 KiB (L1/L2)", 32usize << 10),
("256 KiB (L2)", 256 << 10),
("4 MiB (L3)", 4 << 20),
("32 MiB (DRAM)", 32 << 20),
] {
let buf = alloc::vec![0u8; sz];
let total: usize = 512 << 20;
let reps = total / sz;
let t = Instant::now();
let mut acc = 0u64;
for _ in 0..reps {
acc ^= u64::from(content_checksum(&buf));
}
let s = t.elapsed().as_secs_f64();
std::hint::black_box(acc);
println!(" {label:16} {:7.1} GB/s", (total as f64) / s / 1e9);
}
}
}