Skip to content

Commit 32e7e2a

Browse files
authored
Merge pull request #509 from cv4g/countmin-serialize-to
Introduce `count_min_sketch::serialize_to`
2 parents a904aa4 + 4f6b8a7 commit 32e7e2a

5 files changed

Lines changed: 247 additions & 49 deletions

File tree

‎benchmarks/CMakeLists.txt‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,4 +48,5 @@ set_target_properties(count_min_sketch_benchmark PROPERTIES
4848
target_sources(count_min_sketch_benchmark
4949
PRIVATE
5050
benchmark_count_min_sketch.cpp
51+
benchmark_count_min_sketch_serialization.cpp
5152
)
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
#include <benchmark/benchmark.h>
21+
#include <count_min.hpp>
22+
23+
#include <algorithm>
24+
#include <cstddef>
25+
#include <cstdint>
26+
#include <cstring>
27+
#include <ostream>
28+
#include <streambuf>
29+
#include <vector>
30+
31+
namespace
32+
{
33+
34+
using Sketch = datasketches::count_min_sketch<uint64_t>;
35+
36+
// Roughly 99.9% confidence. Serialization cost then scales with the bucket count.
37+
constexpr uint8_t NUM_HASHES = 7;
38+
39+
Sketch makeSketch(uint32_t num_buckets)
40+
{
41+
Sketch sketch(NUM_HASHES, num_buckets);
42+
for (uint64_t i = 0; i < NUM_HASHES; ++i)
43+
sketch.update(i, i + 1);
44+
return sketch;
45+
}
46+
47+
class fixed_buffer_streambuf final: public std::streambuf
48+
{
49+
public:
50+
explicit fixed_buffer_streambuf(std::vector<uint8_t>& buffer): buffer_(buffer), position_(0) {}
51+
52+
void reset()
53+
{
54+
position_ = 0;
55+
}
56+
57+
size_t bytes_written() const
58+
{
59+
return position_;
60+
}
61+
62+
protected:
63+
std::streamsize xsputn(const char* data, std::streamsize size) override
64+
{
65+
const size_t requested = static_cast<size_t>(size);
66+
const size_t available = buffer_.size() - position_;
67+
const size_t bytes_to_write = std::min(requested, available);
68+
if (bytes_to_write > 0) {
69+
std::memcpy(buffer_.data() + position_, data, bytes_to_write);
70+
position_ += bytes_to_write;
71+
}
72+
return static_cast<std::streamsize>(bytes_to_write);
73+
}
74+
75+
int_type overflow(int_type ch) override
76+
{
77+
if (traits_type::eq_int_type(ch, traits_type::eof()))
78+
return traits_type::not_eof(ch);
79+
if (position_ == buffer_.size())
80+
return traits_type::eof();
81+
buffer_[position_++] = static_cast<uint8_t>(traits_type::to_char_type(ch));
82+
return ch;
83+
}
84+
85+
private:
86+
std::vector<uint8_t>& buffer_;
87+
size_t position_;
88+
};
89+
90+
void BM_CountMinGetSerializedSizeBytes(benchmark::State& state)
91+
{
92+
const auto sketch = makeSketch(static_cast<uint32_t>(state.range(0)));
93+
94+
for (auto _ : state) {
95+
const auto size = sketch.get_serialized_size_bytes();
96+
benchmark::DoNotOptimize(size);
97+
}
98+
99+
state.SetItemsProcessed(state.iterations());
100+
}
101+
102+
void BM_CountMinSerializeVector(benchmark::State& state)
103+
{
104+
const auto sketch = makeSketch(static_cast<uint32_t>(state.range(0)));
105+
const auto serialized_size = sketch.get_serialized_size_bytes();
106+
107+
for (auto _ : state) {
108+
auto bytes = sketch.serialize();
109+
benchmark::DoNotOptimize(bytes.data());
110+
benchmark::DoNotOptimize(bytes.size());
111+
benchmark::ClobberMemory();
112+
}
113+
114+
state.SetBytesProcessed(state.iterations() * static_cast<int64_t>(serialized_size));
115+
}
116+
117+
void BM_CountMinSerializeOStream(benchmark::State& state)
118+
{
119+
const auto sketch = makeSketch(static_cast<uint32_t>(state.range(0)));
120+
const auto serialized_size = sketch.get_serialized_size_bytes();
121+
std::vector<uint8_t> bytes(serialized_size);
122+
fixed_buffer_streambuf stream_buffer(bytes);
123+
std::ostream os(&stream_buffer);
124+
125+
for (auto _ : state) {
126+
stream_buffer.reset();
127+
os.clear();
128+
sketch.serialize(os);
129+
benchmark::DoNotOptimize(stream_buffer.bytes_written());
130+
benchmark::ClobberMemory();
131+
}
132+
133+
state.SetBytesProcessed(state.iterations() * static_cast<int64_t>(serialized_size));
134+
}
135+
136+
void BM_CountMinSerializeToSink(benchmark::State& state)
137+
{
138+
const auto sketch = makeSketch(static_cast<uint32_t>(state.range(0)));
139+
const auto serialized_size = sketch.get_serialized_size_bytes();
140+
std::vector<uint8_t> bytes(serialized_size);
141+
142+
for (auto _ : state) {
143+
uint8_t* ptr = bytes.data();
144+
const auto bytes_written = sketch.serialize_to([&ptr](const void* data, size_t size) {
145+
std::memcpy(ptr, data, size);
146+
ptr += size;
147+
});
148+
benchmark::DoNotOptimize(ptr);
149+
benchmark::DoNotOptimize(bytes_written);
150+
benchmark::ClobberMemory();
151+
}
152+
153+
state.SetBytesProcessed(state.iterations() * static_cast<int64_t>(serialized_size));
154+
}
155+
156+
}
157+
158+
BENCHMARK(BM_CountMinGetSerializedSizeBytes)->RangeMultiplier(8)->Range(1024, 65536);
159+
BENCHMARK(BM_CountMinSerializeVector)->RangeMultiplier(8)->Range(1024, 65536);
160+
BENCHMARK(BM_CountMinSerializeOStream)->RangeMultiplier(8)->Range(1024, 65536);
161+
BENCHMARK(BM_CountMinSerializeToSink)->RangeMultiplier(8)->Range(1024, 65536);

‎count/include/count_min.hpp‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
#define COUNT_MIN_HPP_
2222

2323
#include <iterator>
24+
#include <type_traits>
25+
#include <vector>
2426
#include "common_defs.hpp"
2527

2628
namespace datasketches {
@@ -36,6 +38,7 @@ template <typename W,
3638
typename Allocator = std::allocator<W>>
3739
class count_min_sketch{
3840
static_assert(std::is_arithmetic<W>::value, "Arithmetic type expected");
41+
static_assert(!std::is_same<W, bool>::value, "Boolean weight type is not supported");
3942
public:
4043
using allocator_type = Allocator;
4144
using const_iterator = typename std::vector<W, Allocator>::const_iterator;
@@ -326,6 +329,15 @@ class count_min_sketch{
326329
*/
327330
void serialize(std::ostream& os) const;
328331

332+
/**
333+
* This method serializes the sketch by passing binary fragments to a callback.
334+
* The callback must be callable as write_bytes(const void* data, size_t size).
335+
* The data pointer is valid only until the callback returns.
336+
* @return size in bytes written to the callback
337+
*/
338+
template<typename Sink>
339+
size_t serialize_to(Sink&& sink) const;
340+
329341
// This is a convenience alias for users
330342
// The type returned by the following serialize method
331343
using vector_bytes = std::vector<uint8_t, typename std::allocator_traits<Allocator>::template rebind_alloc<uint8_t>>;

‎count/include/count_min_impl.hpp‎

Lines changed: 37 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -262,39 +262,58 @@ return _sketch_array.end();
262262

263263
template<typename W, typename A>
264264
void count_min_sketch<W,A>::serialize(std::ostream& os) const {
265+
serialize_to([&os](const void* data, size_t size) {
266+
os.write(static_cast<const char*>(data), size);
267+
});
268+
}
269+
270+
template<typename Sink, typename T>
271+
static inline void write_count_min_value(Sink& sink, size_t& bytes_written, const T& value) {
272+
sink(&value, sizeof(value));
273+
bytes_written += sizeof(value);
274+
}
275+
276+
template<typename W, typename A>
277+
template<typename Sink>
278+
size_t count_min_sketch<W,A>::serialize_to(Sink&& sink) const {
279+
size_t bytes_written = 0;
280+
265281
// Long 0
266282
//const uint8_t preamble_longs = is_empty() ? PREAMBLE_LONGS_SHORT : PREAMBLE_LONGS_FULL;
267283
const uint8_t preamble_longs = PREAMBLE_LONGS_SHORT;
268284
const uint8_t ser_ver = SERIAL_VERSION_1;
269285
const uint8_t family_id = FAMILY_ID;
270286
const uint8_t flags_byte = (is_empty() ? 1 << flags::IS_EMPTY : 0);
271287
const uint32_t unused32 = NULL_32;
272-
write(os, preamble_longs);
273-
write(os, ser_ver);
274-
write(os, family_id);
275-
write(os, flags_byte);
276-
write(os, unused32);
288+
write_count_min_value(sink, bytes_written, preamble_longs);
289+
write_count_min_value(sink, bytes_written, ser_ver);
290+
write_count_min_value(sink, bytes_written, family_id);
291+
write_count_min_value(sink, bytes_written, flags_byte);
292+
write_count_min_value(sink, bytes_written, unused32);
277293

278294
// Long 1
279295
const uint32_t nbuckets = _num_buckets;
280296
const uint8_t nhashes = _num_hashes;
281297
const uint16_t seed_hash(compute_seed_hash(_seed));
282298
const uint8_t unused8 = NULL_8;
283-
write(os, nbuckets);
284-
write(os, nhashes);
285-
write(os, seed_hash);
286-
write(os, unused8);
287-
if (is_empty()) { return; } // sketch is empty, no need to write further bytes.
299+
write_count_min_value(sink, bytes_written, nbuckets);
300+
write_count_min_value(sink, bytes_written, nhashes);
301+
write_count_min_value(sink, bytes_written, seed_hash);
302+
write_count_min_value(sink, bytes_written, unused8);
303+
if (is_empty()) { return bytes_written; } // sketch is empty, no need to write further bytes.
288304

289305
// Long 2
290-
write(os, _total_weight);
306+
const W t_weight = _total_weight;
307+
write_count_min_value(sink, bytes_written, t_weight);
291308

292309
// Long 3 onwards: remaining bytes are consumed by writing the weight and the array values.
293-
auto it = _sketch_array.begin();
294-
while (it != _sketch_array.end()) {
295-
write(os, *it);
296-
++it;
310+
const size_t sketch_array_bytes = sizeof(W) * _sketch_array.size();
311+
if (sketch_array_bytes > 0) {
312+
sink(_sketch_array.data(), sketch_array_bytes);
313+
bytes_written += sketch_array_bytes;
297314
}
315+
316+
return bytes_written;
298317
}
299318

300319
template<typename W, typename A>
@@ -345,40 +364,9 @@ template<typename W, typename A>
345364
auto count_min_sketch<W,A>::serialize(unsigned header_size_bytes) const -> vector_bytes {
346365
vector_bytes bytes(header_size_bytes + get_serialized_size_bytes(), 0, _allocator);
347366
uint8_t *ptr = bytes.data() + header_size_bytes;
348-
349-
// Long 0
350-
const uint8_t preamble_longs = PREAMBLE_LONGS_SHORT;
351-
ptr += copy_to_mem(preamble_longs, ptr);
352-
const uint8_t ser_ver = SERIAL_VERSION_1;
353-
ptr += copy_to_mem(ser_ver, ptr);
354-
const uint8_t family_id = FAMILY_ID;
355-
ptr += copy_to_mem(family_id, ptr);
356-
const uint8_t flags_byte = (is_empty() ? 1 << flags::IS_EMPTY : 0);
357-
ptr += copy_to_mem(flags_byte, ptr);
358-
const uint32_t unused32 = NULL_32;
359-
ptr += copy_to_mem(unused32, ptr);
360-
361-
// Long 1
362-
const uint32_t nbuckets = _num_buckets;
363-
const uint8_t nhashes = _num_hashes;
364-
const uint16_t seed_hash(compute_seed_hash(_seed));
365-
const uint8_t null_characters_8 = NULL_8;
366-
ptr += copy_to_mem(nbuckets, ptr);
367-
ptr += copy_to_mem(nhashes, ptr);
368-
ptr += copy_to_mem(seed_hash, ptr);
369-
ptr += copy_to_mem(null_characters_8, ptr);
370-
if (is_empty()) { return bytes; } // sketch is empty, no need to write further bytes.
371-
372-
// Long 2
373-
const W t_weight = _total_weight;
374-
ptr += copy_to_mem(t_weight, ptr);
375-
376-
// Long 3 onwards: remaining bytes are consumed by writing the weight and the array values.
377-
auto it = _sketch_array.begin();
378-
while (it != _sketch_array.end()) {
379-
ptr += copy_to_mem(*it, ptr);
380-
++it;
381-
}
367+
serialize_to([&ptr](const void* data, size_t size) {
368+
ptr += copy_to_mem(data, ptr, size);
369+
});
382370

383371
return bytes;
384372
}

‎count/test/count_min_test.cpp‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,4 +300,40 @@ TEST_CASE("CountMin sketch: bytes serialize-deserialize non-empty", "[cm_sketch]
300300

301301
}
302302

303+
TEST_CASE("CountMin sketch: sink serialize-deserialize round trip", "[cm_sketch]") {
304+
auto check_sink_serialize = [](const count_min_sketch<uint64_t>& c) {
305+
std::vector<uint8_t> sink_bytes;
306+
sink_bytes.reserve(c.get_serialized_size_bytes());
307+
308+
const size_t bytes_written = c.serialize_to([&sink_bytes](const void* data, size_t size) {
309+
const auto* begin = static_cast<const uint8_t*>(data);
310+
sink_bytes.insert(sink_bytes.end(), begin, begin + size);
311+
});
312+
313+
REQUIRE(bytes_written == c.get_serialized_size_bytes());
314+
REQUIRE(sink_bytes.size() == bytes_written);
315+
316+
auto d = count_min_sketch<uint64_t>::deserialize(sink_bytes.data(), sink_bytes.size(), c.get_seed());
317+
REQUIRE(c.get_num_hashes() == d.get_num_hashes());
318+
REQUIRE(c.get_num_buckets() == d.get_num_buckets());
319+
REQUIRE(c.get_seed() == d.get_seed());
320+
REQUIRE(c.get_total_weight() == d.get_total_weight());
321+
322+
auto c_it = c.begin();
323+
auto d_it = d.begin();
324+
while (c_it != c.end()) {
325+
REQUIRE(*c_it == *d_it);
326+
++c_it;
327+
++d_it;
328+
}
329+
};
330+
331+
count_min_sketch<uint64_t> empty(3, 32);
332+
check_sink_serialize(empty);
333+
334+
count_min_sketch<uint64_t> non_empty(5, 64);
335+
for (uint64_t i=0; i < 10; ++i) non_empty.update(i, 10 * i * i);
336+
check_sink_serialize(non_empty);
337+
}
338+
303339
} /* namespace datasketches */

0 commit comments

Comments
 (0)