blob: 2b004591c709c70f6e87859f88656947ee0909ee [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
#pragma once
#include <cstdint>
#include <cstring>
#include <map>
#include <string>
#include <utility>
#include "vec/common/hash_table/phmap_fwd_decl.h"
namespace doris {
#include "common/compile_check_begin.h"
struct Slice;
inline const int HLL_COLUMN_PRECISION = 14;
inline const int HLL_ZERO_COUNT_BITS = (64 - HLL_COLUMN_PRECISION);
inline const int HLL_EXPLICIT_INT64_NUM = 160;
inline const int HLL_SPARSE_THRESHOLD = 4096;
inline const uint16_t HLL_REGISTERS_COUNT = 16 * 1024;
// maximum size in byte of serialized HLL: type(1) + registers (2^14)
inline const int HLL_COLUMN_DEFAULT_LEN = HLL_REGISTERS_COUNT + 1;
// 1 for type; 1 for hash values count; 8 for hash value
inline const int HLL_SINGLE_VALUE_SIZE = 10;
inline const int HLL_EMPTY_SIZE = 1;
// Hyperloglog distinct estimate algorithm.
// See these papers for more details.
// 1) Hyperloglog: The analysis of a near-optimal cardinality estimation
// algorithm (2007)
// 2) HyperLogLog in Practice (paper from google with some improvements)
// Each HLL value is a set of values. To save space, Doris store HLL value
// in different format according to its cardinality.
//
// HLL_DATA_EMPTY: when set is empty.
//
// HLL_DATA_EXPLICIT: when there is only few values in set, store these values explicit.
// If the number of hash values is not greater than 160, set is encoded in this format.
// The max space occupied is (1 + 1 + 160 * 8) = 1282. I don't know why 160 is chosen,
// maybe can be other number. If you are interested, you can try other number and see
// if it will be better.
//
// HLL_DATA_SPARSE: only store non-zero registers. If the number of non-zero registers
// is not greater than 4096, set is encoded in this format. The max space occupied is
// (1 + 4 + 3 * 4096) = 12293.
//
// HLL_DATA_FULL: most space-consuming, store all registers
//
// A HLL value will change in the sequence empty -> explicit -> sparse -> full, and not
// allow reverse.
//
// NOTE: This values are persisted in storage devices, so don't change exist
// enum values.
enum HllDataType {
HLL_DATA_EMPTY = 0,
HLL_DATA_EXPLICIT = 1,
HLL_DATA_SPARSE = 2,
HLL_DATA_FULL = 3,
};
class HyperLogLog {
public:
HyperLogLog() = default;
explicit HyperLogLog(uint64_t hash_value) : _type(HLL_DATA_EXPLICIT) {
_hash_set.emplace(hash_value);
}
explicit HyperLogLog(const Slice& src);
HyperLogLog(const HyperLogLog& other) {
this->_type = other._type;
switch (other._type) {
case HLL_DATA_EMPTY:
break;
case HLL_DATA_EXPLICIT: {
this->_hash_set = other._hash_set;
break;
}
case HLL_DATA_SPARSE:
case HLL_DATA_FULL: {
_registers = new uint8_t[HLL_REGISTERS_COUNT];
memcpy(_registers, other._registers, HLL_REGISTERS_COUNT);
break;
}
default:
break;
}
}
HyperLogLog(HyperLogLog&& other) noexcept {
this->_type = other._type;
switch (other._type) {
case HLL_DATA_EMPTY:
break;
case HLL_DATA_EXPLICIT: {
this->_hash_set = std::move(other._hash_set);
other._type = HLL_DATA_EMPTY;
break;
}
case HLL_DATA_SPARSE:
case HLL_DATA_FULL: {
this->_registers = other._registers;
other._registers = nullptr;
other._type = HLL_DATA_EMPTY;
break;
}
default:
break;
}
}
HyperLogLog& operator=(HyperLogLog&& other) noexcept {
if (this != &other) {
if (_registers != nullptr) {
delete[] _registers;
_registers = nullptr;
}
this->_type = other._type;
switch (other._type) {
case HLL_DATA_EMPTY:
break;
case HLL_DATA_EXPLICIT: {
this->_hash_set = std::move(other._hash_set);
other._type = HLL_DATA_EMPTY;
break;
}
case HLL_DATA_SPARSE:
case HLL_DATA_FULL: {
this->_registers = other._registers;
other._registers = nullptr;
other._type = HLL_DATA_EMPTY;
break;
}
default:
break;
}
}
return *this;
}
HyperLogLog& operator=(const HyperLogLog& other) {
if (this != &other) {
if (_registers != nullptr) {
delete[] _registers;
_registers = nullptr;
}
this->_type = other._type;
switch (other._type) {
case HLL_DATA_EMPTY:
break;
case HLL_DATA_EXPLICIT: {
this->_hash_set = other._hash_set;
break;
}
case HLL_DATA_SPARSE:
case HLL_DATA_FULL: {
_registers = new uint8_t[HLL_REGISTERS_COUNT];
memcpy(_registers, other._registers, HLL_REGISTERS_COUNT);
break;
}
default:
break;
}
}
return *this;
}
~HyperLogLog() { clear(); }
void clear() {
_type = HLL_DATA_EMPTY;
_hash_set.clear();
delete[] _registers;
_registers = nullptr;
}
using SetTypeValueType = uint8_t;
using SparseLengthValueType = int32_t;
using SparseIndexType = uint16_t;
using SparseValueType = uint8_t;
// Add a hash value to this HLL value
// NOTE: input must be a hash_value
void update(uint64_t hash_value);
void merge(const HyperLogLog& other);
// Return max size of serialized binary
size_t max_serialized_size() const;
size_t memory_consumed() const {
size_t size = sizeof(*this);
if (_type == HLL_DATA_EXPLICIT) {
size += _hash_set.size() * sizeof(uint64_t);
} else if (_type == HLL_DATA_SPARSE || _type == HLL_DATA_FULL) {
size += HLL_REGISTERS_COUNT;
}
return size;
}
// Input slice should has enough capacity for serialize, which
// can be get through max_serialized_size(). If insufficient buffer
// is given, this will cause process crash.
// Return actual size of serialized binary.
size_t serialize(uint8_t* dst) const;
// Now, only empty HLL support this function.
bool deserialize(const Slice& slice);
int64_t estimate_cardinality() const;
static HyperLogLog empty() { return HyperLogLog {}; }
// Check if input slice is a valid serialized binary of HyperLogLog.
// This function only check the encoded type in slice, whose complex
// function is O(1).
static bool is_valid(const Slice& slice);
// only for debug
std::string to_string() const {
switch (_type) {
case HLL_DATA_EMPTY:
return {};
case HLL_DATA_EXPLICIT:
case HLL_DATA_SPARSE:
case HLL_DATA_FULL: {
std::string str {"hash set size: "};
str.append(std::to_string(_hash_set.size()));
str.append("\ncardinality:\t");
str.append(std::to_string(estimate_cardinality()));
str.append("\ntype:\t");
str.append(std::to_string(_type));
return str;
}
default:
return {};
}
}
private:
void _convert_explicit_to_register();
// update one hash value into this registers
void _update_registers(uint64_t hash_value) {
// Use the lower bits to index into the number of streams and then
// find the first 1 bit after the index bits.
int idx = hash_value % HLL_REGISTERS_COUNT;
hash_value >>= HLL_COLUMN_PRECISION;
// make sure max first_one_bit is HLL_ZERO_COUNT_BITS + 1
hash_value |= ((uint64_t)1 << HLL_ZERO_COUNT_BITS);
auto first_one_bit = uint8_t(__builtin_ctzl(hash_value) + 1);
_registers[idx] = (_registers[idx] < first_one_bit ? first_one_bit : _registers[idx]);
}
// absorb other registers into this registers
void _merge_registers(const uint8_t* other_registers) {
_do_simd_merge(_registers, other_registers);
}
void _do_simd_merge(uint8_t* __restrict registers, const uint8_t* __restrict other_registers) {
for (int i = 0; i < HLL_REGISTERS_COUNT; ++i) {
registers[i] = (registers[i] < other_registers[i] ? other_registers[i] : registers[i]);
}
}
HllDataType _type = HLL_DATA_EMPTY;
vectorized::flat_hash_set<uint64_t> _hash_set;
// This field is much space consuming(HLL_REGISTERS_COUNT), we create
// it only when it is really needed.
uint8_t* _registers = nullptr;
};
#include "common/compile_check_end.h"
} // namespace doris