Redis3.2源碼分析-動(dòng)態(tài)字符串sds

最近打算閱讀redis源碼七蜘,但是擔(dān)心讀完就忘了,所以決定把閱讀的筆記在簡(jiǎn)書里記錄起來(lái),希望能夠堅(jiān)持讀下去翰蠢。之所以選擇3.2是因?yàn)楣景裷edis升級(jí)成了這個(gè)版本。

本文先介紹redis動(dòng)態(tài)字符串(sds)啰劲。

redis在處理字符串的時(shí)候沒有直接使用以'\0'結(jié)尾的C語(yǔ)言字符串梁沧,而是封裝了一下C語(yǔ)言字符串并命名為sds(simple dynamic string),在sds.h文件里我們可以看到如下類型定義:
typedef char *sds;
也就是說(shuō)實(shí)際上sds類型就是char*類型蝇裤,那sds和char*有什么區(qū)別呢廷支?
主要區(qū)別就是:sds一定有一個(gè)所屬的結(jié)構(gòu)(sdshdr),這個(gè)header結(jié)構(gòu)在每次創(chuàng)建sds時(shí)被創(chuàng)建栓辜,用來(lái)存儲(chǔ)sds以及sds的相關(guān)信息(下文sds的含義僅僅是redis的字符串恋拍,sdshdr才表示sds的header)。

那為什么redis不直接使用char*呢藕甩?總結(jié)起來(lái)理由如下:

  • 想用O(1)的時(shí)間復(fù)雜度獲取字符串長(zhǎng)度(利用sdshdr)施敢。
  • sds實(shí)現(xiàn)了部分自己的字符串處理函數(shù),能夠存儲(chǔ)二進(jìn)制字符串 保證二進(jìn)制安全狭莱,而所有C語(yǔ)言str前綴的字符串處理函數(shù)不保證二進(jìn)制安全(遇到'\0'就停下僵娃,認(rèn)為它是字符串的結(jié)尾,不能存二進(jìn)制數(shù)據(jù))腋妙。
  • 制定內(nèi)存重分配方法默怨,減少 因修改字符串而導(dǎo)致的 內(nèi)存分配和釋放 的次數(shù)。

這只是我看完代碼后得出的結(jié)論辉阶,看不懂也沒事先壕,先列出來(lái)只是為了直觀一點(diǎn)。當(dāng)然還有其他使用sds的理由谆甜,想到再加上垃僚。接下來(lái)看代碼:

1.sdshdr定義

sdshdr和sds是一一對(duì)應(yīng)的關(guān)系,一個(gè)sds一定會(huì)有一個(gè)sdshdr用來(lái)記錄sds的信息规辱。在redis3.2分支出現(xiàn)之前sdshdr只有一個(gè)類型谆棺,定義如下:

struct sdshdr {
    unsigned int len;//表示sds當(dāng)前的長(zhǎng)度
    unsigned int free;//已為sds分配的長(zhǎng)度-sds當(dāng)前的長(zhǎng)度
    char buf[];//sds實(shí)際存放的位置
};

這些版本的redis每次創(chuàng)建一個(gè)sds 不管sds實(shí)際有多長(zhǎng),都會(huì)分配一個(gè)大小固定的sdshdr。根據(jù)成員len的類型可知改淑,sds最多能存長(zhǎng)度為2^(8*sizeof(unsigned int))的字符串碍岔。
而3.2分支引入了五種sdshdr類型,每次在創(chuàng)建一個(gè)sds時(shí)根據(jù)sds的實(shí)際長(zhǎng)度判斷應(yīng)該選擇什么類型的sdshdr朵夏,不同類型的sdshdr占用的內(nèi)存空間不同蔼啦。這樣細(xì)分一下可以省去很多不必要的內(nèi)存開銷,下面是3.2的sdshdr定義:

