您的位置:首页 > 理论基础 > 数据结构算法

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

2014-10-21 20:55 513 查看
这次研究了一下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 = (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);
}
因为是连续的内存,找到移动的起始位置,然后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 == cur) {
if (pos) *pos = mid;
return 1;
} else {
if (pos) *pos = min;
return 0;
}
}

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

有时间突然想去看一下STL Vector的实现了,它的insert是如何实现的?
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息