diff --git a/Cargo.toml b/Cargo.toml index 88cf051..d70b239 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "hyperloglogplus" -version = "0.2.1" +version = "0.2.2" authors = ["Tasos Bakogiannis "] description = "HyperLogLog implementations." homepage = "https://github.com/tabac/hyperloglog.rs" @@ -13,6 +13,11 @@ edition = "2018" exclude = ["evaluation/*"] [features] +# Specify this feature flag to enable an optimization for `count()` that +# is based on Rust's `const_loop` feature. +# +# It requires a Rust compiler version 1.45.0 or higher. +const-loop = [] # Specify this feature flag to run also unit benchmarks # using the nightly compiler. bench-units = [] diff --git a/README.md b/README.md index 02262c1..bee136a 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,16 @@ Add to `Cargo.toml`: hyperloglogplus = "*" ``` +With Rust compiler version 1.45.0 or higher consider enabling the `const-loop` +feature for better performance, see [here](https://github.com/tabac/hyperloglog.rs/pull/3) +for more details. + +```toml +[dependencies] +hyperloglogplus = { version = "*", features = ["const-loop"] } +``` + + A simple example using HyperLogLog++ implementation: ```rust diff --git a/benches/hyperloglog.rs b/benches/hyperloglog.rs index c6ec758..7c83e0c 100644 --- a/benches/hyperloglog.rs +++ b/benches/hyperloglog.rs @@ -103,7 +103,7 @@ fn bench_count(c: &mut Criterion) { "hyperloglogplus_count_p16_below_thres", HyperLogLogPlus, 16, - 50_000 + 49_000 ]; bench_impls![ "hyperloglogplus_count_p16_above_thres", diff --git a/evaluation/Cargo.toml b/evaluation/Cargo.toml index 3501bfe..5745578 100644 --- a/evaluation/Cargo.toml +++ b/evaluation/Cargo.toml @@ -8,4 +8,4 @@ edition = "2018" rand = "0.7" clap = "3.0.0-beta.1" rayon = "1.1" -hyperloglogplus = { path = ".." } +hyperloglogplus = { path = "..", features = ["const-loop"] } diff --git a/evaluation/figures/hyperloglog.png b/evaluation/figures/hyperloglog.png index 8c2f059..5112d32 100644 Binary files a/evaluation/figures/hyperloglog.png and b/evaluation/figures/hyperloglog.png differ diff --git a/evaluation/figures/hyperloglog1e9.png b/evaluation/figures/hyperloglog1e9.png index d03e6c1..b450316 100644 Binary files a/evaluation/figures/hyperloglog1e9.png and b/evaluation/figures/hyperloglog1e9.png differ diff --git a/evaluation/figures/hyperloglogplus.png b/evaluation/figures/hyperloglogplus.png index ab0cfef..0e0da0b 100644 Binary files a/evaluation/figures/hyperloglogplus.png and b/evaluation/figures/hyperloglogplus.png differ diff --git a/evaluation/figures/hyperloglogplus1e9.png b/evaluation/figures/hyperloglogplus1e9.png index 6887991..de901eb 100644 Binary files a/evaluation/figures/hyperloglogplus1e9.png and b/evaluation/figures/hyperloglogplus1e9.png differ diff --git a/src/common.rs b/src/common.rs index 1b17792..e3dafe8 100644 --- a/src/common.rs +++ b/src/common.rs @@ -4,15 +4,14 @@ use serde::{Deserialize, Serialize}; macro_rules! registers_impls { ($len:expr, $ident:ident) => { // A Registers struct. - // - // Contains a `count` and a number of fixed size registers - // packed into `u32` integers. #[derive(Clone, Debug, Serialize, Deserialize)] pub struct $ident { // A buffer containing registers. buf: Vec, // The number of registers stored in buf. count: usize, + // The number of registers set to zero. + zeros: usize, } impl $ident { @@ -28,6 +27,7 @@ macro_rules! registers_impls { $ident { buf: vec![0; ceil(count, Self::COUNT_PER_WORD)], count: count, + zeros: count, } } @@ -45,6 +45,7 @@ macro_rules! registers_impls { } #[inline] // Returns the value of the Register at `index`. + #[allow(dead_code)] pub fn get(&self, index: usize) -> u32 { let (qu, rm) = ( index / Self::COUNT_PER_WORD, @@ -54,17 +55,32 @@ macro_rules! registers_impls { (self.buf[qu] >> (rm * Self::SIZE)) & Self::MASK } - #[inline] // Sets the value of the Register at `index` to `value`. - pub fn set(&mut self, index: usize, value: u32) { + #[inline] // Sets the value of the Register at `index` to `value`, + // if `value` is greater than its current value. + pub fn set_greater(&mut self, index: usize, value: u32) { let (qu, rm) = ( index / Self::COUNT_PER_WORD, index % Self::COUNT_PER_WORD, ); - let mask = Self::MASK << (rm * Self::SIZE); + let cur = (self.buf[qu] >> (rm * Self::SIZE)) & Self::MASK; - self.buf[qu] = - (self.buf[qu] & !mask) | (value << (rm * Self::SIZE)); + if value > cur { + if cur == 0 { + self.zeros -= 1; + self.buf[qu] |= (value << (rm * Self::SIZE)); + } else { + let mask = Self::MASK << (rm * Self::SIZE); + + self.buf[qu] = (self.buf[qu] & !mask) | + (value << (rm * Self::SIZE)); + } + } + } + + #[inline] + pub fn zeros(&self) -> usize { + self.zeros } #[inline] // Returns the size of the Registers in bytes @@ -84,6 +100,28 @@ registers_impls![5, Registers]; // used by HyperLogLog++ implementation. registers_impls![6, RegistersPlus]; +// An array containing all possible values used to calculate +// the "raw" sum. +// +// Instead of computing those values every time, look them up here. +// +// This is used only in the case the `const-loop` feature is enabled, +// it requires a Rust compiler version 1.45.0 or higher. +#[cfg(feature = "const-loop")] +const RAW: [f64; 1 << RegistersPlus::SIZE] = { + const COUNT: usize = 1 << RegistersPlus::SIZE; + + let mut raw = [0.0; COUNT]; + + let mut i = 0; + while i < COUNT { + raw[i] = 1.0 / (1u64 << i) as f64; + i += 1; + } + + raw +}; + // A trait for sharing common HyperLogLog related functionality between // different HyperLogLog implementations. pub trait HyperLogLogCommon { @@ -107,6 +145,30 @@ pub trait HyperLogLogCommon { (raw, zeros) } + #[cfg(not(feature = "const-loop"))] + #[inline] // Returns the "raw" HyperLogLog estimate as defined by + // P. Flajolet et al. for a given `precision`. + fn estimate_raw_plus(registers: I, count: usize) -> f64 + where + I: Iterator, + { + let raw: f64 = registers.map(|val| 1.0 / (1u64 << val) as f64).sum(); + + Self::alpha(count) * (count * count) as f64 / raw + } + + #[cfg(feature = "const-loop")] + #[inline] // Returns the "raw" HyperLogLog estimate as defined by + // P. Flajolet et al. for a given `precision`. + fn estimate_raw_plus(registers: I, count: usize) -> f64 + where + I: Iterator, + { + let raw: f64 = registers.map(|val| RAW[val as usize]).sum(); + + Self::alpha(count) * (count * count) as f64 / raw + } + #[inline] // Estimates the count of distinct elements using linear // counting. fn linear_count(count: usize, zeros: usize) -> f64 { @@ -164,15 +226,49 @@ mod tests { assert_eq!(registers.buf.len(), 2); - registers.set(1, 0b11); + registers.set_greater(1, 0b11); assert_eq!(registers.buf, vec![0b11000000, 0]); - registers.set(9, 0x7); + registers.set_greater(9, 0x7); assert_eq!(registers.buf, vec![0b11000000, 0x07000000]); } + #[test] + fn test_registers_set_greater() { + let mut registers: RegistersPlus = RegistersPlus::with_count(10); + + assert_eq!(registers.buf.len(), 2); + + assert_eq!(registers.zeros(), 10); + + registers.set_greater(1, 0); + + assert_eq!(registers.buf, vec![0, 0]); + assert_eq!(registers.zeros(), 10); + + registers.set_greater(1, 0b11); + + assert_eq!(registers.buf, vec![0b11000000, 0]); + assert_eq!(registers.zeros(), 9); + + registers.set_greater(9, 0x7); + + assert_eq!(registers.buf, vec![0b11000000, 0x07000000]); + assert_eq!(registers.zeros(), 8); + + registers.set_greater(1, 0b10); + + assert_eq!(registers.buf, vec![0b11000000, 0x07000000]); + assert_eq!(registers.zeros(), 8); + + registers.set_greater(9, 0x9); + + assert_eq!(registers.buf, vec![0b11000000, 0x09000000]); + assert_eq!(registers.zeros(), 8); + } + #[test] fn test_extract() { let num = 0b0010101110101101; diff --git a/src/hyperloglog.rs b/src/hyperloglog.rs index def7282..f62cdd4 100644 --- a/src/hyperloglog.rs +++ b/src/hyperloglog.rs @@ -96,9 +96,7 @@ where } for (i, val) in other.registers_iter().enumerate() { - if self.registers.get(i) < val { - self.registers.set(i, val); - } + self.registers.set_greater(i, val); } Ok(()) @@ -146,16 +144,16 @@ where let zeros: u32 = 1 + hash.leading_zeros(); // Update the register with the max leading zeros counts. - if zeros > self.registers.get(index) { - self.registers.set(index, zeros); - } + self.registers.set_greater(index, zeros); } /// Estimates the cardinality of the multiset. fn count(&mut self) -> f64 { // Calculate the raw estimate. - let (mut raw, zeros) = - Self::estimate_raw(self.registers.iter(), self.count); + let (mut raw, zeros) = ( + Self::estimate_raw_plus(self.registers.iter(), self.count), + self.registers.zeros(), + ); let two32 = (1u64 << 32) as f64; diff --git a/src/hyperloglogplus.rs b/src/hyperloglogplus.rs index 97ecc5c..7964245 100644 --- a/src/hyperloglogplus.rs +++ b/src/hyperloglogplus.rs @@ -145,17 +145,13 @@ where for hash_code in other.tmpset.iter() { let (zeros, index) = other.decode_hash(*hash_code); - if registers.get(index) < zeros { - registers.set(index, zeros); - } + registers.set_greater(index, zeros); } for hash_code in other.sparse.into_iter() { let (zeros, index) = other.decode_hash(hash_code); - if registers.get(index) < zeros { - registers.set(index, zeros); - } + registers.set_greater(index, zeros); } } } else { @@ -176,9 +172,7 @@ where let other_registers_iter = other.registers_iter().unwrap(); for (i, val) in other_registers_iter.enumerate() { - if registers.get(i) < val { - registers.set(i, val); - } + registers.set_greater(i, val); } } @@ -263,9 +257,7 @@ where for hash_code in self.sparse.into_iter() { let (zeros, index) = self.decode_hash(hash_code); - if zeros > registers.get(index) { - registers.set(index, zeros); - } + registers.set_greater(index, zeros); } self.registers = Some(registers); @@ -428,9 +420,7 @@ where let zeros: u32 = 1 + hash.leading_zeros(); // Update the register with the max leading zeros counts. - if zeros > registers.get(index) { - registers.set(index, zeros); - } + registers.set_greater(index, zeros); }, None => { // We use sparse representation. @@ -460,25 +450,42 @@ where Some(registers) => { // We use normal representation. - // Calculate the raw estimate. - let (mut raw, zeros) = - Self::estimate_raw(registers.iter(), self.counts.0); - - // Apply correction if required. - if raw <= 5.0 * self.counts.0 as f64 { - raw -= self.estimate_bias(raw); - } + let zeros = registers.zeros(); if zeros != 0 { let correction = Self::linear_count(self.counts.0, zeros); // Use linear counting only if value below threshold. if correction <= Self::threshold(self.precision) { - raw = correction; + correction + } else { + // Calculate the raw estimate. + let mut raw = Self::estimate_raw_plus( + registers.iter(), + self.counts.0, + ); + + // Apply correction if required. + if raw <= 5.0 * self.counts.0 as f64 { + raw -= self.estimate_bias(raw); + } + + raw + } + } else { + // Calculate the raw estimate. + let mut raw = Self::estimate_raw_plus( + registers.iter(), + self.counts.0, + ); + + // Apply correction if required. + if raw <= 5.0 * self.counts.0 as f64 { + raw -= self.estimate_bias(raw); } - } - raw + raw + } }, None => { // We use sparse representation.