const LANES: usize = 8;
#[inline]
pub fn dot(a: &[f32], b: &[f32]) -> f32 {
let n = a.len().min(b.len());
let (a, b) = (&a[..n], &b[..n]);
let mut acc = [0.0f32; LANES];
let (ca, ra) = a.as_chunks::<LANES>();
let (cb, rb) = b.as_chunks::<LANES>();
let tail: f32 = ra.iter().zip(rb).map(|(x, y)| x * y).sum();
for (x, y) in ca.iter().zip(cb) {
for i in 0..LANES {
acc[i] += x[i] * y[i];
}
}
acc.iter().sum::<f32>() + tail
}
#[inline]
pub fn l2_norm(v: &[f32]) -> f32 {
dot(v, v).sqrt()
}
#[inline]
pub fn cosine_with_norms(a: &[f32], b: &[f32], norm_a: f32, norm_b: f32) -> f32 {
if a.len() != b.len() || norm_a == 0.0 || norm_b == 0.0 {
return 0.0;
}
dot(a, b) / (norm_a * norm_b)
}
#[inline]
pub fn cosine_similarity(a: &[f32], b: &[f32]) -> f32 {
if a.len() != b.len() {
return 0.0;
}
let mut ab = [0.0f32; LANES];
let mut aa = [0.0f32; LANES];
let mut bb = [0.0f32; LANES];
let (ca, ra) = a.as_chunks::<LANES>();
let (cb, rb) = b.as_chunks::<LANES>();
let (mut tab, mut taa, mut tbb) = (0.0f32, 0.0f32, 0.0f32);
for (x, y) in ra.iter().zip(rb) {
tab += x * y;
taa += x * x;
tbb += y * y;
}
for (x, y) in ca.iter().zip(cb) {
for i in 0..LANES {
ab[i] += x[i] * y[i];
aa[i] += x[i] * x[i];
bb[i] += y[i] * y[i];
}
}
let norm_a = (aa.iter().sum::<f32>() + taa).sqrt();
let norm_b = (bb.iter().sum::<f32>() + tbb).sqrt();
if norm_a == 0.0 || norm_b == 0.0 {
0.0
} else {
(ab.iter().sum::<f32>() + tab) / (norm_a * norm_b)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn naive_cosine(a: &[f32], b: &[f32]) -> f32 {
let d: f32 = a.iter().zip(b).map(|(x, y)| x * y).sum();
let na = a.iter().map(|x| x * x).sum::<f32>().sqrt();
let nb = b.iter().map(|x| x * x).sum::<f32>().sqrt();
if na == 0.0 || nb == 0.0 {
0.0
} else {
d / (na * nb)
}
}
fn sample(len: usize, seed: f32) -> Vec<f32> {
(0..len).map(|i| ((i as f32 + seed) * 0.37).sin()).collect()
}
#[test]
fn kernels_match_naive_for_lengths_around_the_lane_width() {
for len in [0, 1, 3, 7, 8, 9, 15, 16, 17, 255, 256, 384] {
let a = sample(len, 1.0);
let b = sample(len, 2.5);
let expected = naive_cosine(&a, &b);
assert!((cosine_similarity(&a, &b) - expected).abs() < 1e-5, "{len}");
let fused = cosine_with_norms(&a, &b, l2_norm(&a), l2_norm(&b));
assert!((fused - expected).abs() < 1e-5, "{len}");
let d: f32 = a.iter().zip(&b).map(|(x, y)| x * y).sum();
assert!((dot(&a, &b) - d).abs() < 1e-3, "{len}");
}
}
#[test]
fn degenerate_inputs_score_zero() {
assert_eq!(cosine_similarity(&[0.0; 4], &[1.0; 4]), 0.0);
assert_eq!(cosine_similarity(&[1.0; 4], &[1.0; 3]), 0.0);
assert_eq!(cosine_with_norms(&[1.0; 4], &[1.0; 3], 2.0, 2.0), 0.0);
assert_eq!(cosine_with_norms(&[1.0; 4], &[1.0; 4], 0.0, 2.0), 0.0);
}
}