Coverage Report

Created: 2026-09-29 14:44

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
be/src/util/tdigest.h
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
/*
19
 * Licensed to Derrick R. Burns under one or more
20
 * contributor license agreements.  See the NOTICES file distributed with
21
 * this work for additional information regarding copyright ownership.
22
 * The ASF licenses this file to You under the Apache License, Version 2.0
23
 * (the "License"); you may not use this file except in compliance with
24
 * the License.  You may obtain a copy of the License at
25
 *
26
 *     http://www.apache.org/licenses/LICENSE-2.0
27
 *
28
 * Unless required by applicable law or agreed to in writing, software
29
 * distributed under the License is distributed on an "AS IS" BASIS,
30
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
31
 * See the License for the specific language governing permissions and
32
 * limitations under the License.
33
 */
34
35
// T-Digest :  Percentile and Quantile Estimation of Big Data
36
// A new data structure for accurate on-line accumulation of rank-based statistics
37
// such as quantiles and trimmed means.
38
// See original paper: "Computing extremely accurate quantiles using t-digest"
39
// by Ted Dunning and Otmar Ertl for more details
40
// https://github.com/tdunning/t-digest/blob/07b8f2ca2be8d0a9f04df2feadad5ddc1bb73c88/docs/t-digest-paper/histo.pdf.
41
// https://github.com/derrickburns/tdigest
42
43
#pragma once
44
45
#include <pdqsort.h>
46
47
#include <algorithm>
48
#include <cfloat>
49
#include <cmath>
50
#include <iostream>
51
#include <memory>
52
#include <queue>
53
#include <utility>
54
#include <vector>
55
56
#include "common/factory_creator.h"
57
#include "common/logging.h"
58
59
namespace doris {
60
61
using Value = float;
62
using Weight = float;
63
using Index = size_t;
64
65
constexpr size_t K_HIGH_WATER = 40000;
66
67
class Centroid {
68
public:
69
254k
    Centroid() : Centroid(0.0, 0.0) {}
70
71
388k
    Centroid(Value mean, Weight weight) : _mean(mean), _weight(weight) {}
72
73
29.1M
    Value mean() const noexcept { return _mean; }
74
75
19.4M
    Weight weight() const noexcept { return _weight; }
76
77
5.32k
    Value& mean() noexcept { return _mean; }
78
79
3.73k
    Weight& weight() noexcept { return _weight; }
80
81
1.00M
    void add(const Centroid& c) {
82
1.00M
        DCHECK_GT(c._weight, 0);
83
1.00M
        if (_weight != 0.0) {
84
1.00M
            _weight += c._weight;
85
1.00M
            _mean += c._weight * (c._mean - _mean) / _weight;
86
18.4E
        } else {
87
18.4E
            _weight = c._weight;
88
18.4E
            _mean = c._mean;
89
18.4E
        }
90
1.00M
    }
91
92
private:
93
    Value _mean = 0;
94
    Weight _weight = 0;
95
};
96
97
struct CentroidList {
98
788
    CentroidList(const std::vector<Centroid>& s) : iter(s.cbegin()), end(s.cend()) {}
99
    std::vector<Centroid>::const_iterator iter;
100
    std::vector<Centroid>::const_iterator end;
101
102
1.89M
    bool advance() { return ++iter != end; }
103
};
104
105
class CentroidListComparator {
106
public:
107
    CentroidListComparator() = default;
108
109
1.76M
    bool operator()(const CentroidList& left, const CentroidList& right) const {
110
1.76M
        return left.iter->mean() > right.iter->mean();
111
1.76M
    }
112
};
113
114
using CentroidListQueue =
115
        std::priority_queue<CentroidList, std::vector<CentroidList>, CentroidListComparator>;
116
117
struct CentroidComparator {
118
13.0M
    bool operator()(const Centroid& a, const Centroid& b) const { return a.mean() < b.mean(); }
119
};
120
121
class TDigest {
122
    ENABLE_FACTORY_CREATOR(TDigest);
123
124
    class TDigestComparator {
125
    public:
126
        TDigestComparator() = default;
127
128
0
        bool operator()(const TDigest* left, const TDigest* right) const {
129
0
            return left->total_size() > right->total_size();
130
0
        }
131
    };
132
    using TDigestQueue =
133
            std::priority_queue<const TDigest*, std::vector<const TDigest*>, TDigestComparator>;
134
135
public:
136
0
    TDigest() : TDigest(10000) {}
137
138
3.55k
    explicit TDigest(Value compression) : TDigest(compression, 0) {}
139
140
3.55k
    TDigest(Value compression, Index buffer_size) : TDigest(compression, buffer_size, 0) {}
141
142
    TDigest(Value compression, Index unmerged_size, Index merged_size)
143
3.55k
            : _compression(compression),
144
3.55k
              _max_processed(processed_size(merged_size, compression)),
145
3.55k
              _max_unprocessed(unprocessed_size(unmerged_size, compression)) {
146
3.55k
        _processed.reserve(_max_processed);
147
3.55k
        _unprocessed.reserve(_max_unprocessed + 1);
148
3.55k
    }
149
150
    TDigest(std::vector<Centroid>&& processed, std::vector<Centroid>&& unprocessed,
151
            Value compression, Index unmerged_size, Index merged_size)
152
0
            : TDigest(compression, unmerged_size, merged_size) {
153
0
        _processed = std::move(processed);
154
0
        _unprocessed = std::move(unprocessed);
155
0
156
0
        _processed_weight = weight(_processed);
157
0
        _unprocessed_weight = weight(_unprocessed);
158
0
        if (_processed.size() > 0) {
159
0
            _min = std::min(_min, _processed[0].mean());
160
0
            _max = std::max(_max, (_processed.cend() - 1)->mean());
161
0
        }
162
0
        _update_cumulative();
163
0
    }
164
165
0
    static Weight weight(std::vector<Centroid>& centroids) noexcept {
166
0
        Weight w = 0.0;
167
0
        for (auto centroid : centroids) {
168
0
            w += centroid.weight();
169
0
        }
170
0
        return w;
171
0
    }
172
173
1.02k
    TDigest(const TDigest&) = default;
174
175
    // Compact an independently owned result, leaving no spare write buffer.
176
1.00k
    void compact() {
177
1.00k
        if (have_unprocessed()) {
178
1.00k
            compress();
179
1.00k
        }
180
1.00k
        std::vector<Centroid>().swap(_unprocessed);
181
1.00k
        _processed.shrink_to_fit();
182
1.00k
        _cumulative.shrink_to_fit();
183
1.00k
    }
184
185
1.02k
    size_t allocated_bytes() const {
186
1.02k
        return (_processed.capacity() + _unprocessed.capacity()) * sizeof(Centroid) +
187
1.02k
               _cumulative.capacity() * sizeof(Weight);
188
1.02k
    }
189
190
0
    TDigest& operator=(TDigest&& o) {
191
0
        _compression = o._compression;
192
0
        _max_processed = o._max_processed;
193
0
        _max_unprocessed = o._max_unprocessed;
194
0
        _processed_weight = o._processed_weight;
195
0
        _unprocessed_weight = o._unprocessed_weight;
196
0
        _processed = std::move(o._processed);
197
0
        _unprocessed = std::move(o._unprocessed);
198
0
        _cumulative = std::move(o._cumulative);
199
0
        _min = o._min;
200
0
        _max = o._max;
201
0
        return *this;
202
0
    }
203
204
    TDigest(TDigest&& o)
205
            : TDigest(std::move(o._processed), std::move(o._unprocessed), o._compression,
206
0
                      o._max_unprocessed, o._max_processed) {}
207
208
3.55k
    static inline Index processed_size(Index size, Value compression) noexcept {
209
3.55k
        return (size == 0) ? static_cast<Index>(2 * std::ceil(compression)) : size;
210
3.55k
    }
211
212
3.55k
    static inline Index unprocessed_size(Index size, Value compression) noexcept {
213
3.55k
        return (size == 0) ? static_cast<Index>(8 * std::ceil(compression)) : size;
214
3.55k
    }
215
216
    // merge in another t-digest
217
1.25k
    void merge(const TDigest* other) {
218
1.25k
        std::vector<const TDigest*> others {other};
219
1.25k
        add(others.cbegin(), others.cend());
220
1.25k
    }
221
222
    const std::vector<Centroid>& processed() const { return _processed; }
223
224
    const std::vector<Centroid>& unprocessed() const { return _unprocessed; }
225
226
0
    Index max_unprocessed() const { return _max_unprocessed; }
227
228
0
    Index max_processed() const { return _max_processed; }
229
230
    void add(std::vector<const TDigest*> digests) { add(digests.cbegin(), digests.cend()); }
231
232
    // merge in a vector of tdigests in the most efficient manner possible
233
    // in constant space
234
    // works for any value of K_HIGH_WATER
235
    void add(std::vector<const TDigest*>::const_iterator iter,
236
1.25k
             std::vector<const TDigest*>::const_iterator end) {
237
1.25k
        if (iter != end) {
238
1.25k
            auto size = std::distance(iter, end);
239
1.25k
            TDigestQueue pq(TDigestComparator {});
240
2.50k
            for (; iter != end; iter++) {
241
1.25k
                pq.push((*iter));
242
1.25k
            }
243
1.25k
            std::vector<const TDigest*> batch;
244
1.25k
            batch.reserve(size);
245
246
1.25k
            size_t total_size = 0;
247
2.50k
            while (!pq.empty()) {
248
1.25k
                const auto* td = pq.top();
249
1.25k
                batch.push_back(td);
250
1.25k
                pq.pop();
251
1.25k
                total_size += td->total_size();
252
1.25k
                if (total_size >= K_HIGH_WATER || pq.empty()) {
253
1.25k
                    _merge_processed(batch);
254
1.25k
                    _merge_unprocessed(batch);
255
1.25k
                    _process_if_necessary();
256
1.25k
                    batch.clear();
257
1.25k
                    total_size = 0;
258
1.25k
                }
259
1.25k
            }
260
1.25k
            _update_cumulative();
261
1.25k
        }
262
1.25k
    }
263
264
0
    Weight processed_weight() const { return _processed_weight; }
265
266
0
    Weight unprocessed_weight() const { return _unprocessed_weight; }
267
268
3.47k
    bool have_unprocessed() const { return _unprocessed.size() > 0; }
269
270
3.76k
    size_t total_size() const { return _processed.size() + _unprocessed.size(); }
271
272
    long total_weight() const { return static_cast<long>(_processed_weight + _unprocessed_weight); }
273
274
    // return the cdf on the t-digest
275
    Value cdf(Value x) {
276
        if (have_unprocessed() || is_dirty()) {
277
            _process();
278
        }
279
        return cdf_processed(x);
280
    }
281
282
135k
    bool is_dirty() {
283
135k
        return _processed.size() > _max_processed || _unprocessed.size() > _max_unprocessed;
284
135k
    }
285
286
    // return the cdf on the processed values
287
0
    Value cdf_processed(Value x) const {
288
0
        VLOG_CRITICAL << "cdf value " << x;
289
0
        VLOG_CRITICAL << "processed size " << _processed.size();
290
0
        if (_processed.size() == 0) {
291
0
            // no data to examine
292
0
            VLOG_CRITICAL << "no processed values";
293
0
294
0
            return 0.0;
295
0
        } else if (_processed.size() == 1) {
296
0
            VLOG_CRITICAL << "one processed value "
297
0
                          << " _min " << _min << " _max " << _max;
298
0
            // exactly one centroid, should have _max==_min
299
0
            auto width = _max - _min;
300
0
            if (x < _min) {
301
0
                return 0.0;
302
0
            } else if (x > _max) {
303
0
                return 1.0;
304
0
            } else if (x - _min <= width) {
305
0
                // _min and _max are too close together to do any viable interpolation
306
0
                return 0.5;
307
0
            } else {
308
0
                // interpolate if somehow we have weight > 0 and _max != _min
309
0
                return (x - _min) / (_max - _min);
310
0
            }
311
0
        } else {
312
0
            auto n = _processed.size();
313
0
            if (x <= _min) {
314
0
                VLOG_CRITICAL << "below _min "
315
0
                              << " _min " << _min << " x " << x;
316
0
                return 0;
317
0
            }
318
0
319
0
            if (x >= _max) {
320
0
                VLOG_CRITICAL << "above _max "
321
0
                              << " _max " << _max << " x " << x;
322
0
                return 1;
323
0
            }
324
0
325
0
            // check for the left tail
326
0
            if (x <= _mean(0)) {
327
0
                VLOG_CRITICAL << "left tail "
328
0
                              << " _min " << _min << " mean(0) " << _mean(0) << " x " << x;
329
0
330
0
                // note that this is different than mean(0) > _min ... this guarantees interpolation works
331
0
                if (_mean(0) - _min > 0) {
332
0
                    return static_cast<Value>((x - _min) / (_mean(0) - _min) * _weight(0) /
333
0
                                              _processed_weight / 2.0);
334
0
                } else {
335
0
                    return 0;
336
0
                }
337
0
            }
338
0
339
0
            // and the right tail
340
0
            if (x >= _mean(n - 1)) {
341
0
                VLOG_CRITICAL << "right tail"
342
0
                              << " _max " << _max << " mean(n - 1) " << _mean(n - 1) << " x " << x;
343
0
344
0
                if (_max - _mean(n - 1) > 0) {
345
0
                    return static_cast<Value>(1.0 - (_max - x) / (_max - _mean(n - 1)) *
346
0
                                                            _weight(n - 1) / _processed_weight /
347
0
                                                            2.0);
348
0
                } else {
349
0
                    return 1;
350
0
                }
351
0
            }
352
0
353
0
            CentroidComparator cc;
354
0
            auto iter =
355
0
                    std::upper_bound(_processed.cbegin(), _processed.cend(), Centroid(x, 0), cc);
356
0
357
0
            auto i = std::distance(_processed.cbegin(), iter);
358
0
            auto z1 = x - (iter - 1)->mean();
359
0
            auto z2 = (iter)->mean() - x;
360
0
            DCHECK_LE(0.0, z1);
361
0
            DCHECK_LE(0.0, z2);
362
0
            VLOG_CRITICAL << "middle "
363
0
                          << " z1 " << z1 << " z2 " << z2 << " x " << x;
364
0
365
0
            return _weighted_average(_cumulative[i - 1], z2, _cumulative[i], z1) /
366
0
                   _processed_weight;
367
0
        }
368
0
    }
369
370
    // this returns a quantile on the t-digest
371
830
    Value quantile(Value q) {
372
830
        if (have_unprocessed() || is_dirty()) {
373
618
            _process();
374
618
        }
375
830
        return quantile_processed(q);
376
830
    }
377
378
    void quantiles(const double* quantile_levels, const size_t* permutation, size_t size,
379
427
                   double* result) {
380
427
        if (size == 0) {
381
0
            return;
382
0
        }
383
427
        if (have_unprocessed() || is_dirty()) {
384
389
            _process();
385
389
        }
386
387
427
        if (_processed.empty()) {
388
1
            std::fill(result, result + size, NAN);
389
1
            return;
390
1
        }
391
392
426
        if (_processed.size() == 1) {
393
359
            std::fill(result, result + size, static_cast<double>(_mean(0)));
394
359
            return;
395
359
        }
396
397
67
        const auto n = _processed.size();
398
67
        size_t cumulative_index = 0;
399
201
        for (size_t result_index = 0; result_index < size; ++result_index) {
400
134
            const size_t level_index = permutation[result_index];
401
134
            const auto q = static_cast<Value>(quantile_levels[level_index]);
402
134
            DCHECK_GE(q, 0);
403
134
            DCHECK_LE(q, 1);
404
405
134
            const auto index = q * _processed_weight;
406
134
            if (index <= _weight(0) / 2.0) {
407
39
                DCHECK_GT(_weight(0), 0);
408
39
                result[level_index] =
409
39
                        static_cast<Value>(_min + 2.0 * index / _weight(0) * (_mean(0) - _min));
410
39
                continue;
411
39
            }
412
413
1.33k
            while (cumulative_index < _cumulative.size() && _cumulative[cumulative_index] < index) {
414
1.24k
                ++cumulative_index;
415
1.24k
            }
416
417
95
            if (cumulative_index > 0 && cumulative_index + 1 < _cumulative.size()) {
418
85
                auto z1 = index - _cumulative[cumulative_index - 1];
419
85
                auto z2 = _cumulative[cumulative_index] - index;
420
85
                result[level_index] = static_cast<double>(_weighted_average(
421
85
                        _mean(cumulative_index - 1), z2, _mean(cumulative_index), z1));
422
85
                continue;
423
85
            }
424
425
95
            DCHECK_LE(index, _processed_weight);
426
10
            DCHECK_GE(index, _processed_weight - _weight(n - 1) / 2.0);
427
10
            auto z1 = static_cast<Value>(index - _processed_weight - _weight(n - 1) / 2.0);
428
10
            auto z2 = static_cast<Value>(_weight(n - 1) / 2 - z1);
429
10
            result[level_index] =
430
10
                    static_cast<double>(_weighted_average(_mean(n - 1), z1, _max, z2));
431
10
        }
432
67
    }
433
434
    // this returns a quantile on the currently processed values without changing the t-digest
435
    // the value will not represent the unprocessed values
436
1.68k
    Value quantile_processed(Value q) const {
437
1.68k
        if (q < 0 || q > 1) {
438
0
            VLOG_CRITICAL << "q should be in [0,1], got " << q;
439
0
            return NAN;
440
0
        }
441
442
1.68k
        if (_processed.size() == 0) {
443
            // no sorted means no data, no way to get a quantile
444
121
            return NAN;
445
1.56k
        } else if (_processed.size() == 1) {
446
            // with one data point, all quantiles lead to Rome
447
448
325
            return _mean(0);
449
325
        }
450
451
        // we know that there are at least two sorted now
452
1.23k
        auto n = _processed.size();
453
454
        // if values were stored in a sorted array, index would be the offset we are Weighterested in
455
1.23k
        const auto index = q * _processed_weight;
456
457
        // at the boundaries, we return _min or _max
458
1.23k
        if (index <= _weight(0) / 2.0) {
459
50
            DCHECK_GT(_weight(0), 0);
460
50
            return static_cast<Value>(_min + 2.0 * index / _weight(0) * (_mean(0) - _min));
461
50
        }
462
463
1.18k
        auto iter = std::lower_bound(_cumulative.cbegin(), _cumulative.cend(), index);
464
465
1.19k
        if (iter != _cumulative.cend() && iter != _cumulative.cbegin() &&
466
1.19k
            iter + 1 != _cumulative.cend()) {
467
498
            auto i = std::distance(_cumulative.cbegin(), iter);
468
498
            auto z1 = index - *(iter - 1);
469
498
            auto z2 = *(iter)-index;
470
            // VLOG_CRITICAL << "z2 " << z2 << " index " << index << " z1 " << z1;
471
498
            return _weighted_average(_mean(i - 1), z2, _mean(i), z1);
472
498
        }
473
474
1.18k
        DCHECK_LE(index, _processed_weight);
475
690
        DCHECK_GE(index, _processed_weight - _weight(n - 1) / 2.0);
476
477
690
        auto z1 = static_cast<Value>(index - _processed_weight - _weight(n - 1) / 2.0);
478
690
        auto z2 = static_cast<Value>(_weight(n - 1) / 2 - z1);
479
690
        return _weighted_average(_mean(n - 1), z1, _max, z2);
480
1.18k
    }
481
482
0
    Value compression() const { return _compression; }
483
484
124k
    void add(Value x) { add(x, 1); }
485
486
1.13k
    void compress() { _process(); }
487
488
    // add a single centroid to the unprocessed vector, processing previously unprocessed sorted if our limit has
489
    // been reached.
490
134k
    bool add(Value x, Weight w) {
491
134k
        if (std::isnan(x)) {
492
478
            return false;
493
478
        }
494
134k
        _unprocessed.emplace_back(x, w);
495
134k
        _unprocessed_weight += w;
496
134k
        _process_if_necessary();
497
134k
        return true;
498
134k
    }
499
500
    void add(std::vector<Centroid>::const_iterator iter,
501
0
             std::vector<Centroid>::const_iterator end) {
502
0
        while (iter != end) {
503
0
            const size_t diff = std::distance(iter, end);
504
0
            const size_t room = _max_unprocessed - _unprocessed.size();
505
0
            auto mid = iter + std::min(diff, room);
506
0
            while (iter != mid) {
507
0
                _unprocessed.push_back(*(iter++));
508
0
            }
509
0
            if (_unprocessed.size() >= _max_unprocessed) {
510
0
                _process();
511
0
            }
512
0
        }
513
0
    }
514
515
1.86k
    uint32_t serialized_size() {
516
1.86k
        return static_cast<uint32_t>(sizeof(uint32_t) + sizeof(Value) * 5 + sizeof(Index) * 2 +
517
1.86k
                                     sizeof(uint32_t) * 3 + _processed.size() * sizeof(Centroid) +
518
1.86k
                                     _unprocessed.size() * sizeof(Centroid) +
519
1.86k
                                     _cumulative.size() * sizeof(Weight));
520
1.86k
    }
521
522
930
    size_t serialize(uint8_t* writer) {
523
930
        uint8_t* dst = writer;
524
930
        uint32_t total_size = serialized_size();
525
930
        memcpy(writer, &total_size, sizeof(uint32_t));
526
930
        writer += sizeof(uint32_t);
527
930
        memcpy(writer, &_compression, sizeof(Value));
528
930
        writer += sizeof(Value);
529
930
        memcpy(writer, &_min, sizeof(Value));
530
930
        writer += sizeof(Value);
531
930
        memcpy(writer, &_max, sizeof(Value));
532
930
        writer += sizeof(Value);
533
930
        memcpy(writer, &_max_processed, sizeof(Index));
534
930
        writer += sizeof(Index);
535
930
        memcpy(writer, &_max_unprocessed, sizeof(Index));
536
930
        writer += sizeof(Index);
537
930
        memcpy(writer, &_processed_weight, sizeof(Value));
538
930
        writer += sizeof(Value);
539
930
        memcpy(writer, &_unprocessed_weight, sizeof(Value));
540
930
        writer += sizeof(Value);
541
542
930
        auto size = static_cast<uint32_t>(_processed.size());
543
930
        memcpy(writer, &size, sizeof(uint32_t));
544
930
        writer += sizeof(uint32_t);
545
250k
        for (int i = 0; i < size; i++) {
546
249k
            memcpy(writer, &_processed[i], sizeof(Centroid));
547
249k
            writer += sizeof(Centroid);
548
249k
        }
549
550
930
        size = static_cast<uint32_t>(_unprocessed.size());
551
930
        memcpy(writer, &size, sizeof(uint32_t));
552
930
        writer += sizeof(uint32_t);
553
        //TODO(weixiang): may be once memcpy is enough!
554
6.19k
        for (int i = 0; i < size; i++) {
555
5.26k
            memcpy(writer, &_unprocessed[i], sizeof(Centroid));
556
5.26k
            writer += sizeof(Centroid);
557
5.26k
        }
558
559
930
        size = static_cast<uint32_t>(_cumulative.size());
560
930
        memcpy(writer, &size, sizeof(uint32_t));
561
930
        writer += sizeof(uint32_t);
562
250k
        for (int i = 0; i < size; i++) {
563
249k
            memcpy(writer, &_cumulative[i], sizeof(Weight));
564
249k
            writer += sizeof(Weight);
565
249k
        }
566
930
        return writer - dst;
567
930
    }
568
569
919
    void unserialize(const uint8_t* type_reader) {
570
919
        uint32_t total_length = 0;
571
919
        memcpy(&total_length, type_reader, sizeof(uint32_t));
572
919
        type_reader += sizeof(uint32_t);
573
919
        memcpy(&_compression, type_reader, sizeof(Value));
574
919
        type_reader += sizeof(Value);
575
919
        memcpy(&_min, type_reader, sizeof(Value));
576
919
        type_reader += sizeof(Value);
577
919
        memcpy(&_max, type_reader, sizeof(Value));
578
919
        type_reader += sizeof(Value);
579
580
919
        memcpy(&_max_processed, type_reader, sizeof(Index));
581
919
        type_reader += sizeof(Index);
582
919
        memcpy(&_max_unprocessed, type_reader, sizeof(Index));
583
919
        type_reader += sizeof(Index);
584
919
        memcpy(&_processed_weight, type_reader, sizeof(Value));
585
919
        type_reader += sizeof(Value);
586
919
        memcpy(&_unprocessed_weight, type_reader, sizeof(Value));
587
919
        type_reader += sizeof(Value);
588
589
919
        uint32_t size;
590
919
        memcpy(&size, type_reader, sizeof(uint32_t));
591
919
        type_reader += sizeof(uint32_t);
592
919
        _processed.resize(size);
593
250k
        for (int i = 0; i < size; i++) {
594
249k
            memcpy(&_processed[i], type_reader, sizeof(Centroid));
595
249k
            type_reader += sizeof(Centroid);
596
249k
        }
597
919
        memcpy(&size, type_reader, sizeof(uint32_t));
598
919
        type_reader += sizeof(uint32_t);
599
919
        _unprocessed.resize(size);
600
6.11k
        for (int i = 0; i < size; i++) {
601
5.19k
            memcpy(&_unprocessed[i], type_reader, sizeof(Centroid));
602
5.19k
            type_reader += sizeof(Centroid);
603
5.19k
        }
604
919
        memcpy(&size, type_reader, sizeof(uint32_t));
605
919
        type_reader += sizeof(uint32_t);
606
919
        _cumulative.resize(size);
607
250k
        for (int i = 0; i < size; i++) {
608
249k
            memcpy(&_cumulative[i], type_reader, sizeof(Weight));
609
249k
            type_reader += sizeof(Weight);
610
249k
        }
611
919
    }
612
613
private:
614
    Value _compression;
615
616
    Value _min = std::numeric_limits<Value>::max();
617
618
    // min() is the smallest positive value, so use lowest() for all-negative input,
619
    // e.g. {-3, -2, -1} must set _max to -1.
620
    Value _max = std::numeric_limits<Value>::lowest();
621
622
    Index _max_processed;
623
624
    Index _max_unprocessed;
625
626
    Value _processed_weight = 0.0;
627
628
    Value _unprocessed_weight = 0.0;
629
630
    std::vector<Centroid> _processed;
631
632
    std::vector<Centroid> _unprocessed;
633
634
    std::vector<Weight> _cumulative;
635
636
    // return mean of i-th centroid
637
2.65k
    Value _mean(int64_t i) const noexcept { return _processed[i].mean(); }
638
639
    // return weight of i-th centroid
640
7.00M
    Weight _weight(int64_t i) const noexcept { return _processed[i].weight(); }
641
642
    // append all unprocessed centroids into current unprocessed vector
643
1.25k
    void _merge_unprocessed(const std::vector<const TDigest*>& tdigests) {
644
1.25k
        if (tdigests.size() == 0) {
645
0
            return;
646
0
        }
647
648
1.25k
        size_t total = _unprocessed.size();
649
1.25k
        for (const auto& td : tdigests) {
650
1.25k
            total += td->_unprocessed.size();
651
1.25k
        }
652
653
1.25k
        _unprocessed.reserve(total);
654
1.25k
        for (const auto& td : tdigests) {
655
1.25k
            _unprocessed.insert(_unprocessed.end(), td->_unprocessed.cbegin(),
656
1.25k
                                td->_unprocessed.cend());
657
1.25k
            _unprocessed_weight += td->_unprocessed_weight;
658
1.25k
        }
659
1.25k
    }
660
661
    // merge all processed centroids together into a single sorted vector
662
1.25k
    void _merge_processed(const std::vector<const TDigest*>& tdigests) {
663
1.25k
        if (tdigests.size() == 0) {
664
0
            return;
665
0
        }
666
667
1.25k
        size_t total = 0;
668
1.25k
        CentroidListQueue pq(CentroidListComparator {});
669
1.25k
        for (const auto& td : tdigests) {
670
1.25k
            const auto& sorted = td->_processed;
671
1.25k
            auto size = sorted.size();
672
1.25k
            if (size > 0) {
673
396
                pq.push(CentroidList(sorted));
674
396
                total += size;
675
396
                _processed_weight += td->_processed_weight;
676
396
            }
677
1.25k
        }
678
1.25k
        if (total == 0) {
679
857
            return;
680
857
        }
681
682
397
        if (_processed.size() > 0) {
683
395
            pq.push(CentroidList(_processed));
684
395
            total += _processed.size();
685
395
        }
686
687
397
        std::vector<Centroid> sorted;
688
397
        VLOG_CRITICAL << "total " << total;
689
397
        sorted.reserve(total);
690
691
1.93M
        while (!pq.empty()) {
692
1.93M
            auto best = pq.top();
693
1.93M
            pq.pop();
694
1.93M
            sorted.push_back(*(best.iter));
695
1.93M
            if (best.advance()) {
696
1.86M
                pq.push(best);
697
1.86M
            }
698
1.93M
        }
699
397
        _processed = std::move(sorted);
700
401
        if (_processed.size() > 0) {
701
401
            _min = std::min(_min, _processed[0].mean());
702
401
            _max = std::max(_max, (_processed.cend() - 1)->mean());
703
401
        }
704
397
    }
705
706
135k
    void _process_if_necessary() {
707
135k
        if (is_dirty()) {
708
400
            _process();
709
400
        }
710
135k
    }
711
712
3.80k
    void _update_cumulative() {
713
3.80k
        const auto n = _processed.size();
714
3.80k
        _cumulative.clear();
715
3.80k
        _cumulative.reserve(n + 1);
716
3.80k
        Weight previous = 0.0;
717
6.99M
        for (Index i = 0; i < n; i++) {
718
6.99M
            Weight current = _weight(i);
719
6.99M
            auto half_current = static_cast<Weight>(current / 2.0);
720
6.99M
            _cumulative.push_back(previous + half_current);
721
6.99M
            previous = previous + current;
722
6.99M
        }
723
3.80k
        _cumulative.push_back(previous);
724
3.80k
    }
725
726
    // merges _unprocessed centroids and _processed centroids together and processes them
727
    // when complete, _unprocessed will be empty and _processed will have at most _max_processed centroids
728
2.54k
    void _process() {
729
2.54k
        CentroidComparator cc;
730
        // select percentile_approx(lo_orderkey,0.5) from lineorder;
731
        // have test pdqsort and RadixSort, find here pdqsort performance is better when data is struct Centroid
732
        // But when sort plain type like int/float of std::vector<T>, find RadixSort is better
733
2.54k
        pdqsort(_unprocessed.begin(), _unprocessed.end(), cc);
734
2.54k
        auto count = _unprocessed.size();
735
2.54k
        _unprocessed.insert(_unprocessed.end(), _processed.cbegin(), _processed.cend());
736
2.54k
        std::inplace_merge(_unprocessed.begin(), _unprocessed.begin() + count, _unprocessed.end(),
737
2.54k
                           cc);
738
739
2.54k
        _processed_weight += _unprocessed_weight;
740
2.54k
        _unprocessed_weight = 0;
741
2.54k
        _processed.clear();
742
743
2.54k
        _processed.push_back(_unprocessed[0]);
744
2.54k
        Weight w_so_far = _unprocessed[0].weight();
745
2.54k
        Weight w_limit = _processed_weight * _integrated_q(1.0);
746
747
2.54k
        auto end = _unprocessed.end();
748
6.94M
        for (auto iter = _unprocessed.cbegin() + 1; iter < end; iter++) {
749
6.94M
            const auto& centroid = *iter;
750
6.94M
            Weight projected_w = w_so_far + centroid.weight();
751
6.94M
            if (projected_w <= w_limit) {
752
997k
                w_so_far = projected_w;
753
997k
                (_processed.end() - 1)->add(centroid);
754
5.94M
            } else {
755
5.94M
                auto k1 = _integrated_location(w_so_far / _processed_weight);
756
5.94M
                w_limit = _processed_weight * _integrated_q(static_cast<Value>(k1 + 1.0));
757
5.94M
                w_so_far += centroid.weight();
758
5.94M
                _processed.emplace_back(centroid);
759
5.94M
            }
760
6.94M
        }
761
2.54k
        _unprocessed.clear();
762
2.54k
        _min = std::min(_min, _processed[0].mean());
763
2.54k
        VLOG_CRITICAL << "new _min " << _min;
764
2.54k
        _max = std::max(_max, (_processed.cend() - 1)->mean());
765
2.54k
        VLOG_CRITICAL << "new _max " << _max;
766
2.54k
        _update_cumulative();
767
2.54k
    }
768
769
0
    size_t _check_weights(const std::vector<Centroid>& sorted, Value total) {
770
0
        size_t bad_weight = 0;
771
0
        auto k1 = 0.0;
772
0
        auto q = 0.0;
773
0
        for (auto iter = sorted.cbegin(); iter != sorted.cend(); iter++) {
774
0
            auto w = iter->weight();
775
0
            auto dq = w / total;
776
0
            auto k2 = _integrated_location(static_cast<Value>(q + dq));
777
0
            if (k2 - k1 > 1 && w != 1) {
778
0
                VLOG_CRITICAL << "Oversize centroid at " << std::distance(sorted.cbegin(), iter)
779
0
                              << " k1 " << k1 << " k2 " << k2 << " dk " << (k2 - k1) << " w " << w
780
0
                              << " q " << q;
781
0
                bad_weight++;
782
0
            }
783
0
            if (k2 - k1 > 1.5 && w != 1) {
784
0
                VLOG_CRITICAL << "Egregiously Oversize centroid at "
785
0
                              << std::distance(sorted.cbegin(), iter) << " k1 " << k1 << " k2 "
786
0
                              << k2 << " dk " << (k2 - k1) << " w " << w << " q " << q;
787
0
                bad_weight++;
788
0
            }
789
0
            q += dq;
790
0
            k1 = k2;
791
0
        }
792
0
793
0
        return bad_weight;
794
0
    }
795
796
    /**
797
    * Converts a quantile into a centroid scale value.  The centroid scale is nomin_ally
798
    * the number k of the centroid that a quantile point q should belong to.  Due to
799
    * round-offs, however, we can't align things perfectly without splitting points
800
    * and sorted.  We don't want to do that, so we have to allow for offsets.
801
    * In the end, the criterion is that any quantile range that spans a centroid
802
    * scale range more than one should be split across more than one centroid if
803
    * possible.  This won't be possible if the quantile range refers to a single point
804
    * or an already existing centroid.
805
    * <p/>
806
    * This mapping is steep near q=0 or q=1 so each centroid there will correspond to
807
    * less q range.  Near q=0.5, the mapping is flatter so that sorted there will
808
    * represent a larger chunk of quantiles.
809
    *
810
    * @param q The quantile scale value to be mapped.
811
    * @return The centroid scale value corresponding to q.
812
    */
813
5.98M
    Value _integrated_location(Value q) const {
814
5.98M
        return static_cast<Value>(_compression * (std::asin(2.0 * q - 1.0) + M_PI / 2) / M_PI);
815
5.98M
    }
816
817
5.99M
    Value _integrated_q(Value k) const {
818
5.99M
        return static_cast<Value>(
819
5.99M
                (std::sin(std::min(k, _compression) * M_PI / _compression - M_PI / 2) + 1) / 2);
820
5.99M
    }
821
822
    /**
823
     * Same as {@link #_weighted_average_sorted(Value, Value, Value, Value)} but flips
824
     * the order of the variables if <code>x2</code> is greater than
825
     * <code>x1</code>.
826
    */
827
1.29k
    static Value _weighted_average(Value x1, Value w1, Value x2, Value w2) {
828
1.29k
        return (x1 <= x2) ? _weighted_average_sorted(x1, w1, x2, w2)
829
18.4E
                          : _weighted_average_sorted(x2, w2, x1, w1);
830
1.29k
    }
831
832
    /**
833
    * Compute the weighted average between <code>x1</code> with a weight of
834
    * <code>w1</code> and <code>x2</code> with a weight of <code>w2</code>.
835
    * This expects <code>x1</code> to be less than or equal to <code>x2</code>
836
    * and is guaranteed to return a number between <code>x1</code> and
837
    * <code>x2</code>.
838
    */
839
1.29k
    static Value _weighted_average_sorted(Value x1, Value w1, Value x2, Value w2) {
840
1.29k
        DCHECK_LE(x1, x2);
841
1.29k
        const Value x = (x1 * w1 + x2 * w2) / (w1 + w2);
842
1.29k
        return std::max(x1, std::min(x, x2));
843
1.29k
    }
844
845
0
    static Value _interpolate(Value x, Value x0, Value x1) { return (x - x0) / (x1 - x0); }
846
847
    /**
848
    * Computes an interpolated value of a quantile that is between two sorted.
849
    *
850
    * Index is the quantile desired multiplied by the total number of samples - 1.
851
    *
852
    * @param index              Denormalized quantile desired
853
    * @param previous_index     The denormalized quantile corresponding to the center of the previous centroid.
854
    * @param next_index         The denormalized quantile corresponding to the center of the following centroid.
855
    * @param previous_mean      The mean of the previous centroid.
856
    * @param next_mean          The mean of the following centroid.
857
    * @return  The interpolated mean.
858
    */
859
    static Value _quantile(Value index, Value previous_index, Value next_index, Value previous_mean,
860
0
                           Value next_mean) {
861
0
        const auto delta = next_index - previous_index;
862
0
        const auto previous_weight = (next_index - index) / delta;
863
0
        const auto next_weight = (index - previous_index) / delta;
864
0
        return previous_mean * previous_weight + next_mean * next_weight;
865
0
    }
866
};
867
} // namespace doris