天天看點

Redis源碼學習簡記(六)intset原理與個人了解

intset是一個對記憶體空間锱铢必較的有序set。使用一段連續的記憶體空間存儲一段資料。看起來有點像數組的形式。

資料結構

typedef struct intset {
    uint32_t encoding;//編碼的方式 分為int16 int32 int64
    uint32_t length;//資料的個數
    int8_t contents[];//存儲資料的點。int8_t與char 所用的位元組數相同
} intset;
           

|   encoding   |   length  |   contents[]  |

encoding 4位元組的長度。存儲2,4,8其中一個。

length  整數存儲的個數。

contents 資料存儲的起點。

資料結構看起來還是挺簡單的,那麼我們看看其實作過程吧。

具體的API

建立一個新的set,預設使用2位16位元組的int -32678~32676

這裡面的interv32ifbe 是一個轉換資料大小端,根據計算機的結構把所有的資料都變成以小端存儲,即高位存高位資料

而大端則是高位存低位資料剛好與小端相反

intset *intsetNew(void) {
    //建立intset
    intset *is = zmalloc(sizeof(intset));
    is->encoding = intrev32ifbe(INTSET_ENC_INT16);
    //預設編碼為16.
    is->length = 0;
    return is;
}
           

根據傳進去的len重新設定長度

/* Resize the intset */
static intset *intsetResize(intset *is, uint32_t len) {
    //重新設定大小
    uint32_t size = len*intrev32ifbe(is->encoding);
    is = zrealloc(is,sizeof(intset)+size);
    return is;
}
           

接下來是查找某個整數的操作,由于資料是有序的,利用二分法進行查找。

static uint8_t intsetSearch(intset *is, int64_t value, uint32_t *pos) {
    int min = 0, max = intrev32ifbe(is->length)-1, mid = -1;
    int64_t cur = -1;

    /* The value can never be found when the set is empty */
    if (intrev32ifbe(is->length) == 0) {
        if (pos) *pos = 0;
        return 0;
    } else {
        /* Check for the case where we know we cannot find the value,
         * but do know the insert position. */
        //判斷是否在最前或者最後
        if (value > _intsetGet(is,intrev32ifbe(is->length)-1)) {
            if (pos) *pos = intrev32ifbe(is->length);
            return 0;
        } else if (value < _intsetGet(is,0)) {
            if (pos) *pos = 0;
            return 0;
        }
    }

    while(max >= min) {
        mid = ((unsigned int)min + (unsigned int)max) >> 1;
        //相當于除以2 而且不會溢出。
        cur = _intsetGet(is,mid);
        //擷取到中間的元素,二分法
        if (value > cur) {
            min = mid+1;
        } else if (value < cur) {
            max = mid-1;
        } else {
            break;
        }
    }

    if (value == cur) {
        if (pos) *pos = mid;
        return 1;
    } else {
        if (pos) *pos = min;
        return 0;
    }
    //找到傳回1。pos指向找到的資料,找不到傳回0,pos指向前一個資料。插入的時候會用到該特性
}
           

更新資料(将存儲的位數增加),而更新由value所決定,并且将value添加到set中。

每次更新資料都要重新更新所有資料存儲的位數,然後在頭部,或者尾部對資料進行插入。

/* Upgrades the intset to a larger encoding and inserts the given integer. */
static intset *intsetUpgradeAndAdd(intset *is, int64_t value) {
    uint8_t curenc = intrev32ifbe(is->encoding);
    uint8_t newenc = _intsetValueEncoding(value);
    int length = intrev32ifbe(is->length);
    int prepend = value < 0 ? 1 : 0;
    //用于存儲由value造成的溢出是下限還是上限,然後決定插入的是頭還是尾

    /* First set new encoding and resize */
    is->encoding = intrev32ifbe(newenc);
    //重新設定編碼類型
    is = intsetResize(is,intrev32ifbe(is->length)+1);
    //重新配置設定容量

    /* Upgrade back-to-front so we don't overwrite values.
     * Note that the "prepend" variable is used to make sure we have an empty
     * space at either the beginning or the end of the intset. */
    while(length--)
        _intsetSet(is,length+prepend,_intsetGetEncoded(is,length,curenc));
    //拿到原來的最後一個元素,然後插入到新的位置中

    /* Set the value at the beginning or the end. */
    //需要更新的情況是value會造成溢出,那麼value智能放在頭結點或者尾節點。
    //然後根據正負進行在前面插入還是後面插入
    if (prepend)
        _intsetSet(is,0,value);
    else
        _intsetSet(is,intrev32ifbe(is->length),value);
    is->length = intrev32ifbe(intrev32ifbe(is->length)+1);
    //設定長度
    return is;
}
           

