Redis【2】- SDS原始碼分析

秦怀杂货店發表於2024-12-07

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 版本已經是長這樣:

    1. Sdshdr5 從未被使用,我們只是直接訪問標誌位元組。
  • 然而,這裡文件標識 sdshdr5 的結構。
    1. 結構定義使用了__attribute__ ((__packed__))宣告為非記憶體對齊, 緊湊排列形式(取消編譯階段的記憶體最佳化對齊功能)
    1. 如果定義了一個 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 的優點

    1. 獲取字串的時間效率為 O (1)
    1. 杜絕緩衝區的溢位,複製或者追加字串之前,會對空間進行檢查與擴充,並且預分配一些容量,減少分片記憶體的次數。
    1. 可以儲存二進位制資料,含有 \0 則在讀取時不會被截斷。
    1. 可以複用一部分 c 原生字串的函式。

作者: 秦懷,個人站點 秦懷雜貨店,縱使緩慢,馳而不息。

相關文章