redis基础数据结构(六) 基数统计

基数统计即统计一个数据集中不重复元素的个数,一种显然的实现是使用不相交集,缺陷是随着数据增加内存占用线性增加,海量数据下不可用;一种更常见的方法是使用B-树,所有数据在叶子节点保存,叶子节点在磁盘中,上层节点在内存中,因此占用内存的问题得到解决,查找时间O(logN),但是读取磁盘开销太大;最完美的方法是使用bitmap,因为bit是最小存储空间,可以保证内存占用最小。

以上都是准确基数排序的方法,使用bitmap是内存开销的极限,但是内存仍可以被优化,代价是牺牲基数统计的准确性。使用概率算法,有3个版本:

LC:linear counting,空间复杂度O(N)

LLC:loglog counting,空间复杂度O(loglogN)

HLL:hyperloglog,空间复杂度O(loglogN),使用调和平均替换了LLC中的几何平均数,误差比LLC小

HLL设计思想:

1.使用64bit哈希函数

2.使用16834个6bit寄存器,共计12288字节

3.在数据稀疏时,使用稀疏表示,密集时,使用密集表示

HLL头:

struct hllhdr {
    char magic[4];      /* "HYLL" */
    uint8_t encoding;   /* HLL_DENSE or HLL_SPARSE. */
    uint8_t notused[3]; /* Reserved for future use, must be zero. */
    uint8_t card[8];    /* Cached cardinality, little endian. */
    uint8_t registers[]; /* Data bytes. */
};

其中包括,4字节魔数,1字节flag控制密集和稀疏表示,3字节预留,8字节小端表示最近一次更新的缓存基数值,最高字节的最高位bit是标记位,用来标记基数是否被修改,registers是密集表示或者稀疏表示的数据部分

扫描二维码关注公众号,回复: 1873262 查看本文章

密集表示:

6bit按照小端排列,字节内部从最低有效位开始

稀疏表示:

使用3种微码:

ZERO:00xxxxxx,6bit用来表示连续设置为0的寄存器的个数,可以表示1个到64个

XZERO:10xxxxxx yyyyyyyy,14bit用来表示连续设置为0的寄存器的个数,可以表示1到16834个

VAL:1vvvvvxx,5bit用来表示寄存器的值,可以表示1到32,2bit用来表示连续设置为该值的寄存器个数

一个例子:XZERO:1000,VAL:2,1,ZERO:19,VAL:3,2,XZERO:15632,意思是,开始是1000个设置为0的寄存器,然后跟着一个寄存器被设置为2,接下来19个寄存器被设置为0,然后跟着两个设置为3的寄存器,最后剩余的寄存器全是0。在这个例子中,一共只用了2+1+1+1+2共计7个字节,是非常优秀的。但是稀疏表示的cpu耗时高,原因很简单,cpu访问内存,要先判断标识符,比如00表示这个字节是ZERO,10表示接下来2个字节是XZERO,然后再读取标识符后面的内存进行解析,效率不如密集表示。根据统计,基数平均达到10000时,稀疏表示的内存占用达到10591字节,接近密集表示,但是考虑cpu的开销,在基数3000以内的时候,使用稀疏表示,超过3000使用密集表示(3000时稀疏表示占用内存为4879字节)

稀疏表示中3种微码需要注意的一个问题是,微码值是从0到到全f,为了多表示一个value,加1是实际值(否则0没用了),比如VAL是10000001,xxxxx是00000,加1是1,即值为1,yy是01,加1是2,即表示连续两个寄存器被设置为1

相关宏定义如下,

/* The cached cardinality MSB is used to signal validity of the cached value. */
#define HLL_INVALIDATE_CACHE(hdr) (hdr)->card[7] |= (1<<7)
#define HLL_VALID_CACHE(hdr) (((hdr)->card[7] & (1<<7)) == 0)