根據from與to移動資料。具體是把from節點到尾部的所有資料移動到to節點的後面。

static void intsetMoveTail(intset *is, uint32_t from, uint32_t to) {
	/*
		把from後面的所有元素移到to元素後面
		若form大于to 則删除了to —— from 之間的元素
		若from等于to 沒變化
		若from小于to 那麼則空出了to—from 那麼多個元素

		a[0]a[1]a[2]a[3]a[4]a[5]
		假設from為3 to為2
		意思就是把3後面所有的元素移到第2個元素後面那麼結果為
		a[0]a[1]a[3]a[4]a[5]


	*/
    void *src, *dst;
    uint32_t bytes = intrev32ifbe(is->length)-from;
    uint32_t encoding = intrev32ifbe(is->encoding);
    //下面根據編碼擷取到copy點
    //與拷貝的數量即from後面元素的所有資料。
    if (encoding == INTSET_ENC_INT64) {
        src = (int64_t*)is->contents+from;
        dst = (int64_t*)is->contents+to;
        bytes *= sizeof(int64_t);
    } else if (encoding == INTSET_ENC_INT32) {
        src = (int32_t*)is->contents+from;
        dst = (int32_t*)is->contents+to;
        bytes *= sizeof(int32_t);
    } else {
        src = (int16_t*)is->contents+from;
        dst = (int16_t*)is->contents+to;
        bytes *= sizeof(int16_t);
    }
    memmove(dst,src,bytes);
}
           

添加元素,該操作會先判斷插入的元素是否查過set的編碼,然後跟插入排序一樣,插入點後面的資料整體後移。

具體實作觀察代碼即可。

/* Insert an integer in the intset */
intset *intsetAdd(intset *is, int64_t value, uint8_t *success) {
	//添加元素

    uint8_t valenc = _intsetValueEncoding(value);
    //擷取編碼
    uint32_t pos;
    if (success) *success = 1;

    /* Upgrade encoding if necessary. If we need to upgrade, we know that
     * this value should be either appended (if > 0) or prepended (if < 0),
     * because it lies outside the range of existing values. */
    if (valenc > intrev32ifbe(is->encoding)) {

    	//看看是否要更新元素的值

        /* This always succeeds, so we don't need to curry *success. */
        return intsetUpgradeAndAdd(is,value);
        //插入點不是頭就是尾 是以連起來做
    } else {
    	//若不需要則查找插入點
        /* Abort if the value is already present in the set.
         * This call will populate "pos" with the right position to insert
         * the value when it cannot be found. */
        if (intsetSearch(is,value,&pos)) {
        	//當元素存在則不插入 說明該set是不重複的
        	//傳回失敗
            if (success) *success = 0;
            return is;
        }
        //查找失敗後 pos為插入點

        is = intsetResize(is,intrev32ifbe(is->length)+1);
        //重新配置設定空間
        if (pos < intrev32ifbe(is->length)) intsetMoveTail(is,pos,pos+1);
        //移位空出pos的位置
    }

    _intsetSet(is,pos,value);
    //設定值
    is->length = intrev32ifbe(intrev32ifbe(is->length)+1);
    //重置length
    return is;
}
           

删除資料同樣會重新配置設定資料長度,然後删除點整體前移,然後再進行容量的resize。使用時間換空間的做法。這樣節省幾位帶來的是大量的時間複雜度,這到底好與壞要看具體情況了。

/* Delete integer from intset */
intset *intsetRemove(intset *is, int64_t value, int *success) {
	//删除某個元素
    uint8_t valenc = _intsetValueEncoding(value);
    //擷取編碼
    uint32_t pos;
    if (success) *success = 0;

    if (valenc <= intrev32ifbe(is->encoding) && intsetSearch(is,value,&pos)) {
    	//保證元素編碼值小于編碼最大值,然後查找成功。
    	//接下來就是做删除操作
        uint32_t len = intrev32ifbe(is->length);

        /* We know we can delete */
        if (success) *success = 1;

        /* Overwrite value with tail and update length */
        if (pos < (len-1)) intsetMoveTail(is,pos+1,pos);
        //通過movetial 複寫了要删除的元素,并且長度會減少
        is = intsetResize(is,len-1);
        //重新配置設定空間。真是锱铢必較,但是效率很低
        //一次删除要移動n+(length-pos)的元素
        is->length = intrev32ifbe(len-1);
        //設定長度
    }
    return is;
}
           

intset的具體操作大概就到這裡了。