1. 程式人生 > >redis基礎資料結構(六) 基數統計

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是密集表示或者稀疏表示的實際資料部分

密集表示:

6bit按照小端排列,位元組內部從最低有效位開始

稀疏表示:

使用3種微碼:

ZERO:00xxxxxx,6bit用來表示連續設定為0的暫存器的個數,加1,可以表示1個到64個連續暫存器被設定為0

XZERO:01xxxxxx 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,01表示接下來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

稀疏表示最大能表示的值是5bit,即32,而hyperloglog是用的是6bit暫存器,表示的值可以達到63,這樣設計的原因是,只有entry很多的時候,才有可能達到32以上的值,這個時候剛好使用密集表示。這其實是一個統計學思想,在entry很稀疏的時候很難找到大於32的entry

相關巨集定義如下

/* 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到 63)

hyperloglog.c中定義的關於稀疏表示的巨集:

#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:更新每個暫存器的最大值

hllPatLen:將一個字串計算成64bit hash值,並計算從14bit開始第一個1出現的位置

hllDenseSet:取一個暫存器的值,判斷是否需要更新

hllDenseAdd:向密集表示中加入一個字串

hllDenseSum:計算密集表示下所有暫存器的調和平均數

hllSparseToDense:將稀疏表示轉換成密集表示,用物件接收

hllSparseSet:稀疏表示設定一個值,此函式可能將稀疏表示轉換成密集表示,由於是稀疏表示,需要注意很多分裂和合並微碼的情況

hllSparseAdd:向稀疏表示中加入一個值

hllSparseSum:計算繼續表示所有暫存器的調和平均數

hllRawSum:計算原生表示中所有暫存器的調和平均數

hllCount:根據所有暫存器的調和平均數計算近似基數

hllAdd:向一個hll中更新一個元素

hllMerge:將hll中所有暫存器的值取大的更新到max陣列中

createHLLObject:建立一個hll物件,初始一定是稀疏表示

isHLLObjectOrReply:檢查一個hll物件是否合法