struct __attribute__ ((__packed__)) sdshdr5 {
    //實(shí)際上這個(gè)類型redis不會(huì)被使用仰猖。他的內(nèi)部結(jié)構(gòu)也與其他sdshdr不同捏肢,直接看sdshdr8就好。
    unsigned char flags; //一共8位饥侵,低3位用來(lái)存放真實(shí)的flags(類型),高5位用來(lái)存放len(長(zhǎng)度)鸵赫。
    char buf[];//sds實(shí)際存放的位置
};
struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len;//表示當(dāng)前sds的長(zhǎng)度(單位是字節(jié))
    uint8_t alloc; //表示已為sds分配的內(nèi)存大小(單位是字節(jié))
    unsigned char flags; //用一個(gè)字節(jié)表示當(dāng)前sdshdr的類型,因?yàn)橛衧dshdr有五種類型躏升,所以至少需要3位來(lái)表示000:sdshdr5辩棒,001:sdshdr8,010:sdshdr16膨疏,011:sdshdr32一睁,100:sdshdr64。高5位用不到所以都為0成肘。
    char buf[];//sds實(shí)際存放的位置
};
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[];
};

根據(jù)以上定義卖局,我畫了一個(gè)sdshdr8圖來(lái)說(shuō)明它們的內(nèi)存布局:


首先要說(shuō)明之所以sizeof(struct sdshdr8)的大小是len+alloc+flags 是因?yàn)檫@個(gè)struct擁有一個(gè)柔性數(shù)組成員 buf斧蜕,柔性數(shù)組成員是C99之后引入的一個(gè)新feature双霍,這里可以通過(guò)sizeof整個(gè)struct給出buf變量的偏移量,從而確定buf的位置批销。Flexible array member, is a feature introduced in the [C99] standard of the C programming language,which is an array without a given dimension, and it must be the last member of such a struct. The 'sizeof' operator on such a struct is required to give the offset of the flexible array member. --維基百科

其次需要說(shuō)明的是定義sdshdr的這部分代碼用了__attribute__ ((__packed__))洒闸,這個(gè)語(yǔ)法不存在于任何C語(yǔ)言標(biāo)準(zhǔn),是GCC的一個(gè)extension均芽,用來(lái)告訴編譯器使用最小的內(nèi)存來(lái)存儲(chǔ)sdshdr丘逸。

packed: ...,This attribute, attached to struct or union type definition, specifies that each member of the structure or union is placed to minimize the memory required.

引用里"minimize the memory required"其實(shí)就是讓編譯器盡量不使用內(nèi)存對(duì)齊(alignment),以避免不必要的空間浪費(fèi)掀宋,但其實(shí)這么做會(huì)有時(shí)間上的開銷深纲,假設(shè)CPU總是從存儲(chǔ)器中讀取8個(gè)字節(jié),則變量地址必須為8的倍數(shù)劲妙,為了獲取一個(gè)沒對(duì)齊的8字節(jié)的uint8_t數(shù)據(jù)湃鹊,CPU需要執(zhí)行兩次內(nèi)存訪問(wèn) 從兩個(gè)8字節(jié)的內(nèi)存塊中取出完整的8字節(jié)數(shù)據(jù)。關(guān)于內(nèi)存對(duì)齊的更多信息镣奋,《深入理解計(jì)算機(jī)系統(tǒng)》第三章和《程序員的自我修養(yǎng)》 都有非常詳細(xì)的描述币呵。但這里我們只需要知道禁用(準(zhǔn)確地說(shuō)是盡量不使用)內(nèi)存對(duì)齊是redis為了節(jié)省內(nèi)存開支的一種手段。

