intset,即整数集合。整数集合是Redis集合键的底层实现之一,当集合中只函数整数元素,且整数元素个数不多时,Redis会采用整数集合来实现集合键。
介绍整数集合前,先来说下整数集合用到的整数的类型,因为整数集合用到的类型并不是固定的,而是根据集合里面的类型来改变的,这样可以做到节约内存。
typedef signed char __int8_t;---->int8_t
typedef unsigned char __uint8_t;---->uint8_t
typedef signed short int __int16_t;---->int16_t
typedef unsigned short int __uint16_t;---->uint8_t
typedef signed int __int32_t;---->int32_t
typedef unsigned int __uint32_t;---->uint32_t
typedef signed long int __int64_t;---->int64_t
typedef unsigned long int __uint64_t;---->uint64_t
如上述代码所示_t结尾的类型,是我们熟悉的类型封装了罢了。然后由于intset是在内存上直接操作赋值,并且所存储的值都超过了一个字节,所以需要考虑大小端的问题:
大端模式,是指数据的高字节保存在内存的低地址中,而数据的低字节保存在内存的高地址中,这样的存储模式有点儿类似于把数据当作字符串顺序处理:地址由小向大增加,而数据从高位往低位放;这和我们的阅读习惯一致。
小端模式,是指数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中,这种存储模式将地址的高低和数据位权有效地结合起来,高地址部分权值高,低地址部分权值低。 ------百度百科
而为什么有大小端之分,百度百科这样解释:
这是因为在计算机系统中,我们是以字节为单位的,每个地址单元都对应着一个字节,一个字节为 8bit。但是在C语言中除了8bit的char之外,还有16bit的short型,32bit的long型(要看具体的编译器),另外,对于位数大于 8位的处理器,例如16位或者32位的处理器,由于寄存器宽度大于一个字节,那么必然存在着一个如何将多个字节安排的问题。因此就导致了大端存储模式和小端存储模式。例如一个16bit的short型x,在内存中的地址为0x0010,x的值为0x1122,那么0x11为高字节,0x22为低字节。对于 大端模式,就将0x11放在低地址中,即0x0010中,0x22放在高地址中,即0x0011中。小端模式,刚好相反。我们常用的X86结构是小端模式,而KEIL C51则为大端模式。很多的ARM,DSP都为小端模式。有些ARM处理器还可以随时在程序中(在ARM Cortex 系列使用REV、REV16、REVSH指令 [1] )进行大小端的切换。
所以在Redis整数集合中使用大小端转换,简而言之,就是为了统一字节的排列规则,防止出现因字节排列规则不一而导致的bug。而Redis全部采用的是小端模式。 在Redis 的endianconv.h文件中有如下的代码用于大小端转换:
#if (BYTE_ORDER == LITTLE_ENDIAN)
#define memrev16ifbe(p)
#define memrev32ifbe(p)
#define memrev64ifbe(p)
#define intrev16ifbe(v) (v)
#define intrev32ifbe(v) (v)
#define intrev64ifbe(v) (v)
如果不懂大小端的转换,可以选择无视,因为值的大小经过大小端的转换是不会变的。
定义
typedef struct intset {
// 编码方式
uint32_t encoding;
// 集合包含的元素数量
uint32_t length;
// 保存元素的数组
int8_t contents[];
} intset;
在contents存贮着数据的编码格式有如下:
#define INTSET_ENC_INT16 (sizeof(int16_t))
#define INTSET_ENC_INT32 (sizeof(int32_t))
#define INTSET_ENC_INT64 (sizeof(int64_t))
编码的转换
利用三种数据类型最大值与最小值的比较,用于判断使用哪种类型:
/* Minimum of signed integral types. */
# define INT8_MIN (-128)
# define INT16_MIN (-32767-1)
# define INT32_MIN (-2147483647-1)
# define INT64_MIN (-__INT64_C(9223372036854775807)-1)
/* Maximum of signed integral types. */
# define INT8_MAX (127)
# define INT16_MAX (32767)
# define INT32_MAX (2147483647)
# define INT64_MAX (__INT64_C(9223372036854775807))
//判断传入的值符合哪种编码格式
static uint8_t _intsetValueEncoding(int64_t v) {
if (v < INT32_MIN || v > INT32_MAX)
return INTSET_ENC_INT64;
else if (v < INT16_MIN || v > INT16_MAX)
return INTSET_ENC_INT32;
else
return INTSET_ENC_INT16;
}
//对集合中的一个元素进行编码转换,并返回该元素
static int64_t _intsetGetEncoded(intset *is, int pos, uint8_t enc) {
int64_t v64;
int32_t v32;
int16_t v16;
if (enc == INTSET_ENC_INT64) {
memcpy(&v64,((int64_t*)is->contents)+pos,sizeof(v64));
memrev64ifbe(&v64);
return v64;
} else if (enc == INTSET_ENC_INT32) {
memcpy(&v32,((int32_t*)is->contents)+pos,sizeof(v32));
memrev32ifbe(&v32);
return v32;
} else {
memcpy(&v16,((int16_t*)is->contents)+pos,sizeof(v16));
memrev16ifbe(&v16);
return v16;
}
}
在判断其属于哪种编码时,新建一个变量,采用memcpy函数,将原有的集合里的值经过强制转换后赋值给新值并返回该新值。
值的获取与设置
//获取直接调用_intsetGetEncoded函数
static int64_t _intsetGet(intset *is, int pos) {
return _intsetGetEncoded(is,pos,intrev32ifbe(is->encoding));
}
//设置指定pos位置的值,如果值的编码格式改变,就进行强制转换
static void _intsetSet(intset *is, int pos, int64_t value) {
// 取出集合的编码方式
uint32_t encoding = intrev32ifbe(is->encoding);
if (encoding == INTSET_ENC_INT64) {
((int64_t*)is->contents)[pos] = value;
memrev64ifbe(((int64_t*)is->contents)+pos);
} else if (encoding == INTSET_ENC_INT32) {
((int32_t*)is->contents)[pos] = value;
memrev32ifbe(((int32_t*)is->contents)+pos);
} else {
((int16_t*)is->contents)[pos] = value;
memrev16ifbe(((int16_t*)is->contents)+pos);
}
}
创建和重新分配内存大小
intset *intsetNew(void) {
// 为整数集合结构分配空间
intset *is = zmalloc(sizeof(intset));
// 设置初始编码,初始编码为INTSET_ENC_INT16
is->encoding = intrev32ifbe(INTSET_ENC_INT16);
// 初始化元素数量
is->length = 0;
return is;
}
//重新调整集合的大小,其实是扩充其大小
static intset *intsetResize(intset *is, uint32_t len) {
// 计算数组的空间大小
uint32_t size = len*intrev32ifbe(is->encoding);
// 数组原有的数据会被保留
is = zrealloc(is,sizeof(intset)+size);
return is;
}
升级
因为c语言数据类型是静态的,一种结构中不可能存贮多个类型的数据,且为了节省内存,数据集合就采用了升级的策略。即当遇到数据类型字节数大于原有集合中的时,数据集合就执行升级操作,将所有的数据类型转化为新插入的数据类型,使其新数据可以存储下来。而数据集合是 不存在降级的,一旦升级完,就会保持现状,直到下一次的升级。
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;
// 更新集合的编码方式
is->encoding = intrev32ifbe(newenc);
// 根据新编码对集合(的底层数组)进行空间调整
is = intsetResize(is,intrev32ifbe(is->length)+1);
//这里其实是类似于数组线性表的插入时其它值移位的操作
while(length--)
_intsetSet(is,length+prepend,_intsetGetEncoded(is,length,curenc));
// 设置新值,根据 prepend 的值来决定是添加到数组头还是数组尾
if (prepend)
_intsetSet(is,0,value);
else
_intsetSet(is,intrev32ifbe(is->length),value);
// 更新整数集合的元素数量
is->length = intrev32ifbe(intrev32ifbe(is->length)+1);
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;
// 处理 is 为空时的情况
if (intrev32ifbe(is->length) == 0) {
if (pos) *pos = 0;
return 0;
} else {
// 因为底层数组是有序的,如果 value 比数组中最后一个值都要大
// 那么 value 肯定不存在于集合中,
// 并且应该将 value 添加到底层数组的最末端
if (value > _intsetGet(is,intrev32ifbe(is->length)-1)) {
if (pos) *pos = intrev32ifbe(is->length);
return 0;
// 因为底层数组是有序的,如果 value 比数组中最前一个值都要小
// 那么 value 肯定不存在于集合中,
// 并且应该将它添加到底层数组的最前端
} else if (value < _intsetGet(is,0)) {
if (pos) *pos = 0;
return 0;
}
}
// 在有序数组中进行二分查找
while(max >= min) {
mid = (min+max)/2;
cur = _intsetGet(is,mid);
if (value > cur) {
min = mid+1;
} else if (value < cur) {
max = mid-1;
} else {
break;
}
}
// 检查是否已经找到了 value
if (value == cur) {
if (pos) *pos = mid;
return 1;
} else {
if (pos) *pos = min;
return 0;
}
}
值的移位、新增与删除
值的的移位、新增与删除其实原理就是普通数组线性表的增删操作而已。
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);
}
intset *intsetAdd(intset *is, int64_t value, uint8_t *success) {
// 计算编码 value 所需的长度
uint8_t valenc = _intsetValueEncoding(value);
uint32_t pos;
// 默认设置插入为成功
if (success) *success = 1;
// 如果 value 的编码比整数集合现在的编码要大
// 那么表示 value 必然可以添加到整数集合中
// 并且整数集合需要对自身进行升级,才能满足 value 所需的编码
if (valenc > intrev32ifbe(is->encoding)) {
//执行升级操作
return intsetUpgradeAndAdd(is,value);
} else {
// 运行到这里,表示整数集合现有的编码方式适用于 value
// 在整数集合中查找 value ,看他是否存在:
// - 如果存在,那么将 *success 设置为 0 ,并返回未经改动的整数集合
// - 如果不存在,那么可以插入 value 的位置将被保存到 pos 指针中
// 等待后续程序使用
if (intsetSearch(is,value,&pos)) {
if (success) *success = 0;
return is;
}
// 运行到这里,表示 value 不存在于集合中
// 程序需要将 value 添加到整数集合中
// 为 value 在集合中分配空间
is = intsetResize(is,intrev32ifbe(is->length)+1);
// 如果新元素不是被添加到底层数组的末尾
// 那么需要对现有元素的数据进行移动,空出 pos 上的位置,用于设置新值
if (pos < intrev32ifbe(is->length)) intsetMoveTail(is,pos,pos+1);
}
// 将新值设置到底层数组的指定位置中
_intsetSet(is,pos,value);
// 增一集合元素数量的计数器
is->length = intrev32ifbe(intrev32ifbe(is->length)+1);
// 返回添加新元素后的整数集合
return is;
}
intset *intsetRemove(intset *is, int64_t value, int *success) {
// 计算 value 的编码方式
uint8_t valenc = _intsetValueEncoding(value);
uint32_t pos;
// 默认设置标识值为删除失败
if (success) *success = 0;
// 当 value 的编码大小小于或等于集合的当前编码方式(说明 value 有可能存在于集合)
// 并且 intsetSearch 的结果为真,那么执行删除
if (valenc <= intrev32ifbe(is->encoding) && intsetSearch(is,value,&pos)) {
// 取出集合当前的元素数量
uint32_t len = intrev32ifbe(is->length);
// 设置标识值为删除成功
if (success) *success = 1;
// 如果 value 不是位于数组的末尾
// 那么需要对原本位于 value 之后的元素进行移动
if (pos < (len-1)) intsetMoveTail(is,pos+1,pos);
// 缩小数组的大小,移除被删除元素占用的空间
is = intsetResize(is,len-1);
// 更新集合的元素数量
is->length = intrev32ifbe(len-1);
}
return is;
}
总结
1. 整数集合是有序、且无重复的数据结构,是集合键的底层实现之一。程序会根据所存数据改变其自身的编码格式,即升级操作。
2. 整数集合只支持升级操作,不支持降级。
3. 整数集合利用升级操作尽可能的节约了内存。