• redis源码学习之sds


    参考《Redis 设计与实现》 (基于redis3.0.0) 作者:黄健宏
    学习redis3.2.13

    toc

    介绍

    简单动态字符串SDS(simple dynamic strings) 是redis中的字符串类型

    SDS结构

    redis3.0.0中的结构

    3.0.0中的SDS除了柔性数组及表示其长度的len之外,还多了表示buf剩余可用空间的成员free。此成员为SDS的空间管理提供了灵活性。

    /*
     * 类型别名,用于指向 sdshdr 的 buf 属性
     */
    typedef char *sds;    //供外部使用的类型
    /*
     * 保存字符串对象的结构
     */
    struct sdshdr {
        // buf 中已占用空间的长度,即字符串长度,不包含结尾的空字符
        unsigned int len;
        // buf 中剩余可用空间的长度
        unsigned int free;
        // 数据空间
        char buf[];
    };

    书中SDS结构演示例子:

    redis3.2.13中的结构

    3.2.13中的SDS可以根据不同的初始字符串长度,选择不同的sds头部
    头部结构定义如下:

    typedef char *sds;
    //sdshdr5 中flags低3位用来存头部类,高5位用来存字符串长度
    /* Note: sdshdr5 is never used, we just access the flags byte directly.
     * However is here to document the layout of type 5 SDS strings. */
    struct __attribute__ ((__packed__)) sdshdr5 {
        unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
        char buf[];
    };
    struct __attribute__ ((__packed__)) sdshdr8 {
        uint8_t len; /* used */
        uint8_t alloc; /* excluding the header and null terminator */
        unsigned char flags; /* 3 lsb of type, 5 unused bits */
        char buf[];
    };
    struct __attribute__ ((__packed__)) sdshdr16 {
        uint16_t len; /* used */
        uint16_t alloc; /* excluding the header and null terminator */
        unsigned char flags; /* 3 lsb of type, 5 unused bits */
        char buf[];
    };
    struct __attribute__ ((__packed__)) sdshdr32 {
        uint32_t len; /* used */
        uint32_t alloc; /* excluding the header and null terminator */
        unsigned char flags; /* 3 lsb of type, 5 unused bits */
        char buf[];
    };
    struct __attribute__ ((__packed__)) sdshdr64 {
        uint64_t len; /* used */
        uint64_t alloc; /* excluding the header and null terminator */
        unsigned char flags; /* 3 lsb of type, 5 unused bits */
        char buf[];
    };
    • len表示buf 中已占用空间的长度,即字符串的长度
    • alloc表示已申请的空间的长度除去头部与结尾空字符占用的部分,即字符串长度与剩余可用长度之和
    • flags用于区分头部的类型,仅使用了低三位
      对于sdshdr5 ,注释说不会被用到,我好奇的搜了一下后,发现情况不是那么简单。

    结构变化带来的优势与劣势

    优势:

    • 对于短小字符串来说:取消内存对齐,并将头部从固定的8字节缩短到最低3字节,节省了内存
    • 对于巨大字符串来说:字符串的长度描述从无符号的32位提高到了无符号64位
      • 可存储的数据更大
      • 更能避免记录长度的整形溢出,更安全

    劣势:

    • 当存储的字符串由巨大变为短小时,头部并不会缩短,一定程度上浪费了内存

    SDS也遵守C语音字符串中以空字符串结尾的规则,所以可以使用C语言字符串函数库中的部分函数。

    对比std::string结构

    g++版本

    ubuntu@ubuntu:/usr/include/c++/9$ g++ --version 
    g++ (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0
    Copyright (C) 2019 Free Software Foundation, Inc.
    This is free software; see the source for copying conditions.  There is NO
    warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

    STL源码位置:

    ubuntu@ubuntu:/usr/include/c++/9$ pwd
    /usr/include/c++/9

    string定义如下:

    //文件string中
    #include <bits/basic_string.h>
    ...
    using string    = basic_string<char>;
    ...
    //文件basic_string.h中
    template<typename _CharT, typename _Traits, typename _Alloc>
        class basic_string
        {
    ...
        private:
    ...
          struct _Alloc_hider : allocator_type // TODO check __is_final
          {
    #if __cplusplus < 201103L
        _Alloc_hider(pointer __dat, const _Alloc& __a = _Alloc())
        : allocator_type(__a), _M_p(__dat) { }
    #else
        _Alloc_hider(pointer __dat, const _Alloc& __a)
        : allocator_type(__a), _M_p(__dat) { }
    
        _Alloc_hider(pointer __dat, _Alloc&& __a = _Alloc())
        : allocator_type(std::move(__a)), _M_p(__dat) { }
    #endif
    
        pointer _M_p; // The actual data.
          };
    
          _Alloc_hider    _M_dataplus;
          size_type        _M_string_length;
    ...
        }

    可以看出,std::string实际上是std::basic_string针对char类型的实例化;
    再来看类模板std::basic_string,它有两个成员:

    • 一个是内部含有指向字符串指针的_M_dataplus
    • 另一个是表明字符串长度的_M_string_length

    可以看出,在内存的存储结构上SDS与std::string有明显的不同:

    • SDS的字符串内容直接由其头部的柔性数组来存储
    • std::string中的字符串与描述字符串信息的结构是分离的,由一个指针连接

    相较而言,SDS的内存管理方式更加简单,不用分别管理头部和内容的数据,降低了出错的概率,同时提高了性能。。。。。。。。。额。。。。。。。。还节省了一个指针的空间,并兼顾了性能,直接从头部偏移即可,而不用再对指针中记录的地址寻址

    SDS对比C字符串的优势

    SDS相对于C字符串有性能、安全性、功能方面的优势

    性能优势

    获取字符串长度的时间复杂度为O(1)
    SDS中的len记录了字符串的长度,获取长度时只需返回len即可,无需遍历

    //3.0.0中 直接偏移回头部取长度成员
    static inline size_t sdslen(const sds s) {
        struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr)));
        return sh->len;
    }
    
    //3.2.13中 则需根据flags来偏移再取出长度
    ...
    #define SDS_TYPE_MASK 7
    #define SDS_TYPE_BITS 3
    ...
    #define SDS_HDR(T,s) ((struct sdshdr##T *)((s)-(sizeof(struct sdshdr##T))))
    #define SDS_TYPE_5_LEN(f) ((f)>>SDS_TYPE_BITS)
    
    static inline size_t sdslen(const sds s) {
        unsigned char flags = s[-1];
        switch(flags&SDS_TYPE_MASK) {
            case SDS_TYPE_5:
                return SDS_TYPE_5_LEN(flags);
            case SDS_TYPE_8:
                return SDS_HDR(8,s)->len;
            case SDS_TYPE_16:
                return SDS_HDR(16,s)->len;
            case SDS_TYPE_32:
                return SDS_HDR(32,s)->len;
            case SDS_TYPE_64:
                return SDS_HDR(64,s)->len;
        }
        return 0;
    }

    获取剩余可用空间时间复杂度也是O(1)

    //3.0.0中 也是直接偏移回头部取剩余可用空间
    static inline size_t sdsavail(const sds s) {
        struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr)));
        return sh->free;
    }
    //3.2.13中 同样需根据flags来偏移,但需经过简单计算来得到剩余可用空间
    ...
    #define SDS_HDR_VAR(T,s) struct sdshdr##T *sh = (void*)((s)-(sizeof(struct sdshdr##T)));
    ...
    static inline size_t sdsavail(const sds s) {
        unsigned char flags = s[-1];
        switch(flags&SDS_TYPE_MASK) {
            case SDS_TYPE_5: {
                return 0;
            }
            case SDS_TYPE_8: {
                SDS_HDR_VAR(8,s);
                return sh->alloc - sh->len;
            }
            case SDS_TYPE_16: {
                SDS_HDR_VAR(16,s);
                return sh->alloc - sh->len;
            }
            case SDS_TYPE_32: {
                SDS_HDR_VAR(32,s);
                return sh->alloc - sh->len;
            }
            case SDS_TYPE_64: {
                SDS_HDR_VAR(64,s);
                return sh->alloc - sh->len;
            }
        }
        return 0;
    }

    减少修改字符串时重分配的次数

    1. 空间预分配
      当对SDS修改并且造成其需要扩容时,会为SDS分配额外空间并记录其长度,以供后续使用:
      • 修改后的长度将小于1MB时,额外分配空间大小与len相同
      • 修改后的长度将大于等于1MB时,额外分配1MB空间
    2. 惰性空间释放
      当对SDS进行缩减时,不会释放被缩减字符占用的内存,仅会计入剩余可用空间,以备后续使用

    可以看出,SDS通过额外的空间换取了一定的性能

    安全优势

    杜绝缓冲区溢出
    SDS内部记录了剩余可用空间的长度,对SDS进行追加时,会对剩余可用空间进行判断,剩余可用空间不满足需求时会对SDS扩容

    功能优势

    二进制安全(可存储任意二进制数据)

    • SDS以二进制方式处理存在其中的数据,不对所存数据进行任何限制
    • SDS的长度由其len保存,而不是空字符判断

    兼容部分C字符串函数
    SDS仍然以空字符串结尾,可重用string.h库定义的函数

    主要函数学习

    主要函数速览

    源码为3.2.13

    sdsnewlen

    //根据string_size来选择合适的头部类型
    static inline char sdsReqType(size_t string_size) {
        if (string_size < 1<<5)
            return SDS_TYPE_5;
        if (string_size < 1<<8)
            return SDS_TYPE_8;
        if (string_size < 1<<16)
            return SDS_TYPE_16;
        if (string_size < 1ll<<32)    //long long 1
            return SDS_TYPE_32;
        return SDS_TYPE_64;
    }
    //根据头部类型得出头部大小
    static inline int sdsHdrSize(char type) {
        switch(type&SDS_TYPE_MASK) {
            case SDS_TYPE_5:
                return sizeof(struct sdshdr5);
            case SDS_TYPE_8:
                return sizeof(struct sdshdr8);
            case SDS_TYPE_16:
                return sizeof(struct sdshdr16);
            case SDS_TYPE_32:
                return sizeof(struct sdshdr32);
            case SDS_TYPE_64:
                return sizeof(struct sdshdr64);
        }
        return 0;
    }
    
    //根据initlen指定的长度,使用初始字符串init来构建SDS
    sds sdsnewlen(const void *init, size_t initlen) {
        void *sh;
        sds s;
        char type = sdsReqType(initlen);    //选择头部类型
        /* Empty strings are usually created in order to append. Use type 8
         * since type 5 is not good at this. */
        if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;
        int hdrlen = sdsHdrSize(type);
        unsigned char *fp; /* flags pointer. */
    
        sh = s_malloc(hdrlen+initlen+1);    //#define s_malloc zmalloc
        if (!init)
            memset(sh, 0, hdrlen+initlen+1);
        if (sh == NULL) return NULL;
        s = (char*)sh+hdrlen;
        fp = ((unsigned char*)s)-1;
        switch(type) {
            case SDS_TYPE_5: {
                *fp = type | (initlen << SDS_TYPE_BITS);
                break;
            }
            case SDS_TYPE_8: {
                SDS_HDR_VAR(8,s);
                sh->len = initlen;
                sh->alloc = initlen;
                *fp = type;
                break;
            }
            case SDS_TYPE_16: {
                SDS_HDR_VAR(16,s);
                sh->len = initlen;
                sh->alloc = initlen;
                *fp = type;
                break;
            }
            case SDS_TYPE_32: {
                SDS_HDR_VAR(32,s);
                sh->len = initlen;
                sh->alloc = initlen;
                *fp = type;
                break;
            }
            case SDS_TYPE_64: {
                SDS_HDR_VAR(64,s);
                sh->len = initlen;
                sh->alloc = initlen;
                *fp = type;
                break;
            }
        }
        if (initlen && init)
            memcpy(s, init, initlen);
        s[initlen] = '';
        return s;
    }

    sdsfree

    //释放SDS
    void sdsfree(sds s) {
        if (s == NULL) return;
        //从头部开始释放
        s_free((char*)s-sdsHdrSize(s[-1]));    //#define s_free zfree
    }

    sdstrim

    //从SDS两端去掉包含在字符串cset内的字符
    // s = sdsnew("AA...AA.a.aa.aHelloWorld     :::");
    // s = sdstrim(s,"Aa. :");=======>>>"Hello World"
    sds sdstrim(sds s, const char *cset) {
        char *start, *end, *sp, *ep;
        size_t len;
    
        sp = start = s;
        ep = end = s+sdslen(s)-1;
        while(sp <= end && strchr(cset, *sp)) sp++;
        while(ep > sp && strchr(cset, *ep)) ep--;
        len = (sp > ep) ? 0 : ((ep-sp)+1);
        if (s != sp) memmove(s, sp, len);
        s[len] = '';
        sdssetlen(s,len);
        return s;
    }

    sdscmp

    //对比两个SDS
    int sdscmp(const sds s1, const sds s2) {
        size_t l1, l2, minlen;
        int cmp;
    
        l1 = sdslen(s1);
        l2 = sdslen(s2);
        minlen = (l1 < l2) ? l1 : l2;
        cmp = memcmp(s1,s2,minlen);    //非strcmp,二进制安全
        if (cmp == 0) return l1-l2;
        return cmp;
    }

    sdsfromlonglong

    #define SDS_LLSTR_SIZE 21
    //将longlong转换为字符串buf
    int sdsll2str(char *s, long long value) {
        char *p, aux;
        unsigned long long v;
        size_t l;
        //循环内生成了反向字符串  123 ===> "321"
        /* Generate the string representation, this method produces
         * an reversed string. */
        v = (value < 0) ? -value : value;
        p = s;
        do {
            *p++ = '0'+(v%10);
            v /= 10;
        } while(v);
        if (value < 0) *p++ = '-';
    
        /* Compute length and add null term. */
        l = p-s;
        *p = '';
        //将反向字符串再颠倒一次,复原
        /* Reverse the string. */
        p--;
        //双指针交换,让我联想到快排
        while(s < p) {
            aux = *s;
            *s = *p;
            *p = aux;
            s++;
            p--;
        }
        return l;
    }
    //通过longlong生成SDS
    sds sdsfromlonglong(long long value) {
        char buf[SDS_LLSTR_SIZE];
        int len = sdsll2str(buf,value);
    
        return sdsnewlen(buf,len);
    }

    sdsMakeRoomFor

    经sdsMakeRoomFor函数后的SDS,头部只能变大,没法变小

    #define SDS_MAX_PREALLOC (1024*1024)
    ...
    //为SDS扩容
    sds sdsMakeRoomFor(sds s, size_t addlen/*待扩容长度*/) {
        void *sh, *newsh;
        size_t avail = sdsavail(s); //剩余可用空间
        size_t len, newlen;
        char type, oldtype = s[-1] & SDS_TYPE_MASK; //取SDS头部现有类型
        int hdrlen;
    
        /* Return ASAP if there is enough space left. */
        if (avail >= addlen) return s;  //无需扩容
    
        len = sdslen(s);
        sh = (char*)s-sdsHdrSize(oldtype); //sh指向头部
        newlen = (len+addlen);
        //修改后长度小于1MB则双倍扩容
        //否则,在按修改后长度+1MB扩容
        if (newlen < SDS_MAX_PREALLOC) 
            newlen *= 2;
        else
            newlen += SDS_MAX_PREALLOC;
        //新长度可能超出现有头部的描述范围,需对比新老头部类型
        //若新老头部类型一致,头部类型无需改变,可使用原头部,直接调大已分配的内存
        //否则,头部类型改变,需重分配内存,拷贝原有字符串并重新设置新头部
        type = sdsReqType(newlen);  //新头部类型
    
        /* Don't use type 5: the user is appending to the string and type 5 is
         * not able to remember empty space, so sdsMakeRoomFor() must be called
         * at every appending operation. */
        if (type == SDS_TYPE_5) type = SDS_TYPE_8;
    
        hdrlen = sdsHdrSize(type);  //新头部大小
        if (oldtype==type) {
            newsh = s_realloc(sh, hdrlen+newlen+1); //#define s_realloc zrealloc
            if (newsh == NULL) return NULL;
            s = (char*)newsh+hdrlen;
        } else {
            /* Since the header size changes, need to move the string forward,
             * and can't use realloc */
            newsh = s_malloc(hdrlen+newlen+1);  //#define s_malloc zmalloc
            if (newsh == NULL) return NULL;
            memcpy((char*)newsh+hdrlen, s, len+1);  
            s_free(sh);
            s = (char*)newsh+hdrlen;
            s[-1] = type;   //记录头部类型,以便后续设置头部其他成员
            sdssetlen(s, len);
        }
        sdssetalloc(s, newlen);
        return s;
    }

    sdsRemoveFreeSpace

    //去除SDS内的剩余可用空间
    sds sdsRemoveFreeSpace(sds s) {
        void *sh, *newsh;
        char type, oldtype = s[-1] & SDS_TYPE_MASK;
        int hdrlen;
        size_t len = sdslen(s);
        sh = (char*)s-sdsHdrSize(oldtype);
    
        type = sdsReqType(len);
        hdrlen = sdsHdrSize(type);
        if (oldtype==type) {
            newsh = s_realloc(sh, hdrlen+len+1);
            if (newsh == NULL) return NULL;
            s = (char*)newsh+hdrlen;
        } else {
            newsh = s_malloc(hdrlen+len+1);
            if (newsh == NULL) return NULL;
            memcpy((char*)newsh+hdrlen, s, len+1);
            s_free(sh);
            s = (char*)newsh+hdrlen;
            s[-1] = type;
            sdssetlen(s, len);
        }
        sdssetalloc(s, len);
        return s;
    }

    后记

    • 性能的提高可以通过合理的结构设计、额外的空间、减少系统调用与数据拷贝、合理地内联代码 来实现
    • 为了二进制安全最好不要以特定字符最为结束符,除非特定字符在使用场景出现概率为0 或 有额外结构弥补缺陷




    原创不易,转载请注明出处,谢谢
  • 相关阅读:
    结构体的malloc与数组空间
    绘制K线图
    加载文件
    数据分析(绘图)
    GIT操作
    疑难杂症汇总
    Shell编程2
    shell编程1
    shell命令2
    Shell命令1
  • 原文地址:https://www.cnblogs.com/Keeping-Fit/p/14052658.html
Copyright © 2020-2023  润新知