接下來(lái)分析每個(gè)成員:

  • len表示sds當(dāng)前sds的長(zhǎng)度(單位是字節(jié))侨颈,不包括'\0'終止符余赢,通過(guò)len直接獲取字符串長(zhǎng)度芯义,不需要掃一遍string,這就是上文說(shuō)的封裝sds的理由之一妻柒;
  • alloc表示當(dāng)前為sds分配的大小(單位是字節(jié))(3.2以前的版本用的free是表示還剩free字節(jié)可用空間)扛拨,不包括'\0'終止符;
  • flags表示當(dāng)前sdshdr的類型举塔,聲明為char 一共有1個(gè)字節(jié)(8位)鬼癣,僅用低三位就可以表示所有5種sdshdr類型(詳見上文代碼注釋):
#define SDS_TYPE_5  0 //00000000
#define SDS_TYPE_8  1 //00000001
#define SDS_TYPE_16 2 //00000010
#define SDS_TYPE_32 3 //00000011
#define SDS_TYPE_64 4 //00000100

#define SDS_TYPE_MASK 7 //00000111,作為取flags低3位的掩碼

要判斷一個(gè)sds屬于什么類型的sdshdr啤贩,只需 flags&SDS_TYPE_MASKSDS_TYPE_n比較即可(之所以需要SDS_TYPE_MASK是因?yàn)橛衧dshdr5這個(gè)特例待秃,它的高5位不一定為0,參考上面sdshdr5定義里的代碼注釋)

sds.h里所有給出定義的內(nèi)聯(lián)函數(shù)都是通過(guò)sds作為參數(shù)痹屹,通過(guò)比較flags&SDS_TYPE_MASKSDS_TYPE_n來(lái)判斷該sds屬于哪種類型sdshdr章郁,再按照指定的sdshdr類型取出sds的相關(guān)信息。
例如sdslen函數(shù):

#define SDS_HDR(T,s) ((struct sdshdr##T *)((s)-(sizeof(struct sdshdr##T)))) //返回一個(gè)類型為T包含s字符串的sdshdr的指針
#define SDS_TYPE_5_LEN(f) ((f)>>SDS_TYPE_BITS)  //用sdshdr5的flags成員變量做參數(shù)返回sds的長(zhǎng)度志衍,這其實(shí)是一個(gè)沒辦法的hack
#define SDS_TYPE_BITS 3 
static inline size_t sdslen(const sds s) {
    unsigned char flags = s[-1]; //sdshdr的flags成員變量
    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;//取出sdshdr的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;
}

第一行里的雙井號(hào)##的意思是在一個(gè)宏(macro)定義里連接兩個(gè)子串(token)暖庄,連接之后這##號(hào)兩邊的子串就被編譯器識(shí)別為一個(gè)。
sdslen函數(shù)里第一行出現(xiàn)了s[-1]楼肪,看起來(lái)感覺會(huì)是一個(gè)undefined behavior培廓,其實(shí)不是,這是一種正常又正確的使用方式春叫,它就等同于*(s-1)肩钠。The de?nition of the subscript operator [] is that E1[E2] is identical to (*((E1)+(E2))). --C99。又因?yàn)閟是一個(gè)sds(char*)所以s指向的類型是char暂殖,-1就是-1*sizeof(char)价匠,這也剛好是一個(gè)flags(unsigned char)的地址,所以通過(guò)s[-1]我們可以獲得sds所屬的sdshdr的成員變量flags呛每。
類似sdslen這樣利用sds找到sdshdr類型的還有如下幾個(gè)函數(shù)踩窖,就不一一分析了:

static inline size_t sdsavail(const sds s)
static inline void sdssetlen(sds s, size_t newlen)
static inline void sdsinclen(sds s, size_t inc)
static inline size_t sdsalloc(const sds s)
static inline void sdssetalloc(sds s, size_t newlen)

2.創(chuàng)建一個(gè)sds

