be/src/exprs/datasketches_hll_util.cpp
Line | Count | Source |
1 | | // Licensed to the Apache Software Foundation (ASF) under one |
2 | | // or more contributor license agreements. See the NOTICE file |
3 | | // distributed with this work for additional information |
4 | | // regarding copyright ownership. The ASF licenses this file |
5 | | // to you under the Apache License, Version 2.0 (the |
6 | | // "License"); you may not use this file except in compliance |
7 | | // with the License. You may obtain a copy of the License at |
8 | | // |
9 | | // http://www.apache.org/licenses/LICENSE-2.0 |
10 | | // |
11 | | // Unless required by applicable law or agreed to in writing, |
12 | | // software distributed under the License is distributed on an |
13 | | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
14 | | // KIND, either express or implied. See the License for the |
15 | | // specific language governing permissions and limitations |
16 | | // under the License. |
17 | | |
18 | | #include "exprs/datasketches_hll_util.h" |
19 | | |
20 | | #include <bthread/bthread.h> |
21 | | |
22 | | #include <cstring> |
23 | | #include <exception> |
24 | | #include <mutex> |
25 | | #include <new> |
26 | | #include <stdexcept> |
27 | | |
28 | | #include "common/check.h" |
29 | | #include "common/exception.h" |
30 | | #include "common/status.h" |
31 | | #include "util/debug_points.h" |
32 | | |
33 | | namespace doris::datasketches_hll { |
34 | | |
35 | | struct alignas(std::max_align_t) AllocationHeader { |
36 | | AllocationHeader* previous; |
37 | | AllocationHeader* next; |
38 | | detail::AllocationOwner* owner; |
39 | | size_t count; |
40 | | }; |
41 | | |
42 | | namespace { |
43 | | |
44 | | using namespace datasketches::hll_constants; |
45 | | |
46 | | thread_local detail::AllocationOwner* pthread_scope = nullptr; |
47 | | |
48 | 64 | bthread_key_t scope_key() { |
49 | 64 | static bthread_key_t key; |
50 | 64 | static std::once_flag once; |
51 | 64 | std::call_once(once, [] { DORIS_CHECK(bthread_key_create(&key, nullptr) == 0); }); |
52 | 64 | return key; |
53 | 64 | } |
54 | | |
55 | 30.3k | detail::AllocationOwner* current_scope() { |
56 | 30.3k | return bthread_self() == 0 |
57 | 30.3k | ? pthread_scope |
58 | 30.3k | : static_cast<detail::AllocationOwner*>(bthread_getspecific(scope_key())); |
59 | 30.3k | } |
60 | | |
61 | 18.9k | void set_scope(detail::AllocationOwner* scope) { |
62 | 18.9k | if (bthread_self() == 0) { |
63 | 18.8k | pthread_scope = scope; |
64 | 18.8k | } else { |
65 | 32 | DORIS_CHECK(bthread_setspecific(scope_key(), scope) == 0); |
66 | 32 | } |
67 | 18.9k | } |
68 | | |
69 | | class ActiveAllocationOwner { |
70 | | public: |
71 | 3.38k | explicit ActiveAllocationOwner(detail::AllocationOwner* owner) : _previous(current_scope()) { |
72 | 3.38k | set_scope(owner); |
73 | 3.38k | } |
74 | 3.38k | ~ActiveAllocationOwner() { set_scope(_previous); } |
75 | | |
76 | | private: |
77 | | detail::AllocationOwner* _previous; |
78 | | }; |
79 | | |
80 | | // Reuse the library's protected static header readers without constructing a sketch or |
81 | | // duplicating the mode/type bit decoding. This abstract adapter is never instantiated. |
82 | | struct HeaderReader : datasketches::HllSketchImpl<Alloc> { |
83 | | using datasketches::HllSketchImpl<Alloc>::extractCurMode; |
84 | | using datasketches::HllSketchImpl<Alloc>::extractTgtHllType; |
85 | | }; |
86 | | |
87 | 347k | uint32_t read_uint32(const uint8_t* bytes) { |
88 | 347k | uint32_t value; |
89 | 347k | std::memcpy(&value, bytes, sizeof(value)); |
90 | 347k | return value; |
91 | 347k | } |
92 | | |
93 | 1.16k | void check_set_size(const uint8_t* bytes, size_t size, uint8_t lg_k, bool compact) { |
94 | 1.16k | datasketches::HllUtil<Alloc>::checkMemSize(HASH_SET_INT_ARR_START, size); |
95 | 1.16k | const auto count = read_uint32(bytes + HASH_SET_COUNT_INT); |
96 | | // SET promotes to HLL above 3/4 of its largest table (K/8 entries). Bound the count before |
97 | | // computeLgArrInts(), whose power-of-two and load-factor arithmetic uses uint32_t. |
98 | 1.16k | if (lg_k <= 7 || count > (RESIZE_NUMER * (uint32_t {1} << (lg_k - 3))) / RESIZE_DENOM) { |
99 | 4 | throw std::invalid_argument("Invalid HLL SET coupon count or lgK"); |
100 | 4 | } |
101 | 1.15k | size_t items = count; |
102 | 1.15k | if (!compact) { |
103 | | // The library evaluates 1 << lgArr before checking the payload. For example, lgArr=32 |
104 | | // is an undefined shift. Preserve its support for older images with an unset lgArr. |
105 | 739 | const uint8_t lg_arr = bytes[LG_ARR_BYTE] < LG_INIT_SET_SIZE |
106 | 739 | ? datasketches::HllUtil<Alloc>::computeLgArrInts( |
107 | 93 | datasketches::SET, count, lg_k) |
108 | 739 | : bytes[LG_ARR_BYTE]; |
109 | 739 | if (lg_arr > lg_k - 3) { |
110 | 22 | throw std::invalid_argument("Invalid HLL SET array exponent"); |
111 | 22 | } |
112 | 717 | items = size_t {1} << lg_arr; |
113 | 717 | } |
114 | 1.13k | datasketches::HllUtil<Alloc>::checkMemSize(HASH_SET_INT_ARR_START + items * sizeof(uint32_t), |
115 | 1.13k | size); |
116 | 1.13k | if (!compact) { |
117 | | // newSet() copies the whole table but trusts the declared count. With ten occupied |
118 | | // slots and count=1, a later compact serializer allocates 16 bytes and writes 52. |
119 | 713 | uint32_t occupied = 0; |
120 | 343k | for (size_t i = 0; i < items; ++i) { |
121 | 342k | occupied += read_uint32(bytes + HASH_SET_INT_ARR_START + i * sizeof(uint32_t)) != EMPTY; |
122 | 342k | } |
123 | 713 | if (occupied != count) { |
124 | 533 | throw std::invalid_argument("HLL SET coupon count does not match occupied slots"); |
125 | 533 | } |
126 | 713 | } |
127 | 1.13k | } |
128 | | |
129 | | void check_aux_map(const uint8_t* bytes, size_t size, size_t offset, uint8_t lg_k, uint32_t count, |
130 | 490 | bool compact) { |
131 | 490 | const uint32_t registers = uint32_t {1} << lg_k; |
132 | | // There can be at most one auxiliary entry per register. This also bounds the uint32_t |
133 | | // capacity arithmetic used by the library to rebuild a compact map. |
134 | 490 | if (count > registers) { |
135 | 4 | throw std::invalid_argument("HLL_4 auxiliary count exceeds register count"); |
136 | 4 | } |
137 | 486 | size_t items = count; |
138 | 486 | if (!compact) { |
139 | 210 | const auto lg_arr = bytes[LG_ARR_BYTE]; |
140 | | // An updatable map starts at LG_AUX_ARR_INTS[lgK] and grows at 3/4 load, so at most |
141 | | // K entries need 2*K slots. Reject bad exponents before the library's 1 << lgArr. |
142 | 210 | if (lg_arr < LG_AUX_ARR_INTS[lg_k] || lg_arr > lg_k + 1) { |
143 | 26 | throw std::invalid_argument("Invalid HLL_4 auxiliary array exponent"); |
144 | 26 | } |
145 | 184 | items = size_t {1} << lg_arr; |
146 | 184 | } |
147 | 460 | datasketches::HllUtil<Alloc>::checkMemSize(offset + items * sizeof(uint32_t), size); |
148 | | |
149 | | // AuxHashMap::deserialize owns a raw map until it returns. mustAdd() throws for duplicate |
150 | | // slots before that ownership transfers, leaking the map. Check the same masked slot keys |
151 | | // before allocating it. This bitmap is needed only for sketches with an auxiliary map. |
152 | 460 | DorisVector<uint8_t> seen(registers / 8, 0); |
153 | 2.25k | for (size_t i = 0; i < items; ++i) { |
154 | 1.90k | const auto pair = read_uint32(bytes + offset + i * sizeof(uint32_t)); |
155 | 1.90k | if (pair == EMPTY) { |
156 | | // Compact maps contain only real entries. Treating EMPTY as slot 0 would let |
157 | | // an AUX_TOKEN at register 0 pass this check, but mustFindValueFor(0) still fails. |
158 | 1.13k | if (compact) { |
159 | 26 | throw std::invalid_argument("HLL_4 compact auxiliary map contains an EMPTY entry"); |
160 | 26 | } |
161 | 1.11k | continue; |
162 | 1.13k | } |
163 | 762 | const auto slot = datasketches::HllUtil<Alloc>::getLow26(pair) & (registers - 1); |
164 | 762 | const auto bit = static_cast<uint8_t>(1U << (slot % 8)); |
165 | 762 | if (seen[slot / 8] & bit) { |
166 | 76 | throw std::invalid_argument("HLL_4 auxiliary map contains duplicate slots"); |
167 | 76 | } |
168 | 686 | seen[slot / 8] |= bit; |
169 | 686 | } |
170 | | // A nonempty map is insufficient: every AUX_TOKEN must refer to a slot in that map. |
171 | | // For example, token at register 0 with only slot 3 in the map makes mergeHll() throw |
172 | | // after allocating its target array. Reject that image even on the unchanged-byte path. |
173 | 302k | for (uint32_t slot = 0; slot < registers; ++slot) { |
174 | 301k | const auto byte = bytes[HLL_BYTE_ARR_START + slot / 2]; |
175 | 301k | const auto value = static_cast<uint8_t>((byte >> ((slot % 2) * 4)) & loNibbleMask); |
176 | 301k | if (value == AUX_TOKEN && !(seen[slot / 8] & (1U << (slot % 8)))) { |
177 | 90 | throw std::invalid_argument("HLL_4 AUX_TOKEN has no matching auxiliary slot"); |
178 | 90 | } |
179 | 301k | } |
180 | 358 | } |
181 | | |
182 | 1.71k | bool check_hll_size(const uint8_t* bytes, size_t size, uint8_t lg_k, bool compact) { |
183 | 1.71k | datasketches::HllUtil<Alloc>::checkMemSize(HLL_BYTE_ARR_START, size); |
184 | 1.71k | const auto type = HeaderReader::extractTgtHllType(bytes[MODE_BYTE]); |
185 | 1.71k | const size_t offset = |
186 | 1.71k | HLL_BYTE_ARR_START + datasketches::HllArray<Alloc>::hllArrBytes(type, lg_k); |
187 | 1.71k | datasketches::HllUtil<Alloc>::checkMemSize(offset, size); |
188 | 1.71k | const auto aux_count = read_uint32(bytes + AUX_COUNT_INT); |
189 | 1.71k | if (type != datasketches::HLL_4) { |
190 | | // newHll() installs any nonempty auxiliary map through an unchecked Hll4Array cast. |
191 | | // HLL_6/HLL_8 have no map member, so accepting such an image writes outside the object. |
192 | 871 | if (aux_count != 0) { |
193 | 20 | throw std::invalid_argument("HLL_6/HLL_8 cannot contain an auxiliary map"); |
194 | 20 | } |
195 | 851 | return false; |
196 | 871 | } |
197 | 842 | if (aux_count != 0) { |
198 | 490 | check_aux_map(bytes, size, offset, lg_k, aux_count, compact); |
199 | 490 | return true; |
200 | 490 | } |
201 | | // HLL_4 uses nibble 0xf to reference its auxiliary map. The library accepts auxCount=0 |
202 | | // with such a nibble, then dereferences a null map during union/conversion, even on a no-op. |
203 | 734k | for (size_t i = HLL_BYTE_ARR_START; i < offset; ++i) { |
204 | 734k | if ((bytes[i] & loNibbleMask) == AUX_TOKEN || |
205 | 734k | ((bytes[i] >> 4) & loNibbleMask) == AUX_TOKEN) { |
206 | 2 | throw std::invalid_argument("HLL_4 contains AUX_TOKEN without an auxiliary map"); |
207 | 2 | } |
208 | 734k | } |
209 | 350 | return false; |
210 | 352 | } |
211 | | |
212 | | } // namespace |
213 | | |
214 | | AllocationScope::AllocationScope() |
215 | 6.08k | : _previous(current_scope()), _exceptions(std::uncaught_exceptions()) { |
216 | 6.08k | set_scope(&_owner); |
217 | 6.08k | } |
218 | | |
219 | 6.08k | AllocationScope::~AllocationScope() { |
220 | 6.08k | set_scope(_previous); |
221 | 6.08k | if (std::uncaught_exceptions() == _exceptions) { |
222 | 5.16k | _owner.release(); |
223 | 5.16k | } |
224 | 6.08k | } |
225 | | |
226 | 7.76k | detail::AllocationOwner::~AllocationOwner() { |
227 | 7.76k | clear(); |
228 | 7.76k | } |
229 | | |
230 | 7.76k | void detail::AllocationOwner::clear() noexcept { |
231 | 7.89k | while (_head != nullptr) { |
232 | 125 | auto* header = _head; |
233 | 125 | _head = header->next; |
234 | 125 | CustomStdAllocator<AllocationHeader>().deallocate(header, header->count); |
235 | 125 | } |
236 | 7.76k | } |
237 | | |
238 | 5.16k | void detail::AllocationOwner::release() noexcept { |
239 | 16.0k | while (_head != nullptr) { |
240 | 10.8k | auto* header = _head; |
241 | 10.8k | _head = header->next; |
242 | | // Returned sketches keep normal library ownership after a successful call. |
243 | 10.8k | header->owner = nullptr; |
244 | 10.8k | } |
245 | 5.16k | } |
246 | | |
247 | 21.0k | void* allocate(size_t bytes) { |
248 | 21.0k | DBUG_EXECUTE_IF("datasketches_hll.before_allocate", { DBUG_RUN_CALLBACK(); }); |
249 | 21.0k | auto* scope = current_scope(); |
250 | 21.0k | const size_t count = |
251 | 21.0k | 1 + bytes / sizeof(AllocationHeader) + (bytes % sizeof(AllocationHeader) != 0); |
252 | 21.0k | auto* header = CustomStdAllocator<AllocationHeader>().allocate(count); |
253 | 21.0k | *header = {.previous = nullptr, |
254 | 21.0k | .next = scope == nullptr ? nullptr : scope->_head, |
255 | 21.0k | .owner = scope, |
256 | 21.0k | .count = count}; |
257 | 21.0k | if (scope != nullptr) { |
258 | 17.5k | if (header->next != nullptr) { |
259 | 10.6k | header->next->previous = header; |
260 | 10.6k | } |
261 | 17.5k | scope->_head = header; |
262 | 17.5k | } |
263 | 21.0k | return header + 1; |
264 | 21.0k | } |
265 | | |
266 | 20.7k | void deallocate(void* ptr) noexcept { |
267 | 20.7k | auto* header = static_cast<AllocationHeader*>(ptr) - 1; |
268 | 20.7k | if (header->owner != nullptr) { |
269 | 6.55k | if (header->previous != nullptr) { |
270 | 3.24k | header->previous->next = header->next; |
271 | 3.31k | } else { |
272 | 3.31k | header->owner->_head = header->next; |
273 | 3.31k | } |
274 | 6.55k | if (header->next != nullptr) { |
275 | 3.67k | header->next->previous = header->previous; |
276 | 3.67k | } |
277 | 6.55k | } |
278 | 20.7k | CustomStdAllocator<AllocationHeader>().deallocate(header, header->count); |
279 | 20.7k | } |
280 | | |
281 | | struct Union::Impl { |
282 | | using LibraryUnion = datasketches::hll_union_alloc<Alloc>; |
283 | | |
284 | | detail::AllocationOwner allocations; |
285 | | alignas(LibraryUnion) std::byte storage[sizeof(LibraryUnion)]; |
286 | | bool valid = false; |
287 | | |
288 | 5.03k | LibraryUnion* get() { return std::launder(reinterpret_cast<LibraryUnion*>(storage)); } |
289 | 0 | const LibraryUnion* get() const { |
290 | 0 | return std::launder(reinterpret_cast<const LibraryUnion*>(storage)); |
291 | 0 | } |
292 | | |
293 | 1.68k | ~Impl() { |
294 | 1.68k | if (valid) { |
295 | 1.64k | get()->~LibraryUnion(); |
296 | 1.64k | } |
297 | | // Failed library updates can leave a dangling gadget pointer. AllocationOwner frees |
298 | | // the remaining blocks directly, instead of dereferencing that invalid gadget. |
299 | 1.68k | } |
300 | | }; |
301 | | |
302 | 1.68k | void Union::Deleter::operator()(Impl* ptr) const noexcept { |
303 | 1.68k | ptr->~Impl(); |
304 | 1.68k | datasketches_hll::deallocate(ptr); |
305 | 1.68k | } |
306 | | |
307 | | Union::Union(uint8_t lg_k, const Alloc& allocator) |
308 | 1.68k | : _impl(new(datasketches_hll::allocate(sizeof(Impl))) Impl()) { |
309 | 1.68k | ActiveAllocationOwner scope(&_impl->allocations); |
310 | 1.68k | new (_impl->storage) Impl::LibraryUnion(lg_k, allocator); |
311 | 1.68k | _impl->valid = true; |
312 | 1.68k | } |
313 | | |
314 | 1.67k | Union::~Union() = default; |
315 | 1 | Union::Union(Union&&) noexcept = default; |
316 | 1 | Union& Union::operator=(Union&&) noexcept = default; |
317 | | |
318 | 2 | Union::Union(const Union& other) : _impl(new(datasketches_hll::allocate(sizeof(Impl))) Impl()) { |
319 | 2 | DORIS_CHECK(other._impl->valid); |
320 | 2 | ActiveAllocationOwner scope(&_impl->allocations); |
321 | 2 | new (_impl->storage) Impl::LibraryUnion(*other._impl->get()); |
322 | 2 | _impl->valid = true; |
323 | 2 | } |
324 | | |
325 | 1 | Union& Union::operator=(const Union& other) { |
326 | 1 | Union copy(other); |
327 | 1 | *this = std::move(copy); |
328 | 1 | return *this; |
329 | 1 | } |
330 | | |
331 | 1.69k | void Union::update(const Sketch& sketch) { |
332 | 1.69k | DORIS_CHECK(_impl->valid); |
333 | 1.69k | ActiveAllocationOwner scope(&_impl->allocations); |
334 | 1.69k | try { |
335 | 1.69k | _impl->get()->update(sketch); |
336 | 1.69k | } catch (...) { |
337 | | // The library can free its gadget during promotion and then throw before replacing |
338 | | // the pointer. This state must be discarded; its allocation owner remains intact. |
339 | 20 | _impl->valid = false; |
340 | 20 | throw; |
341 | 20 | } |
342 | 1.69k | } |
343 | | |
344 | 1.57k | Sketch Union::get_result(datasketches::target_hll_type type) const { |
345 | 1.57k | DORIS_CHECK(_impl->valid); |
346 | 1.57k | AllocationScope scope; |
347 | 1.57k | return _impl->get()->get_result(type); |
348 | 1.57k | } |
349 | | |
350 | 70 | double Union::get_estimate() const { |
351 | 70 | DORIS_CHECK(_impl->valid); |
352 | 70 | return _impl->get()->get_estimate(); |
353 | 70 | } |
354 | | |
355 | 3 | double Union::get_lower_bound(uint8_t std_devs) const { |
356 | 3 | DORIS_CHECK(_impl->valid); |
357 | 3 | return _impl->get()->get_lower_bound(std_devs); |
358 | 3 | } |
359 | | |
360 | 3 | double Union::get_upper_bound(uint8_t std_devs) const { |
361 | 3 | DORIS_CHECK(_impl->valid); |
362 | 3 | return _impl->get()->get_upper_bound(std_devs); |
363 | 3 | } |
364 | | |
365 | 38 | uint8_t Union::get_lg_config_k() const { |
366 | 38 | DORIS_CHECK(_impl->valid); |
367 | 38 | return _impl->get()->get_lg_config_k(); |
368 | 38 | } |
369 | | |
370 | 8 | double Union::get_rel_err(bool upper, bool unioned, uint8_t lg_k, uint8_t std_devs) { |
371 | 8 | return Impl::LibraryUnion::get_rel_err(upper, unioned, lg_k, std_devs); |
372 | 8 | } |
373 | | |
374 | 4.49k | Sketch deserialize(const StringRef& value, std::string_view action) { |
375 | 4.49k | AllocationScope allocations; |
376 | | // The library reads the preamble byte before it checks the length. |
377 | 4.49k | if (value.size == 0) { |
378 | 6 | throw Exception(ErrorCode::CORRUPTION, "HLL sketch data corrupted when {}: empty input.", |
379 | 6 | action); |
380 | 6 | } |
381 | 4.48k | try { |
382 | | // All modes have an eight-byte preamble. An invalid lgK would otherwise reach shifts used |
383 | | // to size the register/coupon arrays before the library validates it. |
384 | 4.48k | datasketches::HllUtil<Alloc>::checkMemSize(EMPTY_SKETCH_SIZE_BYTES, value.size); |
385 | 4.48k | const auto* bytes = reinterpret_cast<const uint8_t*>(value.data); |
386 | 4.48k | const auto lg_k = datasketches::HllUtil<Alloc>::checkLgK(bytes[LG_K_BYTE]); |
387 | 4.48k | const auto mode = HeaderReader::extractCurMode(bytes[MODE_BYTE]); |
388 | 4.48k | const bool compact = (bytes[FLAGS_BYTE] & COMPACT_FLAG_MASK) != 0; |
389 | 4.48k | bool has_aux_map = false; |
390 | 4.48k | if (mode == datasketches::SET) { |
391 | 1.16k | check_set_size(bytes, value.size, lg_k, compact); |
392 | 3.32k | } else if (mode == datasketches::HLL) { |
393 | 1.71k | has_aux_map = check_hll_size(bytes, value.size, lg_k, compact); |
394 | 1.71k | } |
395 | | // AuxHashMap::deserialize reads uint32_t directly. STRING rows have no alignment |
396 | | // guarantee: a 233-byte HLL_6 before this HLL_4 leaves its auxiliary entries unaligned. |
397 | | // Only this library reader needs an aligned copy; the other payloads use memcpy. |
398 | 4.48k | if (has_aux_map && reinterpret_cast<uintptr_t>(value.data) % alignof(uint32_t) != 0) { |
399 | 133 | DorisVector<uint32_t> aligned(value.size / sizeof(uint32_t) + |
400 | 133 | (value.size % sizeof(uint32_t) != 0)); |
401 | 133 | std::memcpy(aligned.data(), value.data, value.size); |
402 | 133 | return Sketch::deserialize(aligned.data(), value.size, Alloc()); |
403 | 133 | } |
404 | 4.35k | return Sketch::deserialize(value.data, value.size, Alloc()); |
405 | 4.48k | } catch (const doris::Exception& e) { |
406 | 49 | throw Exception(e.code(), "Failed to deserialize HLL sketch when {}: {}", action, |
407 | 49 | e.to_string()); |
408 | 831 | } catch (const std::exception& e) { |
409 | 831 | throw Exception(ErrorCode::CORRUPTION, "HLL sketch data corrupted when {}: {}", action, |
410 | 831 | e.what()); |
411 | 831 | } catch (...) { |
412 | 0 | throw Exception(ErrorCode::CORRUPTION, |
413 | 0 | "HLL sketch data corrupted when {}: unknown exception.", action); |
414 | 0 | } |
415 | 4.48k | } |
416 | | |
417 | | } // namespace doris::datasketches_hll |