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 e4609c6  perf: avoid clone on serialize frequency items (#144)
e4609c6 is described below

commit e4609c63048138cd26b54b55dbac5c6f12b2fb2d
Author: tison <[email protected]>
AuthorDate: Sun Jul 5 07:39:30 2026 +0800

    perf: avoid clone on serialize frequency items (#144)
    
    Signed-off-by: tison <[email protected]>
---
 CHANGELOG.md                                       |  1 +
 .../src/frequencies/reverse_purge_item_hash_map.rs | 30 +++-----------
 datasketches/src/frequencies/serialization.rs      |  2 +-
 datasketches/src/frequencies/sketch.rs             | 47 ++++++++++------------
 .../tests/frequencies_serialization_test.rs        | 40 ++++++++++++++++++
 datasketches/tests/frequencies_update_test.rs      | 17 ++++++++
 6 files changed, 86 insertions(+), 51 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5f28eda..e7ada56 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,7 @@ All significant changes to this project will be documented in 
this file.
 ### New features
 
 * `FrequentItemsSketch` now supports borrowed-key updates via `update_ref` and 
`update_with_count_ref`, allowing sketches such as 
`FrequentItemsSketch<String>` to update from `&str` without allocating on 
existing-key hits. Frequency queries also accept borrowed key forms matching 
`Borrow<Q>`.
+* `FrequentItemsSketch` no longer requires item types to implement `Clone` for 
core updates, queries, and serialization. Custom `FrequentItemValue` 
implementations can now be non-`Clone`; APIs that return or merge owned items 
still require `Clone`.
 
 ### Bug fixes
 
diff --git a/datasketches/src/frequencies/reverse_purge_item_hash_map.rs 
b/datasketches/src/frequencies/reverse_purge_item_hash_map.rs
index 0e59745..c26f1b5 100644
--- a/datasketches/src/frequencies/reverse_purge_item_hash_map.rs
+++ b/datasketches/src/frequencies/reverse_purge_item_hash_map.rs
@@ -197,37 +197,17 @@ impl<T: Eq + Hash> ReversePurgeItemHashMap<T> {
         self.num_active
     }
 
-    /// Returns the active keys in the map.
-    pub fn active_keys(&self) -> Vec<T>
-    where
-        T: Clone,
-    {
-        if self.num_active == 0 {
-            return vec![];
-        }
-        let mut keys = Vec::with_capacity(self.num_active);
+    /// Returns active keys and values in storage order.
+    pub fn active_entries(&self) -> Vec<(&T, u64)> {
+        let mut entries = Vec::with_capacity(self.num_active);
         for i in 0..self.keys.len() {
             if self.states[i] > 0 {
                 if let Some(key) = self.keys[i].as_ref() {
-                    keys.push(key.clone());
+                    entries.push((key, self.values[i]));
                 }
             }
         }
-        keys
-    }
-
-    /// Returns the active values in the map.
-    pub fn active_values(&self) -> Vec<u64> {
-        if self.num_active == 0 {
-            return vec![];
-        }
-        let mut values = Vec::with_capacity(self.num_active);
-        for i in 0..self.values.len() {
-            if self.states[i] > 0 {
-                values.push(self.values[i]);
-            }
-        }
-        values
+        entries
     }
 
     /// Returns an iterator over active keys and values.
diff --git a/datasketches/src/frequencies/serialization.rs 
b/datasketches/src/frequencies/serialization.rs
index ed7a898..f1f8f0c 100644
--- a/datasketches/src/frequencies/serialization.rs
+++ b/datasketches/src/frequencies/serialization.rs
@@ -33,7 +33,7 @@ pub const PREAMBLE_LONGS_NONEMPTY: u8 = 4;
 pub const EMPTY_FLAG_MASK: u8 = 5;
 
 /// Trait for serializing and deserializing frequent item values.
-pub trait FrequentItemValue: Sized + Eq + Hash + Clone {
+pub trait FrequentItemValue: Sized + Eq + Hash {
     /// Returns the size in bytes required to serialize the given item.
     fn serialize_size(item: &Self) -> usize;
     /// Serializes the item into the given byte buffer.
diff --git a/datasketches/src/frequencies/sketch.rs 
b/datasketches/src/frequencies/sketch.rs
index cf9f06d..49dce3b 100644
--- a/datasketches/src/frequencies/sketch.rs
+++ b/datasketches/src/frequencies/sketch.rs
@@ -34,8 +34,8 @@ use crate::frequencies::serialization::PREAMBLE_LONGS_EMPTY;
 use crate::frequencies::serialization::PREAMBLE_LONGS_NONEMPTY;
 use crate::frequencies::serialization::SERIAL_VERSION;
 
-type CountSerializeSize<T> = fn(&[T]) -> usize;
-type SerializeItems<T> = fn(&mut SketchBytes, &[T]);
+type CountSerializeSize<T> = fn(&T) -> usize;
+type SerializeItem<T> = fn(&mut SketchBytes, &T);
 type DeserializeItems<T> = fn(SketchSlice<'_>, usize) -> Result<Vec<T>, Error>;
 
 const LG_MIN_MAP_SIZE: u8 = 3;
@@ -352,7 +352,7 @@ impl<T: Eq + Hash> FrequentItemsSketch<T> {
         }
         let merged_total = self.stream_weight + other.stream_weight;
         for (item, count) in other.hash_map.iter() {
-            self.update_with_count(item.clone(), count);
+            self.update_with_count_ref(item, count);
         }
         self.offset += other.offset;
         self.stream_weight = merged_total;
@@ -472,11 +472,8 @@ impl<T: Eq + Hash> FrequentItemsSketch<T> {
     fn serialize_inner(
         &self,
         count_serialize_size: CountSerializeSize<T>,
-        serialize_items: SerializeItems<T>,
-    ) -> Vec<u8>
-    where
-        T: Clone, // for self.hash_map.active_keys()
-    {
+        serialize_item: SerializeItem<T>,
+    ) -> Vec<u8> {
         if self.is_empty() {
             let mut bytes = SketchBytes::with_capacity(PREAMBLE_LONGS_EMPTY as 
usize * 8);
             bytes.write_u8(PREAMBLE_LONGS_EMPTY);
@@ -490,12 +487,17 @@ impl<T: Eq + Hash> FrequentItemsSketch<T> {
         }
 
         let active_items = self.num_active_items();
-        let values = self.hash_map.active_values();
-        let keys = self.hash_map.active_keys();
-        let total_bytes =
-            PREAMBLE_LONGS_NONEMPTY as usize * 8 + (active_items * 8) + 
count_serialize_size(&keys);
-
-        let mut bytes = SketchBytes::with_capacity(total_bytes);
+        let active_entries = self.hash_map.active_entries();
+
+        let mut bytes = SketchBytes::with_capacity({
+            let mut total_bytes = 0;
+            total_bytes += PREAMBLE_LONGS_NONEMPTY as usize * 8;
+            total_bytes += active_items * 8;
+            for (k, _) in &active_entries {
+                total_bytes += count_serialize_size(k);
+            }
+            total_bytes
+        });
         bytes.write_u8(PREAMBLE_LONGS_NONEMPTY);
         bytes.write_u8(SERIAL_VERSION);
         bytes.write_u8(Family::FREQUENCY.id);
@@ -509,10 +511,12 @@ impl<T: Eq + Hash> FrequentItemsSketch<T> {
         bytes.write_u64_le(self.stream_weight);
         bytes.write_u64_le(self.offset);
 
-        for value in values {
-            bytes.write_u64_le(value);
+        for (_, v) in &active_entries {
+            bytes.write_u64_le(*v);
+        }
+        for (k, _) in &active_entries {
+            serialize_item(&mut bytes, k);
         }
-        serialize_items(&mut bytes, &keys);
 
         bytes.into_bytes()
     }
@@ -618,14 +622,7 @@ impl<T: FrequentItemValue> FrequentItemsSketch<T> {
     /// assert!(decoded.estimate(&apple) >= 2);
     /// ```
     pub fn serialize(&self) -> Vec<u8> {
-        self.serialize_inner(
-            |items| items.iter().map(T::serialize_size).sum(),
-            |bytes, items| {
-                for item in items {
-                    item.serialize_value(bytes);
-                }
-            },
-        )
+        self.serialize_inner(T::serialize_size, |bytes, item| 
item.serialize_value(bytes))
     }
 
     /// Deserializes a sketch from bytes.
diff --git a/datasketches/tests/frequencies_serialization_test.rs 
b/datasketches/tests/frequencies_serialization_test.rs
index f070e91..4c0944e 100644
--- a/datasketches/tests/frequencies_serialization_test.rs
+++ b/datasketches/tests/frequencies_serialization_test.rs
@@ -22,9 +22,35 @@ mod common;
 use std::fs;
 
 use common::serialization_test_data;
+use datasketches::codec::SketchBytes;
+use datasketches::codec::SketchSlice;
+use datasketches::error::Error;
 use datasketches::error::ErrorKind;
+use datasketches::frequencies::FrequentItemValue;
 use datasketches::frequencies::FrequentItemsSketch;
 
+#[derive(Debug, PartialEq, Eq, Hash)]
+struct NonCloneSerializableItem(i64);
+
+impl FrequentItemValue for NonCloneSerializableItem {
+    fn serialize_size(_item: &Self) -> usize {
+        size_of::<i64>()
+    }
+
+    fn serialize_value(&self, bytes: &mut SketchBytes) {
+        bytes.write_i64_le(self.0);
+    }
+
+    fn deserialize_value(cursor: &mut SketchSlice<'_>) -> Result<Self, Error> {
+        cursor.read_i64_le().map(Self).map_err(|_| {
+            Error::new(
+                ErrorKind::InvalidData,
+                "failed to read non-clone item bytes",
+            )
+        })
+    }
+}
+
 #[test]
 fn test_longs_round_trip() {
     let mut sketch: FrequentItemsSketch<i64> = FrequentItemsSketch::new(32);
@@ -52,6 +78,20 @@ fn test_items_round_trip() {
     assert_eq!(restored.maximum_error(), sketch.maximum_error());
 }
 
+#[test]
+fn test_non_clone_item_round_trip() {
+    let mut sketch = FrequentItemsSketch::<NonCloneSerializableItem>::new(32);
+    sketch.update_with_count(NonCloneSerializableItem(1), 2);
+    sketch.update_with_count(NonCloneSerializableItem(2), 5);
+
+    let bytes = sketch.serialize();
+    let restored = 
FrequentItemsSketch::<NonCloneSerializableItem>::deserialize(&bytes).unwrap();
+
+    assert_eq!(restored.total_weight(), sketch.total_weight());
+    assert_eq!(restored.estimate(&NonCloneSerializableItem(1)), 2);
+    assert_eq!(restored.estimate(&NonCloneSerializableItem(2)), 5);
+}
+
 #[test]
 fn test_empty_round_trip() {
     let sketch = FrequentItemsSketch::<i64>::new(32);
diff --git a/datasketches/tests/frequencies_update_test.rs 
b/datasketches/tests/frequencies_update_test.rs
index 87c702a..792fa9d 100644
--- a/datasketches/tests/frequencies_update_test.rs
+++ b/datasketches/tests/frequencies_update_test.rs
@@ -23,6 +23,23 @@ use datasketches::frequencies::FrequentItemsSketch;
 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
 struct TestItem(i32);
 
+#[derive(Debug, PartialEq, Eq, Hash)]
+struct NonCloneItem(i32);
+
+#[test]
+fn test_non_clone_items_update_and_query() {
+    let mut sketch = FrequentItemsSketch::<NonCloneItem>::new(8);
+
+    sketch.update(NonCloneItem(7));
+    sketch.update_with_count(NonCloneItem(11), 3);
+
+    assert_eq!(sketch.total_weight(), 4);
+    assert_eq!(sketch.num_active_items(), 2);
+    assert_eq!(sketch.estimate(&NonCloneItem(7)), 1);
+    assert_eq!(sketch.lower_bound(&NonCloneItem(11)), 3);
+    assert_eq!(sketch.upper_bound(&NonCloneItem(11)), 3);
+}
+
 #[test]
 fn test_longs_update_with_zero_count_is_noop() {
     let mut sketch: FrequentItemsSketch<i64> = FrequentItemsSketch::new(8);


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

Reply via email to