#define HLL_P 14 /* The greater is P, the smaller the error. */
#define HLL_REGISTERS (1<<HLL_P) /* With P=14, 16384 registers. */
#define HLL_P_MASK (HLL_REGISTERS-1) /* Mask to index register. */
#define HLL_BITS 6 /* Enough to count up to 63 leading zeroes. */
#define HLL_REGISTER_MAX ((1<<HLL_BITS)-1)
#define HLL_HDR_SIZE sizeof(struct hllhdr)
#define HLL_DENSE_SIZE (HLL_HDR_SIZE+((HLL_REGISTERS*HLL_BITS+7)/8))
#define HLL_DENSE 0 /* Dense encoding. */
#define HLL_SPARSE 1 /* Sparse encoding. */
#define HLL_RAW 255 /* Only used internally, never exposed. */
#define HLL_MAX_ENCODING 1

hyperloglog.c中定义的用于bit计算的宏:

HLL_DANSE_GET_REGISTER:获取某个寄存器的值,需要提供寄存器起始地址和寄存器索引(0到16833)

HLL_DANSE_SET_REGISTER:设置某个寄存器的值,需要提供寄存器地址和寄存器索引和要设置的值(0到 31)


关于稀疏表示的宏:

#define HLL_SPARSE_XZERO_BIT 0x40 /* 01xxxxxx */
#define HLL_SPARSE_VAL_BIT 0x80 /* 1vvvvvxx */
#define HLL_SPARSE_IS_ZERO(p) (((*(p)) & 0xc0) == 0) /* 00xxxxxx */
#define HLL_SPARSE_IS_XZERO(p) (((*(p)) & 0xc0) == HLL_SPARSE_XZERO_BIT)
#define HLL_SPARSE_IS_VAL(p) ((*(p)) & HLL_SPARSE_VAL_BIT)
#define HLL_SPARSE_ZERO_LEN(p) (((*(p)) & 0x3f)+1)
#define HLL_SPARSE_XZERO_LEN(p) (((((*(p)) & 0x3f) << 8) | (*((p)+1)))+1)
#define HLL_SPARSE_VAL_VALUE(p) ((((*(p)) >> 2) & 0x1f)+1)
#define HLL_SPARSE_VAL_LEN(p) (((*(p)) & 0x3)+1)
#define HLL_SPARSE_VAL_MAX_VALUE 32
#define HLL_SPARSE_VAL_MAX_LEN 4
#define HLL_SPARSE_ZERO_MAX_LEN 64
#define HLL_SPARSE_XZERO_MAX_LEN 16384
#define HLL_SPARSE_VAL_SET(p,val,len) do { \
    *(p) = (((val)-1)<<2|((len)-1))|HLL_SPARSE_VAL_BIT; \
} while(0)
#define HLL_SPARSE_ZERO_SET(p,len) do { \
    *(p) = (len)-1; \
} while(0)


hyperloglog.c中提供的api

MurmurHash64A:redis的64bit哈希算法,使用第二代murmur哈希算法,返回一个64bit的哈希值

hllPatLen:使用ele进行64bit哈希,得到一个8字节的hash,取hash的低14bit作为哈希索引,并计算从14bit开始第一个1出现的位置

hllDenseSet:仅使用密集表示时可以调用此函数,判断本次提供的count是否比上次大,若大进行更新

hllDenseAdd:调用hllPatLen和hllDenseSet,先获取count值,再加入密集表示

hllDenseSum:计算密集表示中所有寄存器的值的调和平均数,每个寄存器的值计算成2的负幂,比如某个寄存器是0,计算的时候就是2的0次幂,为1,若是2,就是2^-2,即0.25

hllSparseToDense:将稀疏表示转换成密集表示

hllSparseSet:根据提供的count决定是否更新稀疏表示中某个index位置的寄存器值,此函数可能将稀疏表示变成密集表示,转换的条件是,value超过32,或者有值的寄存器个数达到阈值

hllSparseAdd:向稀疏表示中更新一个新的元素

hllSparseSum:计算稀疏表示下,所有寄存器的调和平均数

hllRawSum:按照8bit寄存器计算所有寄存器的值调和平均数

hllCount:计算一个HLL的估计基数值,使用超对数,调用math.h中的log,pow,llroundl

hllAdd:加入一个元素到HLL中

hllmerge:更新每个寄存器的最大值


猜你喜欢

转载自blog.csdn.net/kdb_viewer/article/details/80751661