前面說(shuō)的是在已有結(jié)果的情況下,根據(jù)一個(gè)sds通過(guò)flags變量來(lái)判斷它的sdshdr類型晨横。那么最開始創(chuàng)建一個(gè)sds時(shí)應(yīng)該選用什么類型的sdshdr來(lái)存放它的信息呢洋腮?這就得根據(jù)要存儲(chǔ)的sds的長(zhǎng)度決定了,redis在創(chuàng)建一個(gè)sds之前會(huì)調(diào)用sdsReqType(size_t string_size)來(lái)判斷用哪個(gè)sdshdr手形。該函數(shù)傳遞一個(gè)sds的長(zhǎng)度作為參數(shù)啥供,返回應(yīng)該選用的sdshdr類型。

static inline char sdsReqType(size_t string_size) {
    if (string_size < 1<<5) //小于2^5叁幢,flags成員的高5位即可表示
        return SDS_TYPE_5;
    if (string_size < 1<<8) //小于2^8滤灯,8位整數(shù)(sdshdr8里的uint8_t)即可表示string_size
        return SDS_TYPE_8;
    if (string_size < 1<<16) //小于2^16,16位整數(shù)(sdshdr16里的uint16_t)即可表示string_size
        return SDS_TYPE_16;
    if (string_size < 1ll<<32) /小于2^32,32位整數(shù)(sdshrd32里的uint32_t)即可表示string_size鳞骤,1ll是指1long long(至少64位)的意思窒百,如果沒有l(wèi)l,1就是一個(gè)int豫尽,假設(shè)int為4字節(jié)32位篙梢,1<<32就會(huì)導(dǎo)致undefined behavior.
        return SDS_TYPE_32;
    return SDS_TYPE_64; //若sds的長(zhǎng)度超過(guò)2^64,則所有類型都不法表示這個(gè)sds的len
}

知道了創(chuàng)建一個(gè)sds時(shí)應(yīng)選用什么類型的sdshdr后我們就可以看看創(chuàng)建sds的函數(shù)了:

//用init指針指向的內(nèi)存的內(nèi)容截取initlen長(zhǎng)度來(lái)new一個(gè)sds美旧,這個(gè)函數(shù)是二進(jìn)制安全的
sds sdsnewlen(const void *init, size_t initlen) {
    void *sh;//sdshdr的指針
    sds s; //char * s;
    char type = sdsReqType(initlen);//根據(jù)需要的長(zhǎng)度決定sdshdr的類型
    /* 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;//如果initlen為空并且sdshdr的類型為sdshdr5渤滞,則將類型設(shè)置為sdshdr8
    int hdrlen = sdsHdrSize(type);//每個(gè)sdshdr類型的大小都不一樣,根據(jù)類型返回sdshdr的大小以計(jì)算需要分配的空間
    unsigned char *fp; /* flags pointer. */

    sh = s_malloc(hdrlen+initlen+1);//在heap里申請(qǐng)一段連續(xù)的空間給sdshdr和屬于它的sds榴嗅,+1是因?yàn)橐谖膊糠胖?\0'
    if (!init)
        memset(sh, 0, hdrlen+initlen+1);//如果init為空妄呕,則整個(gè)sdshdr都用0即字符'\0'初始化
    if (sh == NULL) return NULL;
    s = (char*)sh+hdrlen;//通過(guò)sdshdr指針找到sds的位置
    fp = ((unsigned char*)s)-1;//找到flags的位置,等同于&s[-1]
    switch(type) {
        case SDS_TYPE_5: {
            *fp = type | (initlen << SDS_TYPE_BITS);//initlen左移3位到高5位嗽测,給type騰出位置绪励,和type做或運(yùn)算
            break;
        }
        case SDS_TYPE_8: {
            SDS_HDR_VAR(8,s);//#define SDS_HDR_VAR(T,s) struct sdshdr##T *sh = (void*)((s)-(sizeof(struct sdshdr##T))); 可以理解為在switch作用域下申明了一個(gè)新的局部變量sh,類型是struct sdshdr##T唠粥,跟外面的sh值一樣疏魏,變量名一樣,但不是一個(gè)東西晤愧。
            sh->len = initlen;
            sh->alloc = initlen;
            *fp = type;//設(shè)置flags
            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); //memcpy不會(huì)因?yàn)?\0'而停下大莫,支持二進(jìn)制數(shù)據(jù)的拷貝
    s[initlen] = '\0'; //不管是不是二進(jìn)制數(shù)據(jù),尾部都會(huì)加上'\0'
    return s;
}
static inline int sdsHdrSize(char type) {
    switch(type&SDS_TYPE_MASK) {
        case SDS_TYPE_5:
            return sizeof(struct sdshdr5);//之前說(shuō)的柔性數(shù)組成員不會(huì)計(jì)入struct的大小官份,所以這個(gè)hdrsize沒有包括sds的長(zhǎng)度
        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;
}

這就是用于新建或拷貝sds的代碼只厘,流程寫在注釋里可能還是不夠清晰,結(jié)合圖來(lái)看應(yīng)該會(huì)好一點(diǎn)(sdshdr5的圖不一樣贯吓,我就偷懶不畫了懈凹,反正也不用它):



流程如下:

  • 根據(jù)sds的長(zhǎng)度判斷需要選用sdshdr的類型
  • 根據(jù)sdshdr的類型用sdsHdrSize函數(shù)得到hdrlen(其實(shí)就是sizeof(struct sdshdr))
  • 為sdshdr分配一個(gè)hdrlen+initlen+1大小的堆內(nèi)存(+1是為了放置'\0'蜀变,這個(gè)'\0'不計(jì)入alloc或len)
  • 按參數(shù)填充成員變量len悄谐、alloc和type
  • 用memcpy給sds賦值,并在尾部加上'\0'

如果用字符串"PHP is the best programming language"(長(zhǎng)度為36) 調(diào)用sdsnewlen()库北,最終會(huì)產(chǎn)生如下布局:


3.內(nèi)存重分配

上文提到 redis不直接使用C語(yǔ)言字符串還有個(gè)原因是為了定制的自己的內(nèi)存重分配的方法爬舰,減少因堆內(nèi)存申請(qǐng)與釋放產(chǎn)生的時(shí)間開銷。
如果redis使用的是普通的C語(yǔ)言字符串char*寒瓦,那么每次拼接或者截?cái)嘁粋€(gè)字符串之前都需要重新分配/釋放內(nèi)存情屹,否則會(huì)造成內(nèi)存溢出或泄露。但是每次進(jìn)行分配/釋放內(nèi)存的操作又非常影響性能杂腰,所以redis做了兩件事:

  • 提供一個(gè)函數(shù)sdsMakeRoomFor垃你,當(dāng)需要擴(kuò)充一個(gè)sds字符串時(shí)調(diào)用它,它的內(nèi)部實(shí)現(xiàn)了sds的內(nèi)存分配算法,盡可能地降低內(nèi)存分配次數(shù)惜颇。
  • 在截?cái)嘁粋€(gè)sds字符串時(shí)不立刻釋放掉之前為它申請(qǐng)的內(nèi)存皆刺,而是通過(guò)alloc成員變量記錄下這個(gè)值,供后續(xù)操作使用凌摄,然后提供一個(gè)函數(shù)sdsRemoveFreeSpace讓我們根據(jù)需要釋放掉多出的內(nèi)存羡蛾。

1.sds加長(zhǎng)

下面是sdsMakeRoomFor的源碼:

