Skip to content

Commit 8c21c9b

Browse files
committed
Implement ddsketch serialize and deserialize
1 parent 75bba95 commit 8c21c9b

9 files changed

Lines changed: 150 additions & 39 deletions

ddsketch/include/collapsing_dense_store_impl.hpp

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ void CollapsingDenseStore<Derived, Allocator>::serialize(std::ostream& os) const
6868

6969
template<class Derived, typename Allocator>
7070
Derived CollapsingDenseStore<Derived, Allocator>::deserialize(std::istream& is) {
71-
Derived store(read<int>(is));
71+
Derived store(read<size_type>(is));
7272

7373
if (is.peek() == std::istream::traits_type::eof()) {
7474
return store;
@@ -81,22 +81,14 @@ Derived CollapsingDenseStore<Derived, Allocator>::deserialize(std::istream& is)
8181

8282
template<class Derived, typename Allocator>
8383
int CollapsingDenseStore<Derived, Allocator>::get_serialized_size_bytes() const {
84-
int size_bytes = 0;
85-
size_bytes += sizeof(max_num_bins);
86-
size_bytes += sizeof(is_collapsed);
87-
size_bytes += sizeof(this->min_index);
88-
size_bytes += sizeof(this->max_index);
89-
size_bytes += sizeof(size_type);
90-
91-
size_type non_empty_bins = 0;
92-
for (const double& count : this->bins) {
93-
non_empty_bins += (count > 0.0);
84+
// Header written by serialize(): max_num_bins always present
85+
int size_bytes = static_cast<int>(sizeof(max_num_bins));
86+
if (this->is_empty()) {
87+
return size_bytes;
9488
}
95-
96-
size_bytes += sizeof(non_empty_bins);
97-
size_bytes += non_empty_bins * sizeof(size_type);
98-
size_bytes += non_empty_bins * sizeof(double);
99-
89+
// is_collapsed flag, then the common section (range + bins)
90+
size_bytes += static_cast<int>(sizeof(is_collapsed));
91+
size_bytes += this->get_serialized_size_bytes_common();
10092
return size_bytes;
10193
}
10294

ddsketch/include/ddsketch.hpp

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,9 @@
3030

3131
namespace datasketches {
3232

33+
3334
template<store_concept Store, class Mapping>
34-
class DDSketch {
35+
class DDSketch {
3536
public:
3637
// TODO
3738
// DDSketch() = default;
@@ -66,6 +67,8 @@ class DDSketch {
6667
void serialize(std::ostream& os) const;
6768
static DDSketch deserialize(std::istream& is);
6869
int get_serialized_size_bytes() const;
70+
71+
bool operator==(const DDSketch<Store, Mapping>& other) const;
6972
protected:
7073
Store positive_store;
7174
Store negative_store;
@@ -84,11 +87,19 @@ class DDSketch {
8487
template<store_concept OtherStore>
8588
void check_mergeability(const DDSketch<OtherStore, Mapping>& other) const;
8689
double get_quantile(const double& rank, const double& count) const;
90+
91+
enum Flag : uint8_t {
92+
POSITIVE_STORE,
93+
NEGATIVE_STORE,
94+
INDEX_MAPPING,
95+
ZERO_COUNT,
96+
};
8797
};
8898

8999
// CTA (class template argument deduction) deduction guides (so you can write `ddsketch sketch(s1);`)
90100
template<store_concept Store, class Mapping>
91-
DDSketch(Store, Mapping) -> DDSketch<Store, Mapping>;
101+
DDSketch(Store, Mapping) -> DDSketch<Store, Mapping>;\
102+
92103

93104
} /* namespace datasketches */
94105

ddsketch/include/ddsketch_impl.hpp

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -198,17 +198,50 @@ double DDSketch<Store, Mapping>::get_quantile(const double& rank, const double&
198198
template<store_concept Store, class Mapping>
199199
void DDSketch<Store, Mapping>::serialize(std::ostream& os) const {
200200
index_mapping.serialize(os);
201-
if (zero_count > 0.0) {
202-
write(os, zero_count);
203-
}
204201

202+
write(os, zero_count);
203+
204+
205+
auto val = positive_store.get_serialized_size_bytes();
206+
write(os, positive_store.get_serialized_size_bytes());
205207
positive_store.serialize(os);
208+
209+
val = negative_store.get_serialized_size_bytes();
210+
write(os, negative_store.get_serialized_size_bytes());
206211
negative_store.serialize(os);
207212
}
208213

209214
template<store_concept Store, class Mapping>
210215
DDSketch<Store, Mapping> DDSketch<Store, Mapping>::deserialize(std::istream &is) {
216+
Mapping deserialized_index_mapping = Mapping::deserialize(is);
217+
const auto deserialized_zero_count = read<double>(is);
218+
219+
const auto positive_store_serialized_size = read<int>(is);
211220

221+
std::string pos_buf(positive_store_serialized_size, '\0');
222+
is.read(pos_buf.data(), pos_buf.size());
223+
std::stringstream pos_stream(pos_buf);
224+
Store deserialized_positive_store = Store::deserialize(pos_stream);
225+
226+
const auto negative_store_serialized_size = read<int>(is);
227+
std::string neg_buf(negative_store_serialized_size, '\0');
228+
is.read(neg_buf.data(), neg_buf.size());
229+
std::stringstream neg_stream(neg_buf);
230+
Store deserialized_negative_store = Store::deserialize(neg_stream);
231+
232+
DDSketch<Store, Mapping> ddsketch(deserialized_positive_store, deserialized_negative_store, deserialized_index_mapping);
233+
ddsketch.zero_count = deserialized_zero_count;
234+
return ddsketch;
235+
}
236+
237+
template<store_concept Store, class Mapping>
238+
bool DDSketch<Store, Mapping>::operator==(const DDSketch<Store, Mapping>& other) const {
239+
return positive_store == other.positive_store &&
240+
negative_store == other.negative_store &&
241+
index_mapping == other.index_mapping &&
242+
zero_count == other.zero_count &&
243+
min_indexed_value == other.min_indexed_value &&
244+
max_indexed_value == other.max_indexed_value;
212245
}
213246

214247
}

ddsketch/include/dense_store.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ class DenseStore {
136136

137137
void serialize_common(std::ostream& os) const;
138138
static void deserialize_common(Derived& store, std::istream& is);
139+
int get_serialized_size_bytes_common() const;
139140

140141
Derived& derived();
141142
const Derived& derived() const;

ddsketch/include/dense_store_impl.hpp

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -374,7 +374,7 @@ void DenseStore<Derived, Allocator>::serialize_common(std::ostream& os) const {
374374

375375
size_type non_empty_bins = 0;
376376
for (const double& count : bins) {
377-
non_empty_bins += (count > 0.0);
377+
non_empty_bins += (count > 1e-16);
378378
}
379379
write(os, non_empty_bins);
380380

@@ -403,9 +403,46 @@ void DenseStore<Derived, Allocator>::deserialize_common(Derived& store, std::ist
403403
for (size_type i = 0; i < non_empty_bins; ++i) {
404404
const auto index = read<int>(is);
405405
const auto count = read<double>(is);
406-
store.add(index, count);
406+
store.bins[index-store.offset] = count;
407407
}
408408
}
409+
410+
template<class Derived, typename Allocator>
411+
int DenseStore<Derived, Allocator>::get_serialized_size_bytes_common() const {
412+
if (is_empty()) {
413+
return 0;
414+
}
415+
416+
417+
// Keep the running total in size_t, cast to int at the end (the public API returns int)
418+
size_t size_bytes = 0;
419+
420+
// Range metadata written by serialize_common
421+
size_bytes += sizeof(this->min_index); // min_index
422+
size_bytes += sizeof(this->max_index); // max_index
423+
size_bytes += sizeof(this->offset); // offset
424+
425+
// `serialize_common` writes the number of bins (the full allocated length)
426+
size_type num_bins = static_cast<size_type>(this->bins.size());
427+
(void)num_bins; // silence unused warning in templates
428+
size_bytes += sizeof(num_bins);
429+
430+
// Count non-empty bins exactly as in serialize_common (threshold 1e-16)
431+
size_type non_empty_bins = 0;
432+
for (const double& count : this->bins) {
433+
non_empty_bins += (count > 1e-16);
434+
}
435+
436+
// It writes the non_empty_bins counter itself
437+
size_bytes += sizeof(non_empty_bins);
438+
439+
// For each non-empty bin, serialize_common writes: index (int) + count (double)
440+
size_bytes += static_cast<size_t>(non_empty_bins) * sizeof(int);
441+
size_bytes += static_cast<size_t>(non_empty_bins) * sizeof(double);
442+
443+
// Final cast matches the serialized-size field type used elsewhere
444+
return static_cast<int>(size_bytes);
445+
}
409446
}
410447

411448
#endif //DENSE_STORE_IMPL_HPP

ddsketch/include/unbounded_size_dense_store.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ class UnboundedSizeDenseStore: public DenseStore<UnboundedSizeDenseStore<Allocat
4444
void serialize(std::ostream& os) const;
4545
static UnboundedSizeDenseStore deserialize(std::istream& is);
4646
int get_serialized_size_bytes() const;
47+
4748
protected:
4849
size_type normalize(size_type index);
4950
void adjust(size_type new_min_index, size_type new_max_index);

ddsketch/include/unbounded_size_dense_store_impl.hpp

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -97,21 +97,7 @@ UnboundedSizeDenseStore<Allocator> UnboundedSizeDenseStore<Allocator>::deseriali
9797

9898
template<typename Allocator>
9999
int UnboundedSizeDenseStore<Allocator>::get_serialized_size_bytes() const {
100-
int size_bytes = 0;
101-
size_bytes += sizeof(this->min_index);
102-
size_bytes += sizeof(this->max_index);
103-
size_bytes += sizeof(size_type);
104-
105-
size_type non_empty_bins = 0;
106-
for (const double& count : this->bins) {
107-
non_empty_bins += (count > 0.0);
108-
}
109-
110-
size_bytes += sizeof(non_empty_bins);
111-
size_bytes += non_empty_bins * sizeof(size_type);
112-
size_bytes += non_empty_bins * sizeof(double);
113-
114-
return size_bytes;
100+
return this->get_serialized_size_bytes_common();
115101
}
116102

117103

ddsketch/test/DDSketchTest.cpp

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -516,6 +516,56 @@ TEMPLATE_TEST_CASE("DDSketch merging random test", "[ddsketch][random]",
516516
}
517517
}
518518

519+
TEMPLATE_TEST_CASE("DDSketch serialize - deserialize test", "[ddsketch][random]",
520+
// DDSketchUnboundedStoreTestCase,
521+
DDSketchCollapsingHighestStoreTestCase<4096>
522+
// DDSketchCollapsingLowestStoreTestCase<4096>
523+
// DDSketchSparseStoreTestCase
524+
) {
525+
auto positive_store = *TestType::first_type::new_store();
526+
auto negative_store = *TestType::first_type::new_store();
527+
using StoreType = decltype(positive_store);
528+
using MappingType = TestType::second_type;
529+
constexpr double relative_accuracy = 0.01;
530+
constexpr int num_tests = 100;
531+
constexpr int max_num_values = 1000;
532+
533+
DDSketch<StoreType, MappingType> sketch(positive_store, negative_store, MappingType(relative_accuracy));
534+
std::random_device rd;
535+
std::mt19937_64 rng(rd());
536+
std::uniform_int_distribution<int> size_dist(0, max_num_values - 1);
537+
std::uniform_real_distribution<double> value_dist(-1000.0, 1000.0);
538+
539+
std::stringstream ss;
540+
sketch.serialize(ss);
541+
DDSketch<StoreType, MappingType> deserialized_empty_sketch = DDSketch<StoreType, MappingType>::deserialize(ss);
542+
REQUIRE(sketch.is_empty());
543+
REQUIRE(deserialized_empty_sketch.is_empty());
544+
REQUIRE(ss.peek() == std::istream::traits_type::eof());
545+
REQUIRE(sketch == deserialized_empty_sketch);
546+
ss.clear();
547+
548+
for (int i = 0; i < num_tests; ++i) {
549+
std::vector<double> values;
550+
int num_values = size_dist(rng);
551+
552+
for (int j = 0; j < num_values; ++j) {
553+
sketch.update(value_dist(rng));
554+
}
555+
556+
sketch.serialize(ss);
557+
auto deserialized_sketch = DDSketch<StoreType, MappingType>::deserialize(ss);
558+
REQUIRE_FALSE(sketch.is_empty());
559+
REQUIRE_FALSE(deserialized_sketch.is_empty());
560+
REQUIRE(ss.peek() == std::istream::traits_type::eof());
561+
REQUIRE(sketch == deserialized_sketch);
562+
ss.clear();
563+
564+
}
565+
566+
567+
}
568+
519569
TEST_CASE("quantile", "[ddsketch]") {
520570
std::random_device rd{};
521571
std::mt19937_64 gen{rd()};

ddsketch/test/StoreTest.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -524,7 +524,7 @@ TEMPLATE_TEST_CASE("test cross merge", "[storetest]",
524524
}
525525
}
526526

527-
TEMPLATE_TEST_CASE("dense store serialization test", "[serialization]",
527+
TEMPLATE_TEST_CASE("test store serialize - deserialize", "[serialization]",
528528
CollapsingLowestStoreTestCase<8>,
529529
CollapsingLowestStoreTestCase<128>,
530530
CollapsingLowestStoreTestCase<1024>,

0 commit comments

Comments
 (0)