• 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 = (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是怎样实现的?




  • 相关阅读:
    返回页面顶部的方法
    一个获取当前 url 查询字符串中的参数的方法
    那些让你看起来很牛逼的Docker使用技巧
    Docker 1.13 新特性 —— Docker服务编排相关
    docker1.13新功能network关注点
    Docker 1.13 最实用命令行:终于可以愉快地打扫房间了
    Docker 1.13 – 新增功能大揭秘
    Docker 1.13 编排能力进化
    Docker
    Docker
  • 原文地址:https://www.cnblogs.com/mfrbuaa/p/5265268.html
Copyright © 2020-2023  润新知