//注意:這個(gè)函數(shù)是在擴(kuò)充sds前調(diào)用,sds不會(huì)被擴(kuò)充也不會(huì)改變len
sds sdsMakeRoomFor(sds s, size_t addlen) { //addlen是擴(kuò)充部分的長(zhǎng)度
    void *sh, *newsh;
    size_t avail = sdsavail(s);
    size_t len, newlen;
    char type, oldtype = s[-1] & SDS_TYPE_MASK;
    int hdrlen;

    /* Return ASAP if there is enough space left. */
    if (avail >= addlen) return s;//如果足夠存放擴(kuò)充的部分锨亏,則直接返回不申請(qǐng)內(nèi)存痴怨。

    len = sdslen(s);
    sh = (char*)s-sdsHdrSize(oldtype);
    newlen = (len+addlen);
    if (newlen < SDS_MAX_PREALLOC) 
        //擴(kuò)充后的總長(zhǎng)度小于1M(1024*1024),則直接多分配newlen個(gè)字節(jié)閑置器予。
        newlen *= 2;
    else
        //擴(kuò)充后的總長(zhǎng)度大于1M(1024*1024)浪藻,則多分配1M字節(jié)閑置
        newlen += SDS_MAX_PREALLOC;

    type = sdsReqType(newlen);//根據(jù)擴(kuò)充后的總長(zhǎng)度決定需要這個(gè)sds要用什么類型的sdshdr

    /* 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) {
        //如果擴(kuò)充后的sdshdr類型不變,則在原有的地方realloc就好乾翔。因?yàn)閘en和alloc的類型還是原來(lái)的珠移。
        //ps: s_realloc封裝了realloc,realloc返回的指針未必是sh指向的地址末融,可能為了內(nèi)存對(duì)齊移動(dòng)了這塊內(nèi)存
        newsh = s_realloc(sh, hdrlen+newlen+1);
        if (newsh == NULL) return NULL;
        s = (char*)newsh+hdrlen;
    } else {
        //如果擴(kuò)充后的sdshdr類型變了钧惧,那就只能重新在別的地方分配內(nèi)存,然后重新賦值勾习,釋放掉舊的內(nèi)存浓瞪。
        /* Since the header size changes, need to move the string forward,
         * and can't use realloc */
        newsh = s_malloc(hdrlen+newlen+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, newlen);
    return s;
}

剛才已經(jīng)創(chuàng)建了的sds"PHP is the best programming language",它的len是36巧婶,alloc也是36乾颁,現(xiàn)在給它擴(kuò)充一下,把" in the world"加上艺栈,算上空格一共要加13個(gè)字節(jié)英岭,這時(shí)會(huì)調(diào)用sdscatlen()函數(shù)完成整個(gè)拼接sds的操作,它內(nèi)部需要先調(diào)用sdsMakeRoomFor(s, 13)走一遍內(nèi)存重分配的算法湿右,以下是sdsMakeRoomFor的流程

  • len+addlen=49個(gè)字節(jié)诅妹,小于SDS_MAX_PREALLOC(1M),所以最終會(huì)分配49*2=98字節(jié)的內(nèi)存給這個(gè)需要擴(kuò)充的sds字符串(實(shí)際是99毅人,多分配一字節(jié)的'\0')吭狡。
  • 根據(jù)sdsReqType()函數(shù),98字節(jié)小于2^8字節(jié)丈莺,所以擴(kuò)充后的類型仍是sdshdr8划煮,realloc一片內(nèi)存。
  • 重置sds的alloc為98缔俄。

最終獲得如下內(nèi)存布局:



接下來(lái)sdscatlen()函數(shù)用memcpy把" in the world"append到這個(gè)已經(jīng)經(jīng)歷過(guò)內(nèi)存分配的sds的尾部弛秋,并更新len:



附上sdscatlen的代碼:
sds sdscatlen(sds s, const void *t, size_t len) {
    size_t curlen = sdslen(s);

    s = sdsMakeRoomFor(s,len);
    if (s == NULL) return NULL;
    memcpy(s+curlen, t, len);
    sdssetlen(s, curlen+len);
    s[curlen+len] = '\0';
    return s;
}

2.sds縮減

