From f079c98f3f510575f26917717562423ace8064aa Mon Sep 17 00:00:00 2001 From: tison Date: Wed, 2 Sep 2026 20:18:49 +0800 Subject: [PATCH 1/4] perf: reduce sketch hashing overhead --- CHANGELOG.md | 4 ++ benchmarks/Cargo.toml | 8 ++- benchmarks/bloom/mod.rs | 18 +++++ benchmarks/bloom/update.rs | 58 +++++++++++++++ benchmarks/cpc/mod.rs | 1 + benchmarks/cpc/update.rs | 54 ++++++++++++++ benchmarks/main.rs | 1 + datasketches/src/hash/mod.rs | 16 ++--- datasketches/src/hash/murmurhash.rs | 70 +++++++++---------- .../src/hash/value/canonical_float.rs | 2 + datasketches/src/hash/value/mod.rs | 1 + datasketches/src/hash/value/natural_extend.rs | 1 + datasketches/src/hash/value/raw_bytes.rs | 1 + datasketches/src/hash/value/sign_extend.rs | 1 + datasketches/src/hash/xxhash.rs | 18 +++-- 15 files changed, 203 insertions(+), 51 deletions(-) create mode 100644 benchmarks/bloom/mod.rs create mode 100644 benchmarks/bloom/update.rs create mode 100644 benchmarks/cpc/update.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index f65b2678..c944d78b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,10 @@ All significant changes to this project will be documented in this file. * Improve truncated-input diagnostics across sketch deserializers. +### Performance improvements + +* Reduce MurmurHash3 and XXHash64 overhead in hash-backed sketch updates. Local end-to-end benchmarks show roughly 20–32% faster `u64` updates for Bloom and CPC sketches and roughly 21% faster Bloom updates for 32-byte inputs. + ### Bug fixes * T-Digest deserialization now rejects unknown or conflicting flags, reversed extrema, out-of-range values, unsorted centroids, and non-empty images without stored values. diff --git a/benchmarks/Cargo.toml b/benchmarks/Cargo.toml index 3bbbda5c..f1efb7ae 100644 --- a/benchmarks/Cargo.toml +++ b/benchmarks/Cargo.toml @@ -23,7 +23,13 @@ edition.workspace = true rust-version.workspace = true [dev-dependencies] -datasketches = { workspace = true, features = ["cpc", "kll", "req", "tdigest"] } +datasketches = { workspace = true, features = [ + "bloom", + "cpc", + "kll", + "req", + "tdigest", +] } divan = { workspace = true } rand = { workspace = true } diff --git a/benchmarks/bloom/mod.rs b/benchmarks/bloom/mod.rs new file mode 100644 index 00000000..1dea33be --- /dev/null +++ b/benchmarks/bloom/mod.rs @@ -0,0 +1,18 @@ +// 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. + +mod update; diff --git a/benchmarks/bloom/update.rs b/benchmarks/bloom/update.rs new file mode 100644 index 00000000..98050a4a --- /dev/null +++ b/benchmarks/bloom/update.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::bloom::BloomFilterBuilder; +use datasketches::hash::value::raw_bytes; +use divan::Bencher; +use divan::black_box; +use divan::counter::ItemsCount; + +const ITEMS: usize = 10_000; + +#[divan::bench] +fn u64(bencher: Bencher) { + bencher.counter(ItemsCount::new(ITEMS)).bench_local(|| { + let mut filter = BloomFilterBuilder::with_accuracy(ITEMS as u64, 0.01) + .build() + .unwrap(); + for value in 0..ITEMS as u64 { + filter.insert(black_box(value)); + } + black_box(filter) + }); +} + +#[divan::bench] +fn bytes_32(bencher: Bencher) { + let values = (0..ITEMS) + .map(|value| { + let mut bytes = [0; 32]; + bytes[..8].copy_from_slice(&(value as u64).to_le_bytes()); + bytes + }) + .collect::>(); + + bencher.counter(ItemsCount::new(ITEMS)).bench_local(|| { + let mut filter = BloomFilterBuilder::with_accuracy(ITEMS as u64, 0.01) + .build() + .unwrap(); + for value in &values { + filter.insert(raw_bytes::from_slice(black_box(value))); + } + black_box(filter) + }); +} diff --git a/benchmarks/cpc/mod.rs b/benchmarks/cpc/mod.rs index 431ad1f9..13c2486b 100644 --- a/benchmarks/cpc/mod.rs +++ b/benchmarks/cpc/mod.rs @@ -16,3 +16,4 @@ // under the License. mod serde; +mod update; diff --git a/benchmarks/cpc/update.rs b/benchmarks/cpc/update.rs new file mode 100644 index 00000000..40b6213f --- /dev/null +++ b/benchmarks/cpc/update.rs @@ -0,0 +1,54 @@ +// 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::cpc::CpcSketch; +use datasketches::hash::value::raw_bytes; +use divan::Bencher; +use divan::black_box; +use divan::counter::ItemsCount; + +const ITEMS: usize = 10_000; + +#[divan::bench] +fn u64(bencher: Bencher) { + bencher.counter(ItemsCount::new(ITEMS)).bench_local(|| { + let mut sketch = CpcSketch::new(11).unwrap(); + for value in 0..ITEMS as u64 { + sketch.update(black_box(value)); + } + black_box(sketch) + }); +} + +#[divan::bench] +fn bytes_32(bencher: Bencher) { + let values = (0..ITEMS) + .map(|value| { + let mut bytes = [0; 32]; + bytes[..8].copy_from_slice(&(value as u64).to_le_bytes()); + bytes + }) + .collect::>(); + + bencher.counter(ItemsCount::new(ITEMS)).bench_local(|| { + let mut sketch = CpcSketch::new(11).unwrap(); + for value in &values { + sketch.update(raw_bytes::from_slice(black_box(value))); + } + black_box(sketch) + }); +} diff --git a/benchmarks/main.rs b/benchmarks/main.rs index 11c99621..ad2b507a 100644 --- a/benchmarks/main.rs +++ b/benchmarks/main.rs @@ -20,6 +20,7 @@ use divan::AllocProfiler; #[global_allocator] static ALLOC: AllocProfiler = AllocProfiler::system(); +mod bloom; mod cpc; mod kll; mod req; diff --git a/datasketches/src/hash/mod.rs b/datasketches/src/hash/mod.rs index c5c26c43..2084902b 100644 --- a/datasketches/src/hash/mod.rs +++ b/datasketches/src/hash/mod.rs @@ -82,11 +82,12 @@ pub(crate) use self::seed::*; ))] pub(crate) const DEFAULT_UPDATE_SEED: u64 = 9001; -/// Reads an u64 from a byte slice in little-endian order. -/// -/// # Panics -/// -/// Panics if `bytes.len()` is greater than 8. +#[cfg(feature = "bloom")] +#[inline(always)] +fn read_u32_le(bytes: &[u8]) -> u32 { + u32::from_le_bytes(bytes.try_into().expect("four-byte hash input")) +} + #[cfg(any( feature = "bloom", feature = "countmin", @@ -96,8 +97,7 @@ pub(crate) const DEFAULT_UPDATE_SEED: u64 = 9001; feature = "theta", feature = "tuple", ))] +#[inline(always)] fn read_u64_le(bytes: &[u8]) -> u64 { - let mut buf = [0u8; 8]; - buf[..bytes.len()].copy_from_slice(bytes); - u64::from_le_bytes(buf) + u64::from_le_bytes(bytes.try_into().expect("eight-byte hash input")) } diff --git a/datasketches/src/hash/murmurhash.rs b/datasketches/src/hash/murmurhash.rs index 1e0a3833..5f62a402 100644 --- a/datasketches/src/hash/murmurhash.rs +++ b/datasketches/src/hash/murmurhash.rs @@ -35,6 +35,7 @@ pub struct MurmurHash3X64128 { } impl MurmurHash3X64128 { + #[inline(always)] pub fn with_seed(seed: u64) -> Self { MurmurHash3X64128 { h1: seed, @@ -45,18 +46,18 @@ impl MurmurHash3X64128 { } } + #[inline(always)] pub fn finish128(&self) -> (u64, u64) { let mut h1 = self.h1; let mut h2 = self.h2; - let total = self.total + self.buf_len as u64; let rem = self.buf_len; // tail if rem > 0 { if rem > 8 { // read k2 little endian - let mut k2 = read_u64_le(&self.buf[8..rem]); + let mut k2 = read_u64_le_partial(&self.buf[8..rem]); // mix k2 k2 = k2.wrapping_mul(C2); k2 = k2.rotate_left(33); @@ -66,7 +67,7 @@ impl MurmurHash3X64128 { // read k1 little endian let k1_len = rem.min(8); - let mut k1 = read_u64_le(&self.buf[..k1_len]); + let mut k1 = read_u64_le_partial(&self.buf[..k1_len]); // mix k1 k1 = k1.wrapping_mul(C1); k1 = k1.rotate_left(31); @@ -74,8 +75,8 @@ impl MurmurHash3X64128 { h1 ^= k1; } - h1 ^= total; - h2 ^= total; + h1 ^= self.total; + h2 ^= self.total; h1 = h1.wrapping_add(h2); h2 = h2.wrapping_add(h1); h1 = fmix64(h1); @@ -85,7 +86,7 @@ impl MurmurHash3X64128 { (h1, h2) } - #[inline] + #[inline(always)] fn update(&mut self, mut k1: u64, mut k2: u64) { // k1 *= c1; k1 = MURMUR3_ROTL64(k1, 31); k1 *= c2; out.h1 ^= k1; k1 = k1.wrapping_mul(C1); @@ -108,9 +109,6 @@ impl MurmurHash3X64128 { self.h2 = self.h2.rotate_left(31); self.h2 = self.h2.wrapping_add(self.h1); self.h2 = self.h2.wrapping_mul(5).wrapping_add(0x38495ab5); - - // accumulate total length - self.total += 16; } } @@ -121,54 +119,54 @@ impl Default for MurmurHash3X64128 { } impl Hasher for MurmurHash3X64128 { + #[inline(always)] fn finish(&self) -> u64 { self.finish128().0 } + #[inline(always)] fn write(&mut self, mut bytes: &[u8]) { - if self.buf_len + bytes.len() < 16 { - self.buf[self.buf_len..self.buf_len + bytes.len()].copy_from_slice(bytes); - self.buf_len += bytes.len(); - return; - } + self.total = self.total.wrapping_add(bytes.len() as u64); if self.buf_len != 0 { - let wanted = 16 - self.buf_len; - self.buf[self.buf_len..].copy_from_slice(&bytes[..wanted]); + let copied = (16 - self.buf_len).min(bytes.len()); + self.buf[self.buf_len..self.buf_len + copied].copy_from_slice(&bytes[..copied]); + self.buf_len += copied; + bytes = &bytes[copied..]; + + if self.buf_len < 16 { + return; + } let k1 = read_u64_le(&self.buf[0..8]); let k2 = read_u64_le(&self.buf[8..16]); self.update(k1, k2); - - bytes = &bytes[wanted..]; self.buf_len = 0; } - // Number of full 128-bit blocks of 16 bytes. - // Possible exclusion of a remainder of up to 15 bytes. - let blocks = bytes.len() >> 4; // bytes / 16 - - // Process the 128-bit blocks (the body) into the hash - for i in 0..blocks { - let lo = i << 4; - let mi = lo + 8; - let hi = mi + 8; - let k1 = read_u64_le(&bytes[lo..mi]); - let k2 = read_u64_le(&bytes[mi..hi]); + while bytes.len() >= 16 { + let k1 = read_u64_le(&bytes[..8]); + let k2 = read_u64_le(&bytes[8..16]); self.update(k1, k2); + bytes = &bytes[16..]; } - // remain bytes - let len = bytes.len() % 16; - if len > 0 { - self.buf[0..len].copy_from_slice(&bytes[blocks << 4..]); - self.buf_len = len; - } + self.buf[..bytes.len()].copy_from_slice(bytes); + self.buf_len = bytes.len(); + } +} + +#[inline(always)] +fn read_u64_le_partial(bytes: &[u8]) -> u64 { + let mut value = 0; + for (index, &byte) in bytes.iter().enumerate() { + value |= u64::from(byte) << (index * 8); } + value } /// Finalization mix: force all bits of a hash block to avalanche. -#[inline] +#[inline(always)] fn fmix64(mut k: u64) -> u64 { k ^= k >> 33; k = k.wrapping_mul(0xff51afd7ed558ccd); diff --git a/datasketches/src/hash/value/canonical_float.rs b/datasketches/src/hash/value/canonical_float.rs index 4c29b5bd..06edd985 100644 --- a/datasketches/src/hash/value/canonical_float.rs +++ b/datasketches/src/hash/value/canonical_float.rs @@ -97,6 +97,7 @@ pub fn from_f64(v: f64) -> CanonicalFloat { } impl HashStrategy for CanonicalFloatStrategy { + #[inline(always)] fn hash(value: &f32, state: &mut H) { let value = *value as f64; let canonical_value = from_f64(value); @@ -105,6 +106,7 @@ impl HashStrategy for CanonicalFloatStrategy { } impl HashStrategy for CanonicalFloatStrategy { + #[inline(always)] fn hash(value: &f64, state: &mut H) { let canonical = if value.is_nan() { // Java's Double.doubleToLongBits() NaN value. diff --git a/datasketches/src/hash/value/mod.rs b/datasketches/src/hash/value/mod.rs index ee42e19a..88d87e7e 100644 --- a/datasketches/src/hash/value/mod.rs +++ b/datasketches/src/hash/value/mod.rs @@ -166,6 +166,7 @@ impl fmt::Display for Value { } impl> Hash for Value { + #[inline(always)] fn hash(&self, state: &mut H) { S::hash(&self.value, state); } diff --git a/datasketches/src/hash/value/natural_extend.rs b/datasketches/src/hash/value/natural_extend.rs index 1b2ec4d1..981c8b1a 100644 --- a/datasketches/src/hash/value/natural_extend.rs +++ b/datasketches/src/hash/value/natural_extend.rs @@ -133,6 +133,7 @@ pub fn from_u32(v: u32) -> NaturalExtend { macro_rules! impl_natural_extend { ($t:ty, |$v:ident| $extended:expr) => { impl HashStrategy<$t> for NaturalExtendStrategy { + #[inline(always)] fn hash(value: &$t, state: &mut H) { let $v = *value; let extended = $extended; diff --git a/datasketches/src/hash/value/raw_bytes.rs b/datasketches/src/hash/value/raw_bytes.rs index 2b41735b..0c48139f 100644 --- a/datasketches/src/hash/value/raw_bytes.rs +++ b/datasketches/src/hash/value/raw_bytes.rs @@ -129,6 +129,7 @@ pub fn from_str(v: &str) -> RawBytes<&str> { macro_rules! impl_raw_bytes { ($t:ty, |$v:ident| $as_slice:expr) => { impl HashStrategy<$t> for RawBytesStrategy { + #[inline(always)] fn hash(value: &$t, state: &mut H) { let $v = value; let slice = $as_slice; diff --git a/datasketches/src/hash/value/sign_extend.rs b/datasketches/src/hash/value/sign_extend.rs index 561fbd1c..cefcafea 100644 --- a/datasketches/src/hash/value/sign_extend.rs +++ b/datasketches/src/hash/value/sign_extend.rs @@ -160,6 +160,7 @@ pub fn from_u32(v: u32) -> SignExtend { macro_rules! impl_sign_extend { ($t:ty, |$v:ident| $extended:expr) => { impl HashStrategy<$t> for SignExtendStrategy { + #[inline(always)] fn hash(value: &$t, state: &mut H) { let $v = *value; let extended = $extended as u64; diff --git a/datasketches/src/hash/xxhash.rs b/datasketches/src/hash/xxhash.rs index 04c3e473..1ea55035 100644 --- a/datasketches/src/hash/xxhash.rs +++ b/datasketches/src/hash/xxhash.rs @@ -17,6 +17,7 @@ use std::hash::Hasher; +use crate::hash::read_u32_le; use crate::hash::read_u64_le; const DEFAULT_SEED: u64 = 0; @@ -43,6 +44,7 @@ pub struct XxHash64 { } impl XxHash64 { + #[inline(always)] pub fn with_seed(seed: u64) -> Self { XxHash64 { seed, @@ -56,6 +58,7 @@ impl XxHash64 { } } + #[inline(always)] pub fn finish64(&self) -> u64 { let mut hash = if self.total_len >= 32 { let mut acc = self @@ -88,8 +91,8 @@ impl XxHash64 { } if idx + 4 <= buf.len() { - let k1 = read_u64_le(&buf[idx..idx + 4]); - hash ^= k1.wrapping_mul(P1); + let k1 = read_u32_le(&buf[idx..idx + 4]); + hash ^= u64::from(k1).wrapping_mul(P1); hash = hash.rotate_left(23).wrapping_mul(P2).wrapping_add(P3); idx += 4; } @@ -105,6 +108,7 @@ impl XxHash64 { } #[allow(dead_code)] + #[inline(always)] pub fn hash_u64(input: u64, seed: u64) -> u64 { let mut hash = seed.wrapping_add(P5).wrapping_add(8); let mut k1 = input; @@ -116,7 +120,7 @@ impl XxHash64 { finalize(hash) } - #[inline] + #[inline(always)] fn update(&mut self, chunk: &[u8]) { self.v1 = round(self.v1, read_u64_le(&chunk[0..8])); self.v2 = round(self.v2, read_u64_le(&chunk[8..16])); @@ -132,10 +136,12 @@ impl Default for XxHash64 { } impl Hasher for XxHash64 { + #[inline(always)] fn finish(&self) -> u64 { self.finish64() } + #[inline(always)] fn write(&mut self, bytes: &[u8]) { self.total_len = self.total_len.wrapping_add(bytes.len() as u64); @@ -169,14 +175,14 @@ impl Hasher for XxHash64 { } } -#[inline] +#[inline(always)] fn round(mut acc: u64, input: u64) -> u64 { acc = acc.wrapping_add(input.wrapping_mul(P2)); acc = acc.rotate_left(31); acc.wrapping_mul(P1) } -#[inline] +#[inline(always)] fn merge_round(mut acc: u64, val: u64) -> u64 { let mut v = val; v = v.wrapping_mul(P2); @@ -186,7 +192,7 @@ fn merge_round(mut acc: u64, val: u64) -> u64 { acc.wrapping_mul(P1).wrapping_add(P4) } -#[inline] +#[inline(always)] fn finalize(mut hash: u64) -> u64 { hash ^= hash >> 33; hash = hash.wrapping_mul(P2); From f644658c09a56529224d4bc13851b39a2664eb3a Mon Sep 17 00:00:00 2001 From: tison Date: Wed, 2 Sep 2026 20:24:47 +0800 Subject: [PATCH 2/4] docs: simplify hash performance changelog --- CHANGELOG.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c944d78b..c9d6f876 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,10 +15,7 @@ All significant changes to this project will be documented in this file. ### Improvements * Improve truncated-input diagnostics across sketch deserializers. - -### Performance improvements - -* Reduce MurmurHash3 and XXHash64 overhead in hash-backed sketch updates. Local end-to-end benchmarks show roughly 20–32% faster `u64` updates for Bloom and CPC sketches and roughly 21% faster Bloom updates for 32-byte inputs. +* Improve hash-backed sketch update performance, especially for integer inputs and Bloom filter raw-byte inputs. ### Bug fixes From cc3c647ce3461c32974f7a2ee56abeeadd34d109 Mon Sep 17 00:00:00 2001 From: tison Date: Wed, 2 Sep 2026 20:30:21 +0800 Subject: [PATCH 3/4] bench: cover hash-backed sketch updates --- benchmarks/Cargo.toml | 5 +++ benchmarks/bloom/update.rs | 11 ++----- benchmarks/countmin/mod.rs | 18 +++++++++++ benchmarks/countmin/update.rs | 49 ++++++++++++++++++++++++++++++ benchmarks/cpc/update.rs | 11 ++----- benchmarks/frequencies/mod.rs | 18 +++++++++++ benchmarks/frequencies/update.rs | 50 ++++++++++++++++++++++++++++++ benchmarks/hash_inputs.rs | 28 +++++++++++++++++ benchmarks/hll/mod.rs | 18 +++++++++++ benchmarks/hll/update.rs | 50 ++++++++++++++++++++++++++++++ benchmarks/main.rs | 6 ++++ benchmarks/theta/mod.rs | 18 +++++++++++ benchmarks/theta/update.rs | 49 ++++++++++++++++++++++++++++++ benchmarks/tuple/mod.rs | 18 +++++++++++ benchmarks/tuple/update.rs | 52 ++++++++++++++++++++++++++++++++ 15 files changed, 385 insertions(+), 16 deletions(-) create mode 100644 benchmarks/countmin/mod.rs create mode 100644 benchmarks/countmin/update.rs create mode 100644 benchmarks/frequencies/mod.rs create mode 100644 benchmarks/frequencies/update.rs create mode 100644 benchmarks/hash_inputs.rs create mode 100644 benchmarks/hll/mod.rs create mode 100644 benchmarks/hll/update.rs create mode 100644 benchmarks/theta/mod.rs create mode 100644 benchmarks/theta/update.rs create mode 100644 benchmarks/tuple/mod.rs create mode 100644 benchmarks/tuple/update.rs diff --git a/benchmarks/Cargo.toml b/benchmarks/Cargo.toml index f1efb7ae..25af32a6 100644 --- a/benchmarks/Cargo.toml +++ b/benchmarks/Cargo.toml @@ -25,10 +25,15 @@ rust-version.workspace = true [dev-dependencies] datasketches = { workspace = true, features = [ "bloom", + "countmin", "cpc", + "frequencies", + "hll", "kll", "req", "tdigest", + "theta", + "tuple", ] } divan = { workspace = true } rand = { workspace = true } diff --git a/benchmarks/bloom/update.rs b/benchmarks/bloom/update.rs index 98050a4a..2aa6b810 100644 --- a/benchmarks/bloom/update.rs +++ b/benchmarks/bloom/update.rs @@ -21,7 +21,8 @@ use divan::Bencher; use divan::black_box; use divan::counter::ItemsCount; -const ITEMS: usize = 10_000; +use crate::hash_inputs::ITEMS; +use crate::hash_inputs::bytes_32_values; #[divan::bench] fn u64(bencher: Bencher) { @@ -38,13 +39,7 @@ fn u64(bencher: Bencher) { #[divan::bench] fn bytes_32(bencher: Bencher) { - let values = (0..ITEMS) - .map(|value| { - let mut bytes = [0; 32]; - bytes[..8].copy_from_slice(&(value as u64).to_le_bytes()); - bytes - }) - .collect::>(); + let values = bytes_32_values(); bencher.counter(ItemsCount::new(ITEMS)).bench_local(|| { let mut filter = BloomFilterBuilder::with_accuracy(ITEMS as u64, 0.01) diff --git a/benchmarks/countmin/mod.rs b/benchmarks/countmin/mod.rs new file mode 100644 index 00000000..1dea33be --- /dev/null +++ b/benchmarks/countmin/mod.rs @@ -0,0 +1,18 @@ +// 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. + +mod update; diff --git a/benchmarks/countmin/update.rs b/benchmarks/countmin/update.rs new file mode 100644 index 00000000..f25af1ef --- /dev/null +++ b/benchmarks/countmin/update.rs @@ -0,0 +1,49 @@ +// 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) { + bencher.counter(ItemsCount::new(ITEMS)).bench_local(|| { + let mut sketch = CountMinSketch::::new(4, 16_384).unwrap(); + for value in 0..ITEMS as u64 { + sketch.update(black_box(value)); + } + black_box(sketch) + }); +} + +#[divan::bench] +fn bytes_32(bencher: Bencher) { + let values = bytes_32_values(); + + bencher.counter(ItemsCount::new(ITEMS)).bench_local(|| { + let mut sketch = CountMinSketch::::new(4, 16_384).unwrap(); + for value in &values { + sketch.update(raw_bytes::from_slice(black_box(value))); + } + black_box(sketch) + }); +} diff --git a/benchmarks/cpc/update.rs b/benchmarks/cpc/update.rs index 40b6213f..ba532b81 100644 --- a/benchmarks/cpc/update.rs +++ b/benchmarks/cpc/update.rs @@ -21,7 +21,8 @@ use divan::Bencher; use divan::black_box; use divan::counter::ItemsCount; -const ITEMS: usize = 10_000; +use crate::hash_inputs::ITEMS; +use crate::hash_inputs::bytes_32_values; #[divan::bench] fn u64(bencher: Bencher) { @@ -36,13 +37,7 @@ fn u64(bencher: Bencher) { #[divan::bench] fn bytes_32(bencher: Bencher) { - let values = (0..ITEMS) - .map(|value| { - let mut bytes = [0; 32]; - bytes[..8].copy_from_slice(&(value as u64).to_le_bytes()); - bytes - }) - .collect::>(); + let values = bytes_32_values(); bencher.counter(ItemsCount::new(ITEMS)).bench_local(|| { let mut sketch = CpcSketch::new(11).unwrap(); diff --git a/benchmarks/frequencies/mod.rs b/benchmarks/frequencies/mod.rs new file mode 100644 index 00000000..1dea33be --- /dev/null +++ b/benchmarks/frequencies/mod.rs @@ -0,0 +1,18 @@ +// 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. + +mod update; diff --git a/benchmarks/frequencies/update.rs b/benchmarks/frequencies/update.rs new file mode 100644 index 00000000..9026b293 --- /dev/null +++ b/benchmarks/frequencies/update.rs @@ -0,0 +1,50 @@ +// 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::frequencies::FrequentItemsSketch; +use datasketches::hash::value::raw_bytes; +use datasketches::hash::value::raw_bytes::RawBytes; +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) { + bencher.counter(ItemsCount::new(ITEMS)).bench_local(|| { + let mut sketch = FrequentItemsSketch::::new(16_384).unwrap(); + for value in 0..ITEMS as u64 { + sketch.update(black_box(value)); + } + black_box(sketch) + }); +} + +#[divan::bench] +fn bytes_32(bencher: Bencher) { + let values = bytes_32_values(); + + bencher.counter(ItemsCount::new(ITEMS)).bench_local(|| { + let mut sketch = FrequentItemsSketch::>::new(16_384).unwrap(); + for value in &values { + sketch.update(raw_bytes::from_slice(black_box(value))); + } + black_box(sketch) + }); +} diff --git a/benchmarks/hash_inputs.rs b/benchmarks/hash_inputs.rs new file mode 100644 index 00000000..073edf4a --- /dev/null +++ b/benchmarks/hash_inputs.rs @@ -0,0 +1,28 @@ +// 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. + +pub const ITEMS: usize = 10_000; + +pub fn bytes_32_values() -> Vec<[u8; 32]> { + (0..ITEMS) + .map(|value| { + let mut bytes = [0; 32]; + bytes[..8].copy_from_slice(&(value as u64).to_le_bytes()); + bytes + }) + .collect() +} diff --git a/benchmarks/hll/mod.rs b/benchmarks/hll/mod.rs new file mode 100644 index 00000000..1dea33be --- /dev/null +++ b/benchmarks/hll/mod.rs @@ -0,0 +1,18 @@ +// 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. + +mod update; diff --git a/benchmarks/hll/update.rs b/benchmarks/hll/update.rs new file mode 100644 index 00000000..88a335c1 --- /dev/null +++ b/benchmarks/hll/update.rs @@ -0,0 +1,50 @@ +// 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::hash::value::raw_bytes; +use datasketches::hll::HllSketch; +use datasketches::hll::HllType; +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) { + bencher.counter(ItemsCount::new(ITEMS)).bench_local(|| { + let mut sketch = HllSketch::new(12, HllType::Hll8).unwrap(); + for value in 0..ITEMS as u64 { + sketch.update(black_box(value)); + } + black_box(sketch) + }); +} + +#[divan::bench] +fn bytes_32(bencher: Bencher) { + let values = bytes_32_values(); + + bencher.counter(ItemsCount::new(ITEMS)).bench_local(|| { + let mut sketch = HllSketch::new(12, HllType::Hll8).unwrap(); + for value in &values { + sketch.update(raw_bytes::from_slice(black_box(value))); + } + black_box(sketch) + }); +} diff --git a/benchmarks/main.rs b/benchmarks/main.rs index ad2b507a..ee288034 100644 --- a/benchmarks/main.rs +++ b/benchmarks/main.rs @@ -21,10 +21,16 @@ use divan::AllocProfiler; static ALLOC: AllocProfiler = AllocProfiler::system(); mod bloom; +mod countmin; mod cpc; +mod frequencies; +mod hash_inputs; +mod hll; mod kll; mod req; mod tdigest; +mod theta; +mod tuple; fn main() { divan::main(); diff --git a/benchmarks/theta/mod.rs b/benchmarks/theta/mod.rs new file mode 100644 index 00000000..1dea33be --- /dev/null +++ b/benchmarks/theta/mod.rs @@ -0,0 +1,18 @@ +// 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. + +mod update; diff --git a/benchmarks/theta/update.rs b/benchmarks/theta/update.rs new file mode 100644 index 00000000..ca9b94f6 --- /dev/null +++ b/benchmarks/theta/update.rs @@ -0,0 +1,49 @@ +// 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::hash::value::raw_bytes; +use datasketches::theta::ThetaSketchBuilder; +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) { + bencher.counter(ItemsCount::new(ITEMS)).bench_local(|| { + let mut sketch = ThetaSketchBuilder::default().lg_k(14).build().unwrap(); + for value in 0..ITEMS as u64 { + sketch.update(black_box(value)); + } + black_box(sketch) + }); +} + +#[divan::bench] +fn bytes_32(bencher: Bencher) { + let values = bytes_32_values(); + + bencher.counter(ItemsCount::new(ITEMS)).bench_local(|| { + let mut sketch = ThetaSketchBuilder::default().lg_k(14).build().unwrap(); + for value in &values { + sketch.update(raw_bytes::from_slice(black_box(value))); + } + black_box(sketch) + }); +} diff --git a/benchmarks/tuple/mod.rs b/benchmarks/tuple/mod.rs new file mode 100644 index 00000000..1dea33be --- /dev/null +++ b/benchmarks/tuple/mod.rs @@ -0,0 +1,18 @@ +// 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. + +mod update; diff --git a/benchmarks/tuple/update.rs b/benchmarks/tuple/update.rs new file mode 100644 index 00000000..9041fed0 --- /dev/null +++ b/benchmarks/tuple/update.rs @@ -0,0 +1,52 @@ +// 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::hash::value::raw_bytes; +use datasketches::tuple::DefaultUpdatePolicy; +use datasketches::tuple::TupleSketchBuilder; +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) { + bencher.counter(ItemsCount::new(ITEMS)).bench_local(|| { + let policy = DefaultUpdatePolicy::::default(); + let mut sketch = TupleSketchBuilder::new(policy).lg_k(14).build().unwrap(); + for value in 0..ITEMS as u64 { + sketch.update(black_box(value), 1); + } + black_box(sketch) + }); +} + +#[divan::bench] +fn bytes_32(bencher: Bencher) { + let values = bytes_32_values(); + + bencher.counter(ItemsCount::new(ITEMS)).bench_local(|| { + let policy = DefaultUpdatePolicy::::default(); + let mut sketch = TupleSketchBuilder::new(policy).lg_k(14).build().unwrap(); + for value in &values { + sketch.update(raw_bytes::from_slice(black_box(value)), 1); + } + black_box(sketch) + }); +} From ba9567e01f0d35519b1975e9d5582eff0519fc58 Mon Sep 17 00:00:00 2001 From: tison Date: Wed, 2 Sep 2026 20:32:21 +0800 Subject: [PATCH 4/4] docs: cover hash-backed sketch workloads --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c9d6f876..427ec34c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,7 @@ All significant changes to this project will be documented in this file. ### Improvements * Improve truncated-input diagnostics across sketch deserializers. -* Improve hash-backed sketch update performance, especially for integer inputs and Bloom filter raw-byte inputs. +* Improve hash-backed sketch update performance for integer and raw-byte inputs. ### Bug fixes