最近打算閱讀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_MASK和SDS_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_MASK和SDS_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ù)在這里就不一一列舉了。