我粗略地在源碼里找了找器躏,縮短sds字符串的有三個(gè)函數(shù):sdsclear、sdstrim蟹略、sdsrange邀桑,他們都不會(huì)改變alloc的大小即不會(huì)釋放任何內(nèi)存,這就是sds字符串內(nèi)存管理的一種方式:惰性釋放科乎。額外調(diào)用sdsRemoveFreeSpace釋放內(nèi)存壁畸,這樣就節(jié)省了每次sds縮減長(zhǎng)度而導(dǎo)致的內(nèi)存釋放開銷。
三個(gè)縮短sds的函數(shù)就不一一介紹了茅茂,有興趣直接去代碼里看就好捏萍,需要注意的這些函數(shù)里移動(dòng)字符串用的memmove()是允許內(nèi)存重疊的,這點(diǎn)跟memcpy()不一樣空闲。
下面介紹一下sdsRemoveFreeSpace令杈,先放源碼:

//這個(gè)函數(shù)壓縮內(nèi)存,讓alloc=len碴倾。如果type變小了逗噩,則另開一片內(nèi)存復(fù)制,如果type不變跌榔,則realloc
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);
    //這之后的代碼就跟sdsMakeRoomFor后面的代碼差不多了异雁,釋放掉多余內(nèi)存并重置alloc。
    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;
}

繼續(xù)拿之前用sdscatlen()擴(kuò)充的sds執(zhí)行一遍sdsRemoveFreeSpace()僧须,會(huì)得到如下布局:


4.小結(jié)

以上就是sds相關(guān)代碼分析纲刀,還有很多針對(duì)sds的基本操作函數(shù)在這里就不一一列舉了。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末担平,一起剝皮案震驚了整個(gè)濱河市示绊,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌暂论,老刑警劉巖面褐,帶你破解...
    沈念sama閱讀 218,941評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異取胎,居然都是意外死亡展哭,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門扼菠,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)摄杂,“玉大人,你說(shuō)我怎么就攤上這事循榆。” “怎么了墨坚?”我有些...
    開封第一講書人閱讀 165,345評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵秧饮,是天一觀的道長(zhǎng)映挂。 經(jīng)常有香客問(wèn)我,道長(zhǎng)盗尸,這世上最難降的妖魔是什么柑船? 我笑而不...
    開封第一講書人閱讀 58,851評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮泼各,結(jié)果婚禮上鞍时,老公的妹妹穿的比我還像新娘。我一直安慰自己扣蜻,他們只是感情好逆巍,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,868評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著莽使,像睡著了一般锐极。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上芳肌,一...
    開封第一講書人閱讀 51,688評(píng)論 1 305
  • 那天灵再,我揣著相機(jī)與錄音,去河邊找鬼亿笤。 笑死翎迁,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的净薛。 我是一名探鬼主播鸳兽,決...
    沈念sama閱讀 40,414評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼罕拂!你這毒婦竟也來(lái)了揍异?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,319評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤爆班,失蹤者是張志新(化名)和其女友劉穎衷掷,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體柿菩,經(jīng)...
    沈念sama閱讀 45,775評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡戚嗅,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了枢舶。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片懦胞。...
    茶點(diǎn)故事閱讀 40,096評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖凉泄,靈堂內(nèi)的尸體忽然破棺而出躏尉,到底是詐尸還是另有隱情,我是刑警寧澤后众,帶...
    沈念sama閱讀 35,789評(píng)論 5 346
  • 正文 年R本政府宣布胀糜,位于F島的核電站颅拦,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏教藻。R本人自食惡果不足惜距帅,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,437評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望括堤。 院中可真熱鬧碌秸,春花似錦、人聲如沸悄窃。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)广匙。三九已至允趟,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間鸦致,已是汗流浹背潮剪。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評(píng)論 1 271
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留分唾,地道東北人抗碰。 一個(gè)月前我還...
    沈念sama閱讀 48,308評(píng)論 3 372
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像绽乔,于是被迫代替她去往敵國(guó)和親弧蝇。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,037評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容