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 7bf60b2  perf: Countmin  performance improvements  (#300)
7bf60b2 is described below

commit 7bf60b29e30afbfb402cbbc95bdcf6cb2946ffbb
Author: Patrick McGleenon <[email protected]>
AuthorDate: Fri Oct 23 06:34:47 2026 +0100

    perf: Countmin  performance improvements  (#300)
---
 CHANGELOG.md                                    |  2 +-
 benchmarks/countmin/estimate.rs                 | 58 ++++++++++++++++++++++
 benchmarks/countmin/mod.rs                      |  1 +
 datasketches/src/countmin/sketch.rs             | 64 ++++++++++++++++++++++++-
 tests-integration/tests/countmin_test/sketch.rs |  4 +-
 5 files changed, 125 insertions(+), 4 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2e4e3ca..6aeb2a9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -26,7 +26,7 @@ All significant changes to this project will be documented in 
this file.
 * Improve the readability of `Debug` output for HLL and CPC sketches and 
unions.
 * `KllSketch::quantile` and `ReqSketch::quantile` are faster at ranks `0.0` 
and `1.0`, with query work independent of the number of retained samples.
 * `BloomFilter::insert` is faster for integer and raw-byte inputs. 
`BloomFilter::contains_and_insert` is also faster when checking already-present 
integer values.
-* `CountMinSketch` updates are faster for integer and raw-byte inputs.
+* `CountMinSketch` updates and estimates are faster for all item types, with 
the largest gains for integer and raw-byte inputs. Counter placement and 
serialized bytes are unchanged.
 * `CountMinSketch::decay(1.0)` now takes constant time.
 * `CpcSketch` updates are faster for integer and raw-byte inputs.
 * `FrequentItemsSketch` updates are faster for integer and raw-byte keys.
diff --git a/benchmarks/countmin/estimate.rs b/benchmarks/countmin/estimate.rs
new file mode 100644
index 0000000..4a2777f
--- /dev/null
+++ b/benchmarks/countmin/estimate.rs
@@ -0,0 +1,58 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use datasketches::countmin::CountMinSketch;
+use datasketches::hash::value::raw_bytes;
+use divan::Bencher;
+use divan::black_box;
+use divan::counter::ItemsCount;
+
+use crate::hash_inputs::ITEMS;
+use crate::hash_inputs::bytes_32_values;
+
+#[divan::bench]
+fn u64(bencher: Bencher) {
+    let mut sketch = CountMinSketch::<u64>::new(4, 16_384).unwrap();
+    for value in 0..ITEMS as u64 {
+        sketch.update(value);
+    }
+
+    bencher.counter(ItemsCount::new(ITEMS)).bench_local(|| {
+        let mut total = 0u64;
+        for value in 0..ITEMS as u64 {
+            total = total.wrapping_add(sketch.estimate(black_box(value)));
+        }
+        black_box(total)
+    });
+}
+
+#[divan::bench]
+fn bytes_32(bencher: Bencher) {
+    let values = bytes_32_values();
+    let mut sketch = CountMinSketch::<u64>::new(4, 16_384).unwrap();
+    for value in &values {
+        sketch.update(raw_bytes::from_slice(value));
+    }
+
+    bencher.counter(ItemsCount::new(ITEMS)).bench_local(|| {
+        let mut total = 0u64;
+        for value in &values {
+            total = 
total.wrapping_add(sketch.estimate(raw_bytes::from_slice(black_box(value))));
+        }
+        black_box(total)
+    });
+}
diff --git a/benchmarks/countmin/mod.rs b/benchmarks/countmin/mod.rs
index 375b6d8..39f1404 100644
--- a/benchmarks/countmin/mod.rs
+++ b/benchmarks/countmin/mod.rs
@@ -16,4 +16,5 @@
 // under the License.
 
 mod decay;
+mod estimate;
 mod update;
diff --git a/datasketches/src/countmin/sketch.rs 
b/datasketches/src/countmin/sketch.rs
index 24a60e4..e69150c 100644
--- a/datasketches/src/countmin/sketch.rs
+++ b/datasketches/src/countmin/sketch.rs
@@ -55,6 +55,8 @@ 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,
 }
 
 impl<T: CountMinValue> CountMinSketch<T> {
@@ -511,6 +513,7 @@ impl<T: CountMinValue> CountMinSketch<T> {
             total_weight: T::ZERO,
             counts,
             hash_seeds,
+            bucket_multiplier: u128::MAX / u128::from(num_buckets) + 1,
         }
     }
 
@@ -518,7 +521,12 @@ impl<T: CountMinValue> CountMinSketch<T> {
         let mut hasher = MurmurHash3X64128::with_seed(seed);
         item.hash(&mut hasher);
         let (h1, _) = hasher.finish128();
-        (h1 % self.num_buckets as u64) as usize
+        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
+        }
     }
 }
 
@@ -626,3 +634,57 @@ 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::*;
+
+    #[test]
+    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}"
+            );
+        };
+        for d in [3, 5, 7, 1000, 20_000, 65_537, u32::MAX - 1, u32::MAX] {
+            for a in [
+                0,
+                1,
+                u64::MAX,
+                u64::MAX - 1,
+                1 << 63,
+                (1 << 32) - 1,
+                1 << 32,
+            ] {
+                check(d, a);
+            }
+            for _ in 0..1_000_000 {
+                x ^= x << 13;
+                x ^= x >> 7;
+                x ^= x << 17;
+                check(d, x);
+            }
+        }
+        for d in (3..u32::MAX).step_by(1_000_003) {
+            check(d, u64::MAX);
+            check(d, u64::from(d) * 12_345 + 7);
+        }
+    }
+}
diff --git a/tests-integration/tests/countmin_test/sketch.rs 
b/tests-integration/tests/countmin_test/sketch.rs
index ba2ab67..a5d3918 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(), 4200);
+    assert_eq!(sketch.estimated_size(), 4224);
 
     // The backing tables are allocated up front; updates do not grow the 
sketch.
     sketch.update("apple");
-    assert_eq!(sketch.estimated_size(), 4200);
+    assert_eq!(sketch.estimated_size(), 4224);
 }
 
 #[test]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to