use std::mem;
pub struct HyperLogLog<const REGISTERS: usize> {
registers: [u8; REGISTERS],
}
const fn index_bits<const REGISTERS: usize>() -> u32 {
let mut bits = 0u32;
let mut n = REGISTERS;
while n > 1 {
n >>= 1;
bits += 1;
}
bits
}
impl<const REGISTERS: usize> HyperLogLog<REGISTERS> {
pub fn new() -> Self {
HyperLogLog {
registers: [0u8; REGISTERS],
}
}
#[inline]
pub fn add_key(&mut self, key: u64) {
#[cfg(feature = "bcinr")]
{
self.add(crate::bcinr_compat::sketch::fnv1a_64(&key.to_le_bytes()));
}
#[cfg(not(feature = "bcinr"))]
{
let mut h = key.wrapping_mul(0xbf58476d1ce4e5b9);
h ^= h >> 33;
h = h.wrapping_mul(0x94d049bb133111eb);
h ^= h >> 33;
self.add(h);
}
}
#[inline]
pub fn add(&mut self, hash: u64) {
let b = index_bits::<REGISTERS>();
let mask = (1u64 << b) - 1;
let idx = (hash & mask) as usize;
let w = hash >> b;
let w_padded = w << b;
let rho: u8 = if w == 0 {
(64 - b + 1) as u8
} else {
w_padded.leading_zeros() as u8 + 1
};
if rho > self.registers[idx] {
self.registers[idx] = rho;
}
}
pub fn estimate(&self) -> usize {
let m = REGISTERS as f64;
let alpha_m = match REGISTERS {
16 => 0.673,
32 => 0.697,
64 => 0.709,
_ => 0.7213 / (1.0 + 1.079 / m),
};
let sum: f64 = self
.registers
.iter()
.map(|&r| 2.0f64.powi(-(r as i32)))
.sum();
if sum == 0.0 {
return 0;
}
let e = alpha_m * m * m / sum;
if e <= 2.5 * m {
let zeros = self.registers.iter().filter(|&&r| r == 0).count() as f64;
if zeros > 0.0 {
let lc = m * (m / zeros).ln();
return lc.round() as usize;
}
}
let two_32 = 2.0f64.powi(32);
if e > two_32 / 30.0 {
return (-two_32 * (1.0 - e / two_32).ln()).round() as usize;
}
e.round() as usize
}
pub fn merge(&mut self, other: &HyperLogLog<REGISTERS>) {
for i in 0..REGISTERS {
if other.registers[i] > self.registers[i] {
self.registers[i] = other.registers[i];
}
}
}
pub fn memory_bytes(&self) -> usize {
mem::size_of::<Self>()
}
pub fn clear(&mut self) {
self.registers = [0u8; REGISTERS];
}
}
impl<const REGISTERS: usize> Default for HyperLogLog<REGISTERS> {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_empty() {
let hll: HyperLogLog<1024> = HyperLogLog::new();
assert_eq!(hll.estimate(), 0);
}
#[test]
fn test_single_element() {
let mut hll: HyperLogLog<1024> = HyperLogLog::new();
hll.add(42);
let est = hll.estimate();
assert!((1..=3).contains(&est));
}
fn splitmix64(v: u64) -> u64 {
let mut z = v.wrapping_add(0x9e3779b97f4a7c15);
z = (z ^ (z >> 30)).wrapping_mul(0xbf58476d1ce4e5b9);
z = (z ^ (z >> 27)).wrapping_mul(0x94d049bb133111eb);
z ^ (z >> 31)
}
#[test]
fn test_error_rate_10k_unique() {
let mut hll: HyperLogLog<1024> = HyperLogLog::new();
for i in 0..10_000u64 {
hll.add(splitmix64(i));
}
let est = hll.estimate();
let error = (est as f64 - 10_000.0).abs() / 10_000.0;
assert!(
error < 0.10,
"Error {} exceeds 10% (estimated={}, true=10000)",
error,
est
);
}
#[test]
fn test_merge() {
let mut hll_a: HyperLogLog<1024> = HyperLogLog::new();
let mut hll_b: HyperLogLog<1024> = HyperLogLog::new();
for i in 0..5000u64 {
hll_a.add(splitmix64(i));
}
for i in 5000..10_000u64 {
hll_b.add(splitmix64(i));
}
hll_a.merge(&hll_b);
let est = hll_a.estimate();
let error = (est as f64 - 10_000.0).abs() / 10_000.0;
assert!(
error < 0.10,
"Merge error {} exceeds 10% (estimated={})",
error,
est
);
}
#[test]
fn test_clear() {
let mut hll: HyperLogLog<1024> = HyperLogLog::new();
for i in 0..1000u64 {
hll.add(i.wrapping_mul(0x517cc1b727220a95));
}
assert!(hll.estimate() > 0);
hll.clear();
assert_eq!(hll.estimate(), 0);
}
#[test]
fn test_memory_size() {
let hll: HyperLogLog<1024> = HyperLogLog::new();
assert_eq!(hll.memory_bytes(), 1024);
}
}