nodedb_codec/vector_quant/
hamming.rs1#![allow(unsafe_op_in_unsafe_fn)]
17
18use std::sync::OnceLock;
19
20type HammingFn = fn(a: &[u8], b: &[u8]) -> u32;
21
22static DISPATCH: OnceLock<HammingFn> = OnceLock::new();
23
24fn scalar(a: &[u8], b: &[u8]) -> u32 {
25 let chunks = a.len() / 8;
26 let rem = a.len() % 8;
27 let mut count = 0u32;
28 for i in 0..chunks {
29 let av = u64::from_ne_bytes(a[i * 8..i * 8 + 8].try_into().unwrap_or([0u8; 8]));
30 let bv = u64::from_ne_bytes(b[i * 8..i * 8 + 8].try_into().unwrap_or([0u8; 8]));
31 count += (av ^ bv).count_ones();
32 }
33 let base = chunks * 8;
34 for i in 0..rem {
35 count += (a[base + i] ^ b[base + i]).count_ones();
36 }
37 count
38}
39
40#[cfg(target_arch = "x86_64")]
41#[target_feature(enable = "avx512f,avx512vpopcntdq")]
42unsafe fn avx512(a: &[u8], b: &[u8]) -> u32 {
43 use std::arch::x86_64::*;
44 let mut count = 0u32;
45 let chunks = a.len() / 64;
46 let rem = a.len() % 64;
47 for i in 0..chunks {
48 let va = _mm512_loadu_si512(a.as_ptr().add(i * 64) as *const __m512i);
49 let vb = _mm512_loadu_si512(b.as_ptr().add(i * 64) as *const __m512i);
50 let xored = _mm512_xor_si512(va, vb);
51 let popcnt = _mm512_popcnt_epi64(xored);
52 let lo = _mm512_extracti64x4_epi64(popcnt, 0);
53 let hi = _mm512_extracti64x4_epi64(popcnt, 1);
54 let sum4 = _mm256_add_epi64(lo, hi);
55 let sum2 = _mm256_add_epi64(sum4, _mm256_permute4x64_epi64(sum4, 0b0100_1110));
56 let sum1 = _mm256_add_epi64(sum2, _mm256_permute4x64_epi64(sum2, 0b0001_0001));
57 count += _mm256_extract_epi64(sum1, 0) as u32;
58 }
59 let base = chunks * 64;
60 for i in 0..rem {
61 count += (a[base + i] ^ b[base + i]).count_ones();
62 }
63 count
64}
65
66#[cfg(target_arch = "x86_64")]
67#[target_feature(enable = "avx2")]
68unsafe fn avx2(a: &[u8], b: &[u8]) -> u32 {
69 let mut count = 0u32;
70 let n8 = a.len() / 8;
71 let mut i = 0usize;
72 while i + 4 <= n8 {
73 for word in i..i + 4 {
74 let start = word * 8;
75 let end = start + 8;
76 let av = u64::from_ne_bytes(a[start..end].try_into().unwrap_or([0u8; 8]));
77 let bv = u64::from_ne_bytes(b[start..end].try_into().unwrap_or([0u8; 8]));
78 count += (av ^ bv).count_ones();
79 }
80 i += 4;
81 }
82 while i < n8 {
83 let start = i * 8;
84 let end = start + 8;
85 let av = u64::from_ne_bytes(a[start..end].try_into().unwrap_or([0u8; 8]));
86 let bv = u64::from_ne_bytes(b[start..end].try_into().unwrap_or([0u8; 8]));
87 count += (av ^ bv).count_ones();
88 i += 1;
89 }
90 let tail_start = n8 * 8;
91 for j in tail_start..a.len() {
92 count += (a[j] ^ b[j]).count_ones();
93 }
94 count
95}
96
97#[cfg(target_arch = "x86_64")]
98fn avx512_trampoline(a: &[u8], b: &[u8]) -> u32 {
99 unsafe { avx512(a, b) }
100}
101
102#[cfg(target_arch = "x86_64")]
103fn avx2_trampoline(a: &[u8], b: &[u8]) -> u32 {
104 unsafe { avx2(a, b) }
105}
106
107#[cfg(target_arch = "aarch64")]
108fn neon(a: &[u8], b: &[u8]) -> u32 {
109 use std::arch::aarch64::*;
110 let mut count = 0u32;
111 let chunks = a.len() / 16;
112 let rem = a.len() % 16;
113 unsafe {
114 for i in 0..chunks {
115 let va = vld1q_u8(a.as_ptr().add(i * 16));
116 let vb = vld1q_u8(b.as_ptr().add(i * 16));
117 let xored = veorq_u8(va, vb);
118 let popcnt = vcntq_u8(xored);
119 count += vaddvq_u8(popcnt) as u32;
120 }
121 }
122 let base = chunks * 16;
123 for i in 0..rem {
124 count += (a[base + i] ^ b[base + i]).count_ones();
125 }
126 count
127}
128
129fn resolve() -> HammingFn {
130 #[cfg(target_arch = "x86_64")]
131 {
132 if std::is_x86_feature_detected!("avx512f")
133 && std::is_x86_feature_detected!("avx512vpopcntdq")
134 {
135 return avx512_trampoline;
136 }
137 if std::is_x86_feature_detected!("avx2") {
138 return avx2_trampoline;
139 }
140 }
141 #[cfg(target_arch = "aarch64")]
142 {
143 return neon;
144 }
145 #[allow(unreachable_code)]
146 {
147 scalar
148 }
149}
150
151#[inline]
157pub fn hamming_distance(a: &[u8], b: &[u8]) -> u32 {
158 debug_assert_eq!(a.len(), b.len(), "hamming_distance: slice length mismatch");
159 let len = a.len().min(b.len());
160 let f = DISPATCH.get_or_init(resolve);
161 f(&a[..len], &b[..len])
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167
168 #[test]
169 fn zero_distance_to_self() {
170 let bits = vec![0b1010_1010u8, 0b1100_1100, 0b1111_0000];
171 assert_eq!(hamming_distance(&bits, &bits), 0);
172 }
173
174 #[test]
175 fn full_inversion_byte() {
176 let a = vec![0xFFu8];
177 let b = vec![0x00u8];
178 assert_eq!(hamming_distance(&a, &b), 8);
179 }
180
181 #[test]
182 fn full_inversion_multibyte() {
183 let dim = 64;
184 let a: Vec<u8> = (0..dim as u8).collect();
185 let b: Vec<u8> = a.iter().map(|&x| !x).collect();
186 assert_eq!(hamming_distance(&a, &b), 512);
187 }
188
189 #[test]
190 fn unaligned_subslices_match_scalar() {
191 let a_storage: Vec<u8> = (0..258).map(|i| (i * 31 + 7) as u8).collect();
192 let b_storage: Vec<u8> = (0..259).map(|i| (i * 17 + 3) as u8).collect();
193 let a = &a_storage[1..257];
194 let b = &b_storage[2..258];
195 assert_eq!(hamming_distance(a, b), scalar(a, b));
196 }
197
198 #[cfg(target_arch = "x86_64")]
199 #[test]
200 fn avx2_directly_handles_unaligned_subslices() {
201 if !std::is_x86_feature_detected!("avx2") {
202 return;
203 }
204 let a_storage: Vec<u8> = (0..258).map(|i| (i * 31 + 7) as u8).collect();
205 let b_storage: Vec<u8> = (0..259).map(|i| (i * 17 + 3) as u8).collect();
206 let a = &a_storage[1..257];
207 let b = &b_storage[2..258];
208 assert_eq!(unsafe { avx2(a, b) }, scalar(a, b));
209 }
210
211 #[test]
212 fn agrees_across_lengths() {
213 for len in [1usize, 7, 8, 9, 15, 16, 31, 32, 63, 64, 65, 127, 128, 255] {
216 let a: Vec<u8> = (0..len).map(|i| (i * 31 + 7) as u8).collect();
217 let b: Vec<u8> = (0..len).map(|i| (i * 17 + 3) as u8).collect();
218 assert_eq!(hamming_distance(&a, &b), scalar(&a, &b), "len={len}");
219 }
220 }
221}