首页 > 代码库 > Redis源码分析-内存数据结构intset

Redis源码分析-内存数据结构intset

这次研究了一下intset,研究的过程中,一度看不下过去,但是还是咬牙挺过来了,看懂了也就是那么回事,静下心来,切莫浮躁

Redis为了追求高效,在存储下做了很多的优化,像intset就是作者为了节约内存定制的数据结构,包括后面将要阅读的压缩列表。

intset是一个有序的整数集,提供了增加,删除,查找的接口,针对uint16_t uint32_t uint64_t,提供了不同编码的转换(严格的说只是类型的提升)

首先,看一下它的结构定义:

typedef struct intset {                                                                                                                                                
    uint32_t encoding;                                                                                                                                                 
    uint32_t length;                                                                                                                                                                                 
    int8_t contents[];                                                                                                                          
} intset;
encoding:有如下几种编码

#define INTSET_ENC_INT16 (sizeof(int16_t))
#define INTSET_ENC_INT32 (sizeof(int32_t))
#define INTSET_ENC_INT64 (sizeof(int64_t))
实际上这里使用一个uint8_t存储就够了

length:当前整数集有多少个整数

contents[]:具体存储的位置,这里以一个字节为存储单元,方便对高类型进行寻址

看一下它对外提供的接口:

intset *intsetNew(void); 
intset *intsetAdd(intset *is, int64_t value, uint8_t *success);                                         
intset *intsetRemove(intset *is, int64_t value, int *success);                                          
uint8_t intsetFind(intset *is, int64_t value);                                                          
int64_t intsetRandom(intset *is);
uint8_t intsetGet(intset *is, uint32_t pos, int64_t *value);                                            
uint32_t intsetLen(intset *is);
size_t intsetBlobLen(intset *is); 
一种数据结构,必然要提供类似插入,查询,删除这样的接口,另外不要暴露内部使用的接口,这里提供的接口,我们具体分析几个

初始化接口:

/* Create an empty intset. */
intset *intsetNew(void) {
    intset *is = malloc(sizeof(intset));
    is->encoding = intrev32ifbe(INTSET_ENC_INT16);
    is->length = 0;
    return is; 
}
没什么难的,注意默认使用最低的2字节存储

/* 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)) {
            if (success) *success = 0;
            return is;
        }

        is = intsetResize(is,intrev32ifbe(is->length)+1);
        if (pos < intrev32ifbe(is->length)) intsetMoveTail(is,pos,pos+1);
    }

    _intsetSet(is,pos,value);
    is->length = intrev32ifbe(intrev32ifbe(is->length)+1);
    return is;
}

这个接口比较有难度,具体分析:

1、首先判断要增加的值的编码是否大于当前编码,大于则进行类型提升,并加入value

2、如果小于当前编码,首先查询数据是否存在,存在则返回,不存在则设置插入位置pos

3、重新分配内存大小

4、移动数据,所有数据往后移动,复杂度有点高啊

5、插入数据,设置数据个数

其中,类型提升并插入value的接口如下:

/* 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;

    /* 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. */
    if (prepend)
        _intsetSet(is,0,value);
    else
        _intsetSet(is,intrev32ifbe(is->length),value);
    is->length = intrev32ifbe(intrev32ifbe(is->length)+1);
    return is;
}
可以看到,类型提升的过程如下:

1、因为整数集是有序的,所以首先判断要加入的数是正数还是负数,正数就在尾部添加,负数则在头部添加

2、增加内存大小

3、移动数据,这里和第一步挂钩,而且移动的过程比较难以理解,首先根据原来编码取出数据,然后根据新的编码插入数据

4、插入数据,在头部还是尾部插入

5、修改数据个数


另外移动数据的接口如下:

static void intsetMoveTail(intset *is, uint32_t from, uint32_t to) {
    void *src, *dst;
    uint32_t bytes = intrev32ifbe(is->length)-from;
    uint32_t encoding = intrev32ifbe(is->encoding);

    if (encoding == INTSET_ENC_INT64) {
        src = http://www.mamicode.com/(int64_t*)is->contents+from;>因为是连续的内存,找到移动的起始位置,然后memmove(),bingo!!!


查找数据的接口实现:

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;
        cur = _intsetGet(is,mid);
        if (value > cur) {
            min = mid+1;
        } else if (value < cur) {
            max = mid-1;
        } else {
            break;
        }
    }

    if (value =http://www.mamicode.com/= cur) {>

还是个二分查找,niubility!!!个人感觉这种数据结构的高效就体现在这里,因为是有序,所以查找快速,因为是数组,所以插入,删除,是连续内存拷贝,也很快

有时间突然想去看一下STL Vector的实现了,它的insert是如何实现的?




Redis源码分析-内存数据结构intset