Skip to content

Commit 016256f

Browse files
authored
Merge pull request #362 from jmalkin/density_serialization
Density serialization
2 parents e31440f + cd0ef32 commit 016256f

7 files changed

Lines changed: 498 additions & 5 deletions

File tree

common/include/memory_operations.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
#include <exception>
2525
#include <iostream>
2626
#include <string>
27+
#include <cstring>
2728

2829
namespace datasketches {
2930

density/include/density_sketch.hpp

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,44 @@ class density_sketch {
126126
*/
127127
Allocator get_allocator() const;
128128

129+
/**
130+
* This method serializes the sketch into a given stream in a binary form
131+
* @param os output stream
132+
*/
133+
void serialize(std::ostream& os) const;
134+
135+
using vector_bytes = std::vector<uint8_t, typename std::allocator_traits<Allocator>::template rebind_alloc<uint8_t>>;
136+
137+
/**
138+
* This method serializes the sketch as a vector of bytes.
139+
* An optional header can be reserved in front of the sketch.
140+
* It is an uninitialized space of a given size.
141+
* This header is used in Datasketches PostgreSQL extension.
142+
* @param header_size_bytes space to reserve in front of the sketch
143+
*/
144+
vector_bytes serialize(unsigned header_size_bytes = 0) const;
145+
146+
/**
147+
* This method deserializes a sketch from a given stream.
148+
* @param is input stream
149+
* @param kernel the kernel function to use for this sketch
150+
* @param allocator the memory allocator to use with this sketch
151+
* @return an instance of the sketch
152+
*/
153+
static density_sketch deserialize(std::istream& is,
154+
const Kernel& kernel=Kernel(), const Allocator& allocator = Allocator());
155+
156+
/**
157+
* This method deserializes a sketch from a given array of bytes.
158+
* @param bytes pointer to the array of bytes
159+
* @param size the size of the array
160+
* @param kernel the kernel function to use for this sketch
161+
* @param allocator the memory allocator to use with this sketch
162+
* @return an instance of the sketch
163+
*/
164+
static density_sketch deserialize(const void* bytes, size_t size,
165+
const Kernel& kernel=Kernel(), const Allocator& allocator = Allocator());
166+
129167
/**
130168
* Prints a summary of the sketch.
131169
* @param print_levels if true include information about levels
@@ -138,6 +176,14 @@ class density_sketch {
138176
const_iterator end() const;
139177

140178
private:
179+
enum flags { RESERVED0, RESERVED1, IS_EMPTY };
180+
static const uint8_t PREAMBLE_INTS_SHORT = 3;
181+
static const uint8_t PREAMBLE_INTS_LONG = 6;
182+
static const uint8_t FAMILY_ID = 19;
183+
static const uint8_t SERIAL_VERSION = 1;
184+
static const size_t LEVELS_ARRAY_START = 5;
185+
186+
Allocator allocator_;
141187
Kernel kernel_;
142188
uint16_t k_;
143189
uint32_t dim_;
@@ -147,6 +193,14 @@ class density_sketch {
147193

148194
void compact();
149195
void compact_level(unsigned height);
196+
197+
static void check_k(uint16_t k);
198+
static void check_serial_version(uint8_t serial_version);
199+
static void check_family_id(uint8_t family_id);
200+
static void check_header_validity(uint8_t preamble_ints, uint8_t flags_byte, uint8_t serial_version);
201+
202+
density_sketch(uint16_t k, uint32_t dim, uint32_t num_retained, uint64_t n, Levels&& levels,
203+
const Kernel& kernel = Kernel());
150204
};
151205

152206
template<typename T, typename K, typename A>

density/include/density_sketch_impl.hpp

Lines changed: 270 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
#include <algorithm>
2424
#include <sstream>
2525

26+
#include "memory_operations.hpp"
2627
#include "conditional_forward.hpp"
2728

2829
namespace datasketches {
@@ -35,7 +36,22 @@ dim_(dim),
3536
num_retained_(0),
3637
n_(0),
3738
levels_(1, Level(allocator), allocator)
38-
{}
39+
{
40+
check_k(k);
41+
}
42+
43+
template<typename T, typename K, typename A>
44+
density_sketch<T, K, A>::density_sketch(uint16_t k, uint32_t dim, uint32_t num_retained, uint64_t n,
45+
Levels&& levels, const K& kernel):
46+
kernel_(kernel),
47+
k_(k),
48+
dim_(dim),
49+
num_retained_(num_retained),
50+
n_(n),
51+
levels_(std::move(levels))
52+
{
53+
check_k(k);
54+
}
3955

4056
template<typename T, typename K, typename A>
4157
uint16_t density_sketch<T, K, A>::get_k() const {
@@ -146,6 +162,259 @@ void density_sketch<T, K, A>::compact_level(unsigned height) {
146162
level.clear();
147163
}
148164

165+
/* Serialized sketch layout:
166+
* Int || Start Byte Addr:
167+
* Addr:
168+
* || 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
169+
* 0 || Preamble_Ints | SerVer | FamID | Flags |------- K -------|---- unused -----|
170+
*
171+
* || 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
172+
* 2 ||------------- Num Dimensions --------------|------ Num Retained Items ---------|
173+
*
174+
* || 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
175+
* 4 ||---------------------------Items Seen Count (N)--------------------------------|
176+
*
177+
* Ints 2 and 3 are omitted when the sketch is empty, meaning Num Dimensions is stored at
178+
* offset 8 in that case. Otherwise, Int 5 is the start of level data, consisting of the
179+
* size of the level (as a uint32 value) followed by that number of points, with
180+
* Num Dimensions per point.
181+
*/
182+
183+
template<typename T, typename K, typename A>
184+
void density_sketch<T, K, A>::serialize(std::ostream& os) const {
185+
const uint8_t preamble_ints = is_empty() ? PREAMBLE_INTS_SHORT : PREAMBLE_INTS_LONG;
186+
write(os, preamble_ints);
187+
const uint8_t ser_ver = SERIAL_VERSION;
188+
write(os, ser_ver);
189+
const uint8_t family = FAMILY_ID;
190+
write(os, family);
191+
192+
// only empty is a valid flag
193+
const uint8_t flags_byte = (is_empty() ? 1 << flags::IS_EMPTY : 0);
194+
write(os, flags_byte);
195+
write(os, k_);
196+
const uint16_t unused = 0;
197+
write(os, unused);
198+
write(os, dim_);
199+
200+
if (is_empty())
201+
return;
202+
203+
write(os, num_retained_);
204+
write(os, n_);
205+
206+
// levels array -- uint32_t since a single level may be larger than k
207+
size_t pt_size = sizeof(T) * dim_;
208+
for (const Level& lvl : levels_) {
209+
const uint32_t level_size = static_cast<uint32_t>(lvl.size());
210+
write(os, level_size);
211+
for (const Vector& pt : lvl) {
212+
write(os, pt.data(), pt_size);
213+
}
214+
}
215+
}
216+
217+
template<typename T, typename K, typename A>
218+
auto density_sketch<T, K, A>::serialize(unsigned header_size_bytes) const -> vector_bytes {
219+
const uint8_t preamble_ints = (is_empty() ? PREAMBLE_INTS_SHORT : PREAMBLE_INTS_LONG);
220+
221+
// pre-compute size
222+
size_t size = header_size_bytes + preamble_ints * sizeof(uint32_t);
223+
if (!is_empty())
224+
for (const Level& lvl : levels_)
225+
size += sizeof(uint32_t) + (lvl.size() * dim_ * sizeof(T));
226+
227+
vector_bytes bytes(size, 0, levels_.get_allocator());
228+
uint8_t* ptr = bytes.data() + header_size_bytes;
229+
const uint8_t* end_ptr = ptr + size;
230+
231+
ptr += copy_to_mem(preamble_ints, ptr);
232+
const uint8_t ser_ver = SERIAL_VERSION;
233+
ptr += copy_to_mem(ser_ver, ptr);
234+
const uint8_t family = FAMILY_ID;
235+
ptr += copy_to_mem(family, ptr);
236+
237+
// empty is the only valid flat
238+
const uint8_t flags_byte = (is_empty() ? 1 << flags::IS_EMPTY : 0);
239+
ptr += copy_to_mem(flags_byte, ptr);
240+
ptr += copy_to_mem(k_, ptr);
241+
ptr += sizeof(uint16_t); // 2 unused bytes
242+
ptr += copy_to_mem(dim_, ptr);
243+
244+
if (is_empty())
245+
return bytes;
246+
247+
ptr += copy_to_mem(num_retained_, ptr);
248+
ptr += copy_to_mem(n_, ptr);
249+
250+
// levels array -- uint32_t since a single level may be larger than k
251+
size_t pt_size = sizeof(T) * dim_;
252+
for (const Level& lvl : levels_) {
253+
ptr += copy_to_mem(static_cast<uint32_t>(lvl.size()), ptr);
254+
for (const Vector& pt : lvl) {
255+
ptr += copy_to_mem(pt.data(), ptr, pt_size);
256+
}
257+
}
258+
259+
if (ptr != end_ptr)
260+
throw std::runtime_error("Actual output size does not equal expected output size");
261+
262+
return bytes;
263+
}
264+
265+
template<typename T, typename K, typename A>
266+
density_sketch<T, K, A> density_sketch<T, K, A>::deserialize(std::istream& is, const K& kernel, const A& allocator) {
267+
const auto preamble_ints = read<uint8_t>(is);
268+
const auto serial_version = read<uint8_t>(is);
269+
const auto family_id = read<uint8_t>(is);
270+
const auto flags_byte = read<uint8_t>(is);
271+
const auto k = read<uint16_t>(is);
272+
read<uint16_t>(is); // unused
273+
const auto dim = read<uint32_t>(is);
274+
275+
check_k(k); // do we have constraints?
276+
check_serial_version(serial_version); // a little redundant with the header check
277+
check_family_id(family_id);
278+
check_header_validity(preamble_ints, flags_byte, serial_version);
279+
280+
if (!is.good()) throw std::runtime_error("error reading from std::istream");
281+
const bool is_empty = (flags_byte & (1 << flags::IS_EMPTY)) > 0;
282+
if (is_empty) {
283+
return density_sketch(k, dim, kernel, allocator);
284+
}
285+
286+
const auto num_retained = read<uint32_t>(is);
287+
const auto n = read<uint64_t>(is);
288+
289+
// levels arrays
290+
size_t pt_size = sizeof(T) * dim;
291+
Levels levels(allocator);
292+
int64_t num_to_read = num_retained; // num_retrained is uint32_t so this allows error checking
293+
while (num_to_read > 0) {
294+
const auto level_size = read<uint32_t>(is);
295+
Level lvl(allocator);
296+
lvl.reserve(level_size);
297+
for (uint32_t i = 0; i < level_size; ++i) {
298+
Vector pt(dim, 0, allocator);
299+
read(is, pt.data(), pt_size);
300+
lvl.push_back(pt);
301+
}
302+
levels.push_back(lvl);
303+
num_to_read -= lvl.size();
304+
}
305+
306+
if (num_to_read != 0)
307+
throw std::runtime_error("Error deserializing sketch: Incorrect number of items read");
308+
if (!is.good()) throw std::runtime_error("error reading from std::istream");
309+
310+
return density_sketch(k, dim, num_retained, n, std::move(levels), kernel);
311+
}
312+
313+
template<typename T, typename K, typename A>
314+
density_sketch<T, K, A> density_sketch<T, K, A>::deserialize(const void* bytes, size_t size, const K& kernel, const A& allocator) {
315+
ensure_minimum_memory(size, PREAMBLE_INTS_SHORT * sizeof(uint32_t));
316+
const char* ptr = static_cast<const char*>(bytes);
317+
const char* end_ptr = static_cast<const char*>(bytes) + size;
318+
uint8_t preamble_ints;
319+
ptr += copy_from_mem(ptr, preamble_ints);
320+
uint8_t serial_version;
321+
ptr += copy_from_mem(ptr, serial_version);
322+
uint8_t family_id;
323+
ptr += copy_from_mem(ptr, family_id);
324+
uint8_t flags_byte;
325+
ptr += copy_from_mem(ptr, flags_byte);
326+
uint16_t k;
327+
ptr += copy_from_mem(ptr, k);
328+
uint16_t unused;
329+
ptr += copy_from_mem(ptr, unused);
330+
uint32_t dim;
331+
ptr += copy_from_mem(ptr, dim);
332+
333+
check_k(k);
334+
check_serial_version(serial_version); // a little redundant with the header check
335+
check_family_id(family_id);
336+
check_header_validity(preamble_ints, flags_byte, serial_version);
337+
338+
const bool is_empty = (flags_byte & (1 << flags::IS_EMPTY)) > 0;
339+
if (is_empty) {
340+
return density_sketch(k, dim, kernel, allocator);
341+
}
342+
343+
ensure_minimum_memory(size, PREAMBLE_INTS_LONG * sizeof(uint32_t));
344+
uint32_t num_retained;
345+
ptr += copy_from_mem(ptr, num_retained);
346+
uint64_t n;
347+
ptr += copy_from_mem(ptr, n);
348+
349+
// Predicting the number of levels seems hard so determining the exact remaining
350+
// size is also hard. But we need at least num_retained * dim * sizeof(T)
351+
// bytes for the points so we can check that.
352+
size_t pt_size = sizeof(T) * dim;
353+
ensure_minimum_memory(end_ptr - ptr, num_retained * pt_size);
354+
355+
// levels arrays
356+
Levels levels(allocator);
357+
int64_t num_to_read = num_retained; // num_retrained is uint32_t so this allows error checking
358+
while (num_to_read > 0) {
359+
uint32_t level_size;
360+
ptr += copy_from_mem(ptr, level_size);
361+
Level lvl(allocator);
362+
lvl.reserve(level_size);
363+
for (uint32_t i = 0; i < level_size; ++i) {
364+
Vector pt(dim, 0, allocator);
365+
ptr += copy_from_mem(ptr, pt.data(), pt_size);
366+
lvl.push_back(pt);
367+
}
368+
levels.push_back(lvl);
369+
num_to_read -= lvl.size();
370+
}
371+
372+
if (num_to_read != 0)
373+
throw std::runtime_error("Error deserializing sketch: Incorrect number of items read");
374+
if (ptr > end_ptr) throw std::runtime_error("Error deserializing sketch: Read beyond provided memory");
375+
376+
return density_sketch(k, dim, num_retained, n, std::move(levels), kernel);
377+
}
378+
379+
template<typename T, typename K, typename A>
380+
void density_sketch<T, K, A>::check_k(uint16_t k) {
381+
if (k < 2)
382+
throw std::invalid_argument("k must be > 1. Found: " + std::to_string(k));
383+
}
384+
385+
template<typename T, typename K, typename A>
386+
void density_sketch<T, K, A>::check_serial_version(uint8_t serial_version) {
387+
if (serial_version == SERIAL_VERSION)
388+
return;
389+
else
390+
throw std::invalid_argument("Possible corruption. Unrecognized serialization version: " + std::to_string(serial_version));
391+
}
392+
393+
template<typename T, typename K, typename A>
394+
void density_sketch<T, K, A>::check_family_id(uint8_t family_id) {
395+
if (family_id == FAMILY_ID)
396+
return;
397+
else
398+
throw std::invalid_argument("Possible corruption. Family id does not indicate density sketch: " + std::to_string(family_id));
399+
}
400+
401+
template<typename T, typename K, typename A>
402+
void density_sketch<T, K, A>::check_header_validity(uint8_t preamble_ints, uint8_t flags_byte, uint8_t serial_version) {
403+
const bool empty = (flags_byte & (1 << flags::IS_EMPTY)) > 0;
404+
405+
if ((empty && preamble_ints == PREAMBLE_INTS_SHORT)
406+
|| (!empty && preamble_ints == PREAMBLE_INTS_LONG))
407+
return;
408+
else {
409+
std::ostringstream os;
410+
os << "Possible sketch corruption. Inconsistent state: "
411+
<< "preamble_ints = " << preamble_ints
412+
<< ", empty = " << (empty ? "true" : "false")
413+
<< ", serialization_version = " << serial_version;
414+
throw std::invalid_argument(os.str());
415+
}
416+
}
417+
149418
template<typename T, typename K, typename A>
150419
string<A> density_sketch<T, K, A>::to_string(bool print_levels, bool print_items) const {
151420
// Using a temporary stream for implementation here does not comply with AllocatorAwareContainer requirements.

0 commit comments

Comments
 (0)