redis源码分析与思考(六)——整数集合

    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. 整数集合利用升级操作尽可能的节约了内存。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值