This is an automated email from the ASF dual-hosted git repository.
tisonkun pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/datasketches-rust.git
The following commit(s) were added to refs/heads/main by this push:
new 28769d4 refactor(countmin): encapsulate bucket count state (#301)
28769d4 is described below
commit 28769d460589b2bae5b97bde4249d649b3326be0
Author: tison <[email protected]>
AuthorDate: Sat Oct 24 10:01:39 2026 +0800
refactor(countmin): encapsulate bucket count state (#301)
---
datasketches/src/countmin/sketch.rs | 86 ++++++++++++++-----------
tests-integration/tests/countmin_test/sketch.rs | 4 +-
2 files changed, 52 insertions(+), 38 deletions(-)
diff --git a/datasketches/src/countmin/sketch.rs
b/datasketches/src/countmin/sketch.rs
index e69150c..7e3c3ed 100644
--- a/datasketches/src/countmin/sketch.rs
+++ b/datasketches/src/countmin/sketch.rs
@@ -46,7 +46,7 @@ const MAX_TABLE_ENTRIES: usize = 1 << 30;
#[derive(Debug, Clone, PartialEq)]
pub struct CountMinSketch<T: CountMinValue> {
num_hashes: u8,
- num_buckets: u32,
+ buckets: BucketCount,
seed: u64,
seed_hash: u16,
// Every bucket satisfies |count| <= total_weight, so a checked total also
bounds bucket
@@ -55,8 +55,45 @@ pub struct CountMinSketch<T: CountMinValue> {
total_weight: T,
counts: Vec<T>,
hash_seeds: Vec<u64>,
- /// Multiplier for [`fast_remainder`], derived from `num_buckets`.
- bucket_multiplier: u128,
+}
+
+#[derive(Debug, Clone, PartialEq)]
+struct BucketCount {
+ count: u32,
+ multiplier: u128,
+}
+
+impl BucketCount {
+ fn new(count: u32) -> Self {
+ Self {
+ count,
+ multiplier: u128::MAX / u128::from(count) + 1,
+ }
+ }
+
+ #[inline]
+ fn index(&self, hash: u64) -> usize {
+ if self.count.is_power_of_two() {
+ // Same result as `%` for a power-of-two divisor.
+ (hash & (u64::from(self.count) - 1)) as usize
+ } else {
+ self.remainder(hash) as usize
+ }
+ }
+
+ /// Returns `a % self.count` without a hardware division, using Lemire's
direct remainder
+ /// ("Faster Remainder by Direct Computation", 2019).
+ ///
+ /// Exact for every `u64` input and every `self.count >= 2`.
+ #[inline]
+ fn remainder(&self, a: u64) -> u64 {
+ let low_bits = self.multiplier.wrapping_mul(u128::from(a));
+ // High 64 bits of the 192-bit product `low_bits * d`.
+ let d = u128::from(self.count);
+ let lo = u128::from(low_bits as u64) * d;
+ let hi = (low_bits >> 64) * d;
+ ((hi + (lo >> 64)) >> 64) as u64
+ }
}
impl<T: CountMinValue> CountMinSketch<T> {
@@ -116,7 +153,7 @@ impl<T: CountMinValue> CountMinSketch<T> {
/// Returns the number of buckets per hash function.
pub fn num_buckets(&self) -> u32 {
- self.num_buckets
+ self.buckets.count
}
/// Returns the seed used by the sketch.
@@ -131,7 +168,7 @@ impl<T: CountMinValue> CountMinSketch<T> {
/// Returns the relative error (epsilon) implied by the number of buckets.
pub fn relative_error(&self) -> f64 {
- std::f64::consts::E / self.num_buckets as f64
+ std::f64::consts::E / self.buckets.count as f64
}
/// Returns `true` if the total weight is zero.
@@ -223,7 +260,7 @@ impl<T: CountMinValue> CountMinSketch<T> {
.checked_abs()
.and_then(|weight| self.total_weight.checked_add(weight))
.expect("total absolute weight overflow");
- let num_buckets = self.num_buckets as usize;
+ let num_buckets = self.buckets.count as usize;
for (row, seed) in self.hash_seeds.iter().enumerate() {
let bucket = self.bucket_index(&item, *seed);
let index = row * num_buckets + bucket;
@@ -243,7 +280,7 @@ impl<T: CountMinValue> CountMinSketch<T> {
/// assert!(sketch.estimate("pear") >= 2);
/// ```
pub fn estimate<I: Hash>(&self, item: I) -> T {
- let num_buckets = self.num_buckets as usize;
+ let num_buckets = self.buckets.count as usize;
let mut min = T::MAX;
for (row, seed) in self.hash_seeds.iter().enumerate() {
let bucket = self.bucket_index(&item, *seed);
@@ -291,7 +328,7 @@ impl<T: CountMinValue> CountMinSketch<T> {
/// ```
pub fn merge(&mut self, other: &CountMinSketch<T>) -> Result<(), Error> {
if self.num_hashes != other.num_hashes
- || self.num_buckets != other.num_buckets
+ || self.buckets.count != other.buckets.count
|| self.seed != other.seed
{
return Err(Error::invalid_argument(
@@ -340,7 +377,7 @@ impl<T: CountMinValue> CountMinSketch<T> {
bytes.write_u8(if self.is_empty() { FLAGS_IS_EMPTY } else { 0 });
bytes.write_u32_le(0); // unused
- bytes.write_u32_le(self.num_buckets);
+ bytes.write_u32_le(self.buckets.count);
bytes.write_u8(self.num_hashes);
debug_assert_eq!(
self.seed_hash,
@@ -507,13 +544,12 @@ impl<T: CountMinValue> CountMinSketch<T> {
let hash_seeds = make_hash_seeds(seed, num_hashes);
CountMinSketch {
num_hashes,
- num_buckets,
+ buckets: BucketCount::new(num_buckets),
seed,
seed_hash,
total_weight: T::ZERO,
counts,
hash_seeds,
- bucket_multiplier: u128::MAX / u128::from(num_buckets) + 1,
}
}
@@ -521,12 +557,7 @@ impl<T: CountMinValue> CountMinSketch<T> {
let mut hasher = MurmurHash3X64128::with_seed(seed);
item.hash(&mut hasher);
let (h1, _) = hasher.finish128();
- if self.num_buckets.is_power_of_two() {
- // Same result as `%` for a power-of-two divisor.
- (h1 & (u64::from(self.num_buckets) - 1)) as usize
- } else {
- fast_remainder(h1, self.bucket_multiplier, self.num_buckets) as
usize
- }
+ self.buckets.index(h1)
}
}
@@ -635,19 +666,6 @@ fn make_hash_seeds(seed: u64, num_hashes: u8) -> Vec<u64> {
seeds
}
-/// Returns `a % d` without a hardware division, using Lemire's direct
remainder
-/// ("Faster Remainder by Direct Computation", 2019) with `multiplier` =
-/// `u128::MAX / d + 1`. Exact for every `u64` `a` and `u32` `d >= 2`.
-#[inline]
-fn fast_remainder(a: u64, multiplier: u128, d: u32) -> u64 {
- let low_bits = multiplier.wrapping_mul(u128::from(a));
- // High 64 bits of the 192-bit product `low_bits * d`.
- let d = u128::from(d);
- let lo = u128::from(low_bits as u64) * d;
- let hi = (low_bits >> 64) * d;
- ((hi + (lo >> 64)) >> 64) as u64
-}
-
#[cfg(test)]
mod tests {
use super::*;
@@ -656,12 +674,8 @@ mod tests {
fn test_fast_remainder_matches_remainder() {
let mut x = 0x9E37_79B9_7F4A_7C15u64;
let check = |d: u32, a: u64| {
- let multiplier = u128::MAX / u128::from(d) + 1;
- assert_eq!(
- fast_remainder(a, multiplier, d),
- a % u64::from(d),
- "a={a} d={d}"
- );
+ let buckets = BucketCount::new(d);
+ assert_eq!(buckets.remainder(a), a % u64::from(d), "a={a} d={d}");
};
for d in [3, 5, 7, 1000, 20_000, 65_537, u32::MAX - 1, u32::MAX] {
for a in [
diff --git a/tests-integration/tests/countmin_test/sketch.rs
b/tests-integration/tests/countmin_test/sketch.rs
index a5d3918..9edae10 100644
--- a/tests-integration/tests/countmin_test/sketch.rs
+++ b/tests-integration/tests/countmin_test/sketch.rs
@@ -445,11 +445,11 @@ fn test_increment_single_key_like_rust_count_min_sketch()
{
#[test]
fn test_estimated_size() {
let mut sketch = CountMinSketch::<i64>::new(4, 128).unwrap();
- assert_eq!(sketch.estimated_size(), 4224);
+ assert_eq!(sketch.estimated_size(), 4240);
// The backing tables are allocated up front; updates do not grow the
sketch.
sketch.update("apple");
- assert_eq!(sketch.estimated_size(), 4224);
+ assert_eq!(sketch.estimated_size(), 4240);
}
#[test]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]