1 簡介&基礎用法
Redis 中用得最多的就是字符串,在 C 語言中其實可以直接使用 char* 字符數組來實現字符串,也有很多可以直接使用得函數。但是 Redis 並沒有使用 C 語言原生的字符串,而是自己實現了一個 SDS(簡單動態字符串,Simple Dynamic String) 。
Redis 的 SDS 兼容了 C 語言的字符串類型的用法,
下面是 Redis 中 string 類型最常用的用法:
本地:0>set hello world
OK
本地:0>get hello
world
本地:0>type hello
string
本地:0>strlen hello
5
2 為什麼 Redis 自實現字符串?
2.1 存儲二進制的限制
C 語言的 char* 是以 \0 作為結束字符串的標識,如果需要存儲的數據中本身就含有 \0 ,那就沒有辦法正確表示,而像圖像這種數據,一般存儲下來都是二進制格式的,所以 Redis 不能直接使用 char*。
下面是 C 語言的 \0 對字符串長度判斷的影響:
#include "stdio.h"
#include "string.h"
int main(void) {
char *a = "hello\0Wolrd";
char *b = "helloWolrd\0";
printf("字符串的長度:%lu\n",
strlen(a)
); printf("字符串的長度:%lu\n",
strlen(b)
);}
輸出結果則會不一樣,\0 後面的數據會被截斷:
字符串的長度:5
字符串的長度:10
在 SDS 結構中卻能保證二進制安全,因為 SDS 保存了 len 屬性,這就可以不適用 \0 這個標識來判斷字符串是否結束。
2.2 操作效率問題
2.2.1 空間效率
2.2.1.1 預分配內存
原生的 C 語言字符串,在添加的時候,可能會因為可用空間不足,無法添加,而 Redis 追加字符串的時候,使用了預分配的策略,如果內存不夠,先進行內存拓展,再追加,有效減少修改字符串帶來的內存重新分配的次數。
類似於 Java 中的 ArrayList,採取預分配,內部真實的容量一般都是大於實際的字符串的長度的,當字符串的長度小於 1MB 的時候,如果內存不夠,擴容都是加倍現在的空間;如果字符串的長度已經超過了 1MB,擴容的時候也只會多擴 1MB 的空間,但是最大的字符串的長度是 512MB。
2.2.1.2 惰性空間釋放
惰性空間釋放用於優化 SDS 的字符串縮短操作,當 SDS 的 API 需要縮短字符串保存的字符串的時候,程序並不會立即使用內存重新分配來回縮短多出來的字節,而是使用 free 屬性將這些字節的數量記錄下來,並等待將來使用。
當然 SDS 也提供了 SDS 顯式調用,真正的釋放未使用的空間。
2.2.2 操作效率
原生的 C 語言在獲取字符的長度的時候,底層實際是遍歷,時間複雜度是 O(n) ,String 作為 Redis 用得最多的數據類型,獲取字符串的長度是比較頻繁的操作,肯定不能這麼幹,那就用一個變量把 String 的長度存儲起來,獲取的時候時間複雜度是 O(1)。
2.2.3 兼容性較好
Redis 雖然使用了 \0 來結尾,但是 sds 字符串的末端還是會遵循 c 語言的慣例,所以可以重用一部分<string. h> 的函數。比如對比的函數 strcasecmp ,可以用來對比 SDS 保存的字符串是否和另外一個字符串是否相同。
strcasecmp(sds->buf,"hello world");
3 源碼解讀
3.1 簡單指針介紹
數組的指針操作:
#include <stdio.h>
int main() {
printf("Hello, World!\n");
char t[] = {'a','b','c','d'};
char* s = t+1; // 指針前進一位
char bb = s[0];
char cc = s[1];
char dd = s[2];
char flag = s[-1]; // 指針後退一位等價於 char flag = *(s - 1); 或者 char *flag = s - 1; printf("%c %c %c %c", *flag, bb, cc ,dd);
printf("%c %c %c %c", flag, bb, cc ,dd);
return 0;
}
最終輸出結果:
Hello, World!
a b c d
3.1.1 sdshdr 巧妙的結構設計
SDS 的相關代碼主要在下面兩個文件:
- sds. h:頭文件
- sds. c:源文件
SDS 定義在 sds. h 中,為了兼容 C 風格的字符串,給 char 取了個別名叫 sds :
typedef char *sds;
《Redis 設計與實現》中,解釋的是 Redis 3.0 的代碼,提到 sds 的實現結構 sdshdr 是這樣的:
struct sdshdr {
// 記錄buf數組已使用字節的數量
// 等於SDS所保存字符串的長度
int len;
// 記錄buf數組中未使用的字節數
int free;
// 字節數組,用於保存字符串
char buf[];
};
但是實際上 7.0 版本已經是長這樣:
-
- Sdshdr5 從未被使用,我們只是直接訪問標誌字節。
- 然而,這裏文檔標識 sdshdr5 的結構。
-
- 結構定義使用了__attribute__ ((__packed__))聲明為非內存對齊, 緊湊排列形式(取消編譯階段的內存優化對齊功能)
-
- 如果定義了一個
sds *s, 可以非常方便的使用s[-1]獲取到 flags 地址,避免了在上層調用各種類型判斷。
- 如果定義了一個
struct __attribute__ ((__packed__)) sdshdr5 {
unsigned char flags; /* 低 3 位存儲類型,高 5 位存儲字符串長度 */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 {
uint8_t len; /* 已使用 */
uint8_t alloc; /* 總分配的,不包括頭部和空的終止符*/
unsigned char flags; /* 低 3 位存儲類型,高 5 位預留,還沒使用 */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr16 {
uint16_t len; /* 已使用 */
uint16_t alloc; /* 總分配的,不包括頭部和空的終止符*/
unsigned char flags; /* 低 3 位存儲類型,高 5 位預留,還沒使用 */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {
uint32_t len; /* 已使用 */
uint32_t alloc; /* 總分配的,不包括頭部和空的終止符*/
unsigned char flags; /* 低 3 位存儲類型,高 5 位預留,還沒使用 */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {
uint64_t len; /* used */
uint64_t alloc; /* 總分配的,不包括頭部和空的終止符*/
unsigned char flags; /* 低 3 位存儲類型,高 5 位預留,還沒使用 */
char buf[];
};
// 類型定義一共佔用了 0,1,2,3,4 五個數字,也就是三位就可以標識,
// 那麼我們可以使用 flags&SDS_TYPE_MASK 來獲取動態字符串對應的字符串類型
#define SDS_TYPE_5 0
#define SDS_TYPE_8 1
#define SDS_TYPE_16 2
#define SDS_TYPE_32 3
#define SDS_TYPE_64 4
#define SDS_TYPE_MASK 7
#define SDS_TYPE_BITS 3
#define SDS_HDR_VAR(T,s) struct sdshdr##T *sh = (void*)((s)-(sizeof(struct sdshdr##T)));
#define SDS_HDR(T,s) ((struct sdshdr##T *)((s)-(sizeof(struct sdshdr##T))))
#define SDS_TYPE_5_LEN(f) ((f)>>SDS_TYPE_BITS)
上面定義了 4 種結構體,**Redis 根據不同的字符串的長度,來選擇合適的結構體,每個結構體有對應數據部分和頭部。
類型一共有這些:
#define SDS_TYPE_5 0
#define SDS_TYPE_8 1
#define SDS_TYPE_16 2
#define SDS_TYPE_32 3
#define SDS_TYPE_64 4
用二進制表示,只需要 3 位即可,這也是為什麼上面的結構體 sdshdr5 裏面的 flags 字段註釋裏寫的:前三位表示類型,後 5 位用於表示字符串長度。
unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
而其他的 sds 結構體類型,因為長度太長了,存不下,所以後 5 位暫時沒有作用,而是另外使用屬性存儲字符串的長度。
uint8_t len; /* used */
uint8_t alloc; /* excluding the header and null terminator */
3.2 3.2 attribute 的作用是什麼?
__attribute__ ((packed)) 的作用就是告訴編譯器取消結構在編譯過程中的優化對齊, 按照實際佔用字節數進行對齊,是 GCC 特有的語法。這個功能是跟操作系統沒關係,跟編譯器有關,gcc 編譯器不是緊湊模式的。
__attribute__關鍵字主要是用來在函數或數據聲明中設置其屬性。給函數賦給屬性的主要目的在於讓編譯器進行優化。函數聲明中的__attribute__((noreturn)),就是告訴編譯器這個函數不會返回給調用者,以便編譯器在優化時去掉不必要的函數返回代碼。
__attribute__書寫特徵是:__attribute__前後都有兩個下劃線,並且後面會緊跟一對括弧,括弧裏面是相應的__attribute__參數,其語法格式為:
__attribute__ ((attribute-list))
下面是實驗的一些代碼,實驗環境為 Mac:
#include "stdio.h"
struct One{ char ch; int a;} one;
struct __attribute__ ((__packed__)) Tow{ char ch; int a;} tow;
int main(void) {
printf("int 的內存大小:%lu\n",
sizeof(int)
); printf("新結構體one的大小(不壓縮):%lu\n",
sizeof(one)
); printf("新結構體tow的大小(壓縮):%lu\n",
sizeof(tow)
);}
運行結果:
int 的內存大小:4
新結構體one的大小(不壓縮):8
新結構體tow的大小(壓縮):5
編譯器壓縮優化(內存不對齊)後,確實體積從 8 變成了 5,縮小了不少,別看這小小的變化,其實在巨大的數量面前,就是很大的空間優化。
3.3 宏操作
redis 基於前面 sds 設計,定義了一些十分巧妙的宏操作:
3.3.1 通過 sds 獲取不同類型 sdshdr 變量
/*
* 宏操作
* SDS_HDR_VAR(8,s);
* 下面是對應宏定義翻譯的產物
* struct sdshdr8 *sh = (void*)((s)-(sizeof(struct sdshdr##T)));
* 可以根據指向 buf 的sds變量s得到 sdshdr8 的指針,sh 是創建出來的變量
*/
#define SDS_HDR_VAR(T,s) struct sdshdr##T *sh = (void*)((s)-(sizeof(struct sdshdr##T)));
/**
* 和上面類似
* 根據指向buf的sds變量s得到sdshdr的指針,只不過這裏是獲取的是指針地址
*/
#define SDS_HDR(T,s) ((struct sdshdr##T *)((s)-(sizeof(struct sdshdr##T))))
3.3.2 獲取 sdshdr5 字符串類型的長度
/**
* 該函數就是獲取sdshdr5字符串類型的長度,由於根本不使用sdshdr5類型,所以需要直接返回空,
* 而flags成員使用最低三位有效位來表示類型,所以讓f代表的flags的值右移三位即可
*/
#define SDS_TYPE_5_LEN(f) ((f)>>SDS_TYPE_BITS)
3.3.3 通過 sds 獲取 len 的值
/**
* 使用到了取消編譯階段的內存優化對齊功能,直接使用s[-1]獲取到flags成員的值,
* 然後根據flags&&SDS_TYPE_MASK來獲取到動態字符串對應的類型進而獲取動態字符串的長度。
* SDS_TYPE_5_LEN 比較特殊一點,因為結構有點不一樣
*/
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;
}
3.4 創建新字符串
創建新的字符串一般是傳遞初始化的長度:
sds sdsnewlen(const void *init, size_t initlen) {
// 內部封裝的函數,最後一個參數是是否嘗試分配
return _sdsnewlen(init, initlen, 0);
}
下面我們看具體的函數實現:
創建的返回的是指針,指向的是結構體中 buf 開始的位置,
sds _sdsnewlen(const void *init, size_t initlen, int trymalloc) {
// sh 指向sds分配開始的地方
void *sh;
// s 也是指針,指向 buf 開始的位置
sds s;
// 不同長度返回不同的類型sds
char type = sdsReqType(initlen);
/* Empty strings are usually created in order to append. Use type 8
* since type 5 is not good at this. */
// 空字符串經常被創建出來之後,就會執行append操作,所以用type 8替換掉它,type 5 太短了。
if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;
// 獲取整個struct的長度
int hdrlen = sdsHdrSize(type);
// flag 指針,標識sds 是哪一個類型的
unsigned char *fp; /* flags pointer. */
// 可用大小
size_t usable;
// 防止溢出
assert(initlen + hdrlen + 1 > initlen); /* Catch size_t overflow */
// 分配內存,其中s_trymalloc_usable是調整內存,s_malloc_usable是新分配內存,是兩種內存分配的方式,通過參數trymalloc控制(+1 是為了處理 \0)
sh = trymalloc?
s_trymalloc_usable(hdrlen+initlen+1, &usable) :
s_malloc_usable(hdrlen+initlen+1, &usable);
// 分配不成功,提前結束
if (sh == NULL) return NULL;
// 如果需要完全為空的字符串,直接返回null
if (init==SDS_NOINIT)
init = NULL;
else if (!init)
memset(sh, 0, hdrlen+initlen+1); // 初始化
// s 指向數組 buf 的位置(從結構體往後加上hdrlen就是buf數組開頭的位置)
s = (char*)sh+hdrlen;
// buf數組的位置-1,就是flags字段的位置
fp = ((unsigned char*)s)-1;
// 可用空間減去hdrlen(已用空間),再減1(‘\0‘)
usable = usable-hdrlen-1;
// 如果可用空間大於當前結構體中alloc字段的大小,就使用alloc的最大值
if (usable > sdsTypeMaxSize(type))
usable = sdsTypeMaxSize(type);
// 初始化不同類型的數組,字符串長度,可用大小和類型
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 = usable;
*fp = type;
break;
}
case SDS_TYPE_16: {
SDS_HDR_VAR(16,s);
sh->len = initlen;
sh->alloc = usable;
*fp = type;
break;
}
case SDS_TYPE_32: {
SDS_HDR_VAR(32,s);
sh->len = initlen;
sh->alloc = usable;
*fp = type;
break;
}
case SDS_TYPE_64: {
SDS_HDR_VAR(64,s);
sh->len = initlen;
sh->alloc = usable;
*fp = type;
break;
}
}
if (initlen && init)
memcpy(s, init, initlen);
s[initlen] = '\0';
return s;
}
3.5 獲取可用空間
SDS 和我平常所用到的 C 語言的原生字符串有差別,因為從獲取可用空間的計算方法來看,並未考慮到字符串需要以 \0 結尾,結構體本身帶有長度的成員 len,不需要 \0 來做字符串結尾的判定,而且不使用 \0 作為結尾有很多好處, 分配的減去使用的即可。
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;
}
3.6 設置 & 增加 sds 的長度
// 設置 sds 的長度
static inline void sdssetlen(sds s, size_t newlen) {
unsigned char flags = s[-1];
switch(flags&SDS_TYPE_MASK) {
case SDS_TYPE_5:
{
unsigned char *fp = ((unsigned char*)s)-1;
*fp = SDS_TYPE_5 | (newlen << SDS_TYPE_BITS);
}
break;
case SDS_TYPE_8:
SDS_HDR(8,s)->len = newlen;
break;
case SDS_TYPE_16:
SDS_HDR(16,s)->len = newlen;
break;
case SDS_TYPE_32:
SDS_HDR(32,s)->len = newlen;
break;
case SDS_TYPE_64:
SDS_HDR(64,s)->len = newlen;
break;
}
}
// 增加 sds 的長度
static inline void sdsinclen(sds s, size_t inc) {
unsigned char flags = s[-1];
switch(flags&SDS_TYPE_MASK) {
case SDS_TYPE_5:
{
unsigned char *fp = ((unsigned char*)s)-1;
unsigned char newlen = SDS_TYPE_5_LEN(flags)+inc;
*fp = SDS_TYPE_5 | (newlen << SDS_TYPE_BITS);
}
break;
case SDS_TYPE_8:
SDS_HDR(8,s)->len += inc;
break;
case SDS_TYPE_16:
SDS_HDR(16,s)->len += inc;
break;
case SDS_TYPE_32:
SDS_HDR(32,s)->len += inc;
break;
case SDS_TYPE_64:
SDS_HDR(64,s)->len += inc;
break;
}
}
3.7 設置 & 獲取已分配空間大小
/* sdsalloc() = sdsavail() + sdslen() */
// 獲取 sds 已經分配的空間的大小
static inline size_t sdsalloc(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)->alloc;
case SDS_TYPE_16:
return SDS_HDR(16,s)->alloc;
case SDS_TYPE_32:
return SDS_HDR(32,s)->alloc;
case SDS_TYPE_64:
return SDS_HDR(64,s)->alloc;
}
return 0;
}
// 設置 sds 已經分配的空間的大小
static inline void sdssetalloc(sds s, size_t newlen) {
unsigned char flags = s[-1];
switch(flags&SDS_TYPE_MASK) {
case SDS_TYPE_5:
/* Nothing to do, this type has no total allocation info. */
break;
case SDS_TYPE_8:
SDS_HDR(8,s)->alloc = newlen;
break;
case SDS_TYPE_16:
SDS_HDR(16,s)->alloc = newlen;
break;
case SDS_TYPE_32:
SDS_HDR(32,s)->alloc = newlen;
break;
case SDS_TYPE_64:
SDS_HDR(64,s)->alloc = newlen;
break;
}
}
3.8 擴大 sds 空間
/**
* 擴大sds字符串末尾的空閒空間,以便調用者確信在調用此函數後可以覆蓋到字符串末尾 addlen字節,再加上null term的一個字節。
* 如果已經有足夠的空閒空間,這個函數返回時不做任何操作,如果沒有足夠的空閒空間,它將分配缺失的部分,甚至更多:
* 當greedy為1時,放大比需要的更多,以避免將來在增量增長時需要重新分配。
* 當greedy為0時,將其放大到足夠大以便為addlen騰出空間。
* 注意:這不會改變sdslen()返回的sds字符串的長度,而只會改變我們擁有的空閒緩衝區空間。
*/
// 擴大sds空間
sds _sdsMakeRoomFor(sds s, size_t addlen, int greedy) {
void *sh, *newsh;
// 獲取剩餘可用的空間
size_t avail = sdsavail(s);
size_t len, newlen, reqlen;
// 獲取sds 具體數據類型
char type, oldtype = s[-1] & SDS_TYPE_MASK;
int hdrlen;
size_t usable;
/* Return ASAP if there is enough space left. */
// 可用空間足夠直接返回
if (avail >= addlen) return s;
// 已用字符長度
len = sdslen(s);
// sh 回溯到sds起始位置
sh = (char*)s-sdsHdrSize(oldtype);
// newlen 為最小需要的長度
reqlen = newlen = (len+addlen);
assert(newlen > len); /* Catch size_t overflow */
// 在newlen小於SDS_MAX_PREALLOC(1M),對newlen進行翻倍,在newlen大於SDS_MAX_PREALLOC的情況下,讓newlen加上SDS_MAX_PREALLOC。
if (greedy == 1) {
if (newlen < SDS_MAX_PREALLOC) // 小於1Kb 預分配2倍長度 = newlen + newlen
newlen *= 2;
else
newlen += SDS_MAX_PREALLOC; // 多餘1Mb 預分配 = newlen + 1Mb
}
// 獲取新長度的類型
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);
// 校驗是否溢出
assert(hdrlen + newlen + 1 > reqlen); /* Catch size_t overflow */
if (oldtype==type) {
/**
* 本質上是 使用 zrealloc_usable函數,指針ptr必須為指向堆內存空間的指針,即由malloc函數、calloc函數或realloc函數分配空間的指針。
* realloc函數將指針p指向的內存塊的大小改變為n字節。
* 1.如果n小於或等於p之前指向的空間大小,那麼。保持原有狀態不變。
* 2.如果n大於原來p之前指向的空間大小,那麼,系統將重新為p從堆上分配一塊大小為n的內存空間,同時,將原來指向空間的內容依次複製到新的內存空間上,p之前指向的空間被釋放。
* relloc函數分配的空間也是未初始化的。
*/
newsh = s_realloc_usable(sh, hdrlen+newlen+1, &usable);
// 申請空間失敗
if (newsh == NULL) return NULL;
// s指向新sds結構的buf開始位置
s = (char*)newsh+hdrlen;
} else {
/* Since the header size changes, need to move the string forward,
* and can't use realloc */
// 數據結構發生變更,協議頭部變更,需要從堆上重新申請數據空間
newsh = s_malloc_usable(hdrlen+newlen+1, &usable);
if (newsh == NULL) return NULL;
// 系統copy,越過頭部結構長度,複製s的有效數據集合
memcpy((char*)newsh+hdrlen, s, len+1);
// 釋放舊空間
s_free(sh);
// s執行新的空間,buf起始位置
s = (char*)newsh+hdrlen;
// flag 賦值 頭部的第三個有效字段
s[-1] = type;
// 更新有效數據長度
sdssetlen(s, len);
}
// 實際可用數據空間
usable = usable-hdrlen-1;
if (usable > sdsTypeMaxSize(type))
usable = sdsTypeMaxSize(type);
// 更新分配的空間值
sdssetalloc(s, usable);
return s;
}
3.9 釋放多餘空間
/* 對sds中多餘的空間進行釋放
* 重新分配sds字符串,使其末尾沒有空閒空間。所包含的字符串保持不變,
* 但下一個連接操作將需要重新分配。
* 調用之後,傳遞的sds字符串不再有效,所有引用必須用調用返回的新指針替換。
*/
sds sdsRemoveFreeSpace(sds s, int would_regrow) {
return sdsResize(s, sdslen(s), would_regrow);
}
/**
* 調整分配的大小,這可以使分配更大或更小,如果大小小於當前使用的len,數據將被截斷。
* 當將d_regrow參數設置為1時,它會阻止使用SDS_TYPE_5,這是在sds可能再次更改時所需要的。
* 無論實際分配大小如何,sdsAlloc大小都將被設置為請求的大小,這樣做是為了避免在調用者檢測到它有多餘的空間時重複調用該函數
*/
sds sdsResize(sds s, size_t size, int would_regrow) {
void *sh, *newsh;
char type, oldtype = s[-1] & SDS_TYPE_MASK;
int hdrlen, oldhdrlen = sdsHdrSize(oldtype);
size_t len = sdslen(s);
sh = (char*)s-oldhdrlen;
/* Return ASAP if the size is already good. */
if (sdsalloc(s) == size) return s;
/* Truncate len if needed. */
if (size < len) len = size;
/* Check what would be the minimum SDS header that is just good enough to
* fit this string. */
type = sdsReqType(size);
if (would_regrow) {
/* Don't use type 5, it is not good for strings that are expected to grow back. */
if (type == SDS_TYPE_5) type = SDS_TYPE_8;
}
hdrlen = sdsHdrSize(type);
/* If the type is the same, or can hold the size in it with low overhead
* (larger than SDS_TYPE_8), we just realloc(), letting the allocator
* to do the copy only if really needed. Otherwise if the change is
* huge, we manually reallocate the string to use the different header
* type. */
int use_realloc = (oldtype==type || (type < oldtype && type > SDS_TYPE_8));
size_t newlen = use_realloc ? oldhdrlen+size+1 : hdrlen+size+1;
int alloc_already_optimal = 0;
#if defined(USE_JEMALLOC)
/* je_nallocx returns the expected allocation size for the newlen.
* We aim to avoid calling realloc() when using Jemalloc if there is no
* change in the allocation size, as it incurs a cost even if the
* allocation size stays the same. */
alloc_already_optimal = (je_nallocx(newlen, 0) == zmalloc_size(sh));
#endif
if (use_realloc && !alloc_already_optimal) {
newsh = s_realloc(sh, newlen);
if (newsh == NULL) return NULL;
s = (char*)newsh+oldhdrlen;
} else if (!alloc_already_optimal) {
newsh = s_malloc(newlen);
if (newsh == NULL) return NULL;
memcpy((char*)newsh+hdrlen, s, len);
s_free(sh);
s = (char*)newsh+hdrlen;
s[-1] = type;
}
s[len] = 0;
sdssetlen(s, len);
sdssetalloc(s, size);
return s;
}
3.10 拼接字符串
將一個字符串拼接到原 sds 後面:
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);
// 結尾'\0'
s[curlen+len] = '\0';
return s;
}
3.11 拷貝
sds sdscpylen(sds s, const char *t, size_t len) {
// 長度不夠需要擴容
if (sdsalloc(s) < len) {
s = sdsMakeRoomFor(s,len-sdslen(s));
if (s == NULL) return NULL;
}
// 複製
memcpy(s, t, len);
// 末尾 '\0'
s[len] = '\0';
// 設置長度
sdssetlen(s, len);
return s;
}
4 SDS 的優點
-
- 獲取字符串的時間效率為
O (1)
- 獲取字符串的時間效率為
-
- 杜絕緩衝區的溢出,複製或者追加字符串之前,會對空間進行檢查與拓展,並且預分配一些容量,減少分片內存的次數。
-
- 可以存儲二進制數據,含有
\0則在讀取時不會被截斷。
- 可以存儲二進制數據,含有
-
- 可以複用一部分 c 原生字符串的函數。
作者: 秦懷,個人站點 秦懷雜貨店,縱使緩慢,